Skip to content

Commit f1f469e

Browse files
jherforthclaude
andcommitted
Generation-first flow: diff cards, live preview by default, activity rows
- AI file changes now render per-file diff cards in chat: collapsed rows with +/− counts, expanding to a hunked unified diff (lib/diff.ts, LCS with prefix/suffix trim and a coarse fallback for huge files). - The preview pane is open by default (persisted stores migrate once) and auto-reloads whenever the AI or a manual save writes files, via a monastery:files-written event. - @read/@search context pulls and edit-recovery chatter render as slim activity rows instead of chat bubbles; only messages needing the user keep a bubble. - Typography floor raised: all 9/10px text is now 11px minimum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3eb94e2 commit f1f469e

14 files changed

Lines changed: 273 additions & 22 deletions

packages/web-ui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,8 @@ export default function App() {
484484
markTabSaved();
485485
// Keep the LLM context map in sync with the saved file.
486486
setAllFileContents(prev => ({ ...prev, [currentFile]: editorContent }));
487+
// The live preview listens for this and reloads.
488+
window.dispatchEvent(new CustomEvent('monastery:files-written'));
487489
} catch (e) {
488490
console.error('Save failed:', e);
489491
}

packages/web-ui/src/components/ChatPane.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useRef, useEffect } from 'react';
22
import { Send, Paperclip, X, StopCircle, Copy, Check, RotateCcw, Brain, ChevronDown, ChevronRight, Bot, Loader2, Coins, MessageSquare, Plus, Trash2, SlidersHorizontal, Settings } from 'lucide-react';
33
import { Message, Attachment, SessionInfo } from '../types';
4+
import { DiffCard } from './DiffCard';
45
import { useAppStore } from '../store/useAppStore';
56
import { useSnapshots } from '../hooks/useSnapshots';
67
import { useAgents } from '../hooks/useAgents';
@@ -487,6 +488,17 @@ export function ChatPane({
487488
</div>
488489
) : (
489490
messages.map((message) => {
491+
// Activity chatter (context pulls, recovery steps) renders as a slim inline row,
492+
// not a bubble — the conversation stays about the conversation.
493+
if (message.kind === 'activity') {
494+
return (
495+
<div key={message.id} className="flex justify-start">
496+
<div className="px-2 py-0.5 text-[11px] text-monastery-text-muted leading-relaxed">
497+
{renderInline(message.content)}
498+
</div>
499+
</div>
500+
);
501+
}
490502
// Compact timestamp: time-of-day for today's messages, date + time for older ones.
491503
// Hidden when the timestamp is missing/unparseable (e.g. a malformed session row).
492504
const ts = new Date(message.timestamp);
@@ -515,7 +527,7 @@ export function ChatPane({
515527
{message.role === 'user' && message.agentLabels && message.agentLabels.length > 0 && (
516528
<div className="flex flex-wrap gap-1 mb-1.5">
517529
{message.agentLabels.map((label, i) => (
518-
<span key={i} className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] bg-white/15 font-medium">
530+
<span key={i} className="inline-flex items-center px-1.5 py-0.5 rounded text-[11px] bg-white/15 font-medium">
519531
{label}
520532
</span>
521533
))}
@@ -536,7 +548,7 @@ export function ChatPane({
536548
)}
537549
{/* Badge showing which backend answered (Hermes agent vs local LLM) */}
538550
{message.role === 'assistant' && message.via === 'hermes' && (
539-
<span className="inline-flex items-center gap-1 mb-1.5 px-1.5 py-0.5 rounded text-[10px] font-medium bg-monastery-lantern/15 text-monastery-lantern">
551+
<span className="inline-flex items-center gap-1 mb-1.5 px-1.5 py-0.5 rounded text-[11px] font-medium bg-monastery-lantern/15 text-monastery-lantern">
540552
<Bot size={10} /> via Hermes
541553
</span>
542554
)}
@@ -547,6 +559,10 @@ export function ChatPane({
547559
<div className={`text-sm ${message.role === 'system' ? 'text-monastery-text-secondary' : ''}`}>
548560
{renderContent(message.content)}
549561
</div>
562+
{/* Per-file diff cards on AI-change feedback messages */}
563+
{message.fileChanges && message.fileChanges.map(change => (
564+
<DiffCard key={change.path} change={change} />
565+
))}
550566
{/* Auto-continuation status + token usage (when the endpoint reports usage). */}
551567
{message.role === 'assistant' && (message.continuing || (message.autoContinueCount ?? 0) > 0 || message.usage?.total_tokens) && (
552568
<div className="mt-1.5 flex items-center flex-wrap gap-x-3 gap-y-1 text-[11px] text-monastery-text-muted">
@@ -617,7 +633,7 @@ export function ChatPane({
617633
{/* Timestamp footer on every message */}
618634
{timeLabel && (
619635
<div
620-
className={`mt-1 text-[10px] leading-none ${
636+
className={`mt-1 text-[11px] leading-none ${
621637
message.role === 'user'
622638
? 'text-white/50 text-right'
623639
: message.role === 'system'

packages/web-ui/src/components/CommandPalette.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function CommandPalette({ commands, files = [], onOpenFile }: CommandPale
119119
placeholder="Type a command or file name…"
120120
className="flex-1 bg-transparent text-sm text-monastery-text-primary placeholder-monastery-text-muted focus:outline-none"
121121
/>
122-
<kbd className="text-[10px] text-monastery-text-muted border border-monastery-dark-border rounded px-1">Esc</kbd>
122+
<kbd className="text-[11px] text-monastery-text-muted border border-monastery-dark-border rounded px-1">Esc</kbd>
123123
</div>
124124
<div ref={listRef} className="max-h-80 overflow-y-auto py-1">
125125
{matches.length === 0 && (
@@ -132,7 +132,7 @@ export function CommandPalette({ commands, files = [], onOpenFile }: CommandPale
132132
return (
133133
<div key={m.kind === 'command' ? m.item.id : `file-${m.path}`}>
134134
{header && (
135-
<div className="px-3 pt-2 pb-1 text-[10px] font-medium uppercase tracking-wider text-monastery-text-muted">
135+
<div className="px-3 pt-2 pb-1 text-[11px] font-medium uppercase tracking-wider text-monastery-text-muted">
136136
{header}
137137
</div>
138138
)}
@@ -149,7 +149,7 @@ export function CommandPalette({ commands, files = [], onOpenFile }: CommandPale
149149
{m.kind === 'file' && <FileCode size={13} className="text-monastery-text-muted shrink-0" />}
150150
<span className="flex-1 truncate">{m.kind === 'command' ? m.item.label : m.path}</span>
151151
{m.kind === 'command' && m.item.hint && (
152-
<span className="text-[10px] text-monastery-text-muted shrink-0">{m.item.hint}</span>
152+
<span className="text-[11px] text-monastery-text-muted shrink-0">{m.item.hint}</span>
153153
)}
154154
{i === selected && <CornerDownLeft size={12} className="text-monastery-text-muted shrink-0" />}
155155
</button>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useState, useMemo } from 'react';
2+
import { ChevronDown, ChevronRight, FilePlus2, FileDiff } from 'lucide-react';
3+
import { FileChange } from '../types';
4+
import { diffLines } from '../lib/diff';
5+
6+
/**
7+
* Collapsed: one row per changed file with +added/−removed counts.
8+
* Expanded: a unified line diff (computed lazily on first expand).
9+
*/
10+
export function DiffCard({ change }: { change: FileChange }) {
11+
const [open, setOpen] = useState(false);
12+
const isNew = change.before === '';
13+
14+
// Counts are cheap enough to show collapsed; the hunked render happens in the same pass.
15+
const diff = useMemo(() => diffLines(change.before, change.after), [change]);
16+
17+
return (
18+
<div className="mt-1.5 rounded-lg border border-monastery-dark-border overflow-hidden text-left">
19+
<button
20+
onClick={() => setOpen(o => !o)}
21+
className="w-full flex items-center gap-2 px-2.5 py-1.5 bg-monastery-dark-bg hover:bg-monastery-dark-tertiary transition-colors"
22+
title={open ? 'Hide diff' : 'Show diff'}
23+
>
24+
{open ? <ChevronDown size={12} className="text-monastery-text-muted shrink-0" /> : <ChevronRight size={12} className="text-monastery-text-muted shrink-0" />}
25+
{isNew
26+
? <FilePlus2 size={12} className="text-green-400 shrink-0" />
27+
: <FileDiff size={12} className="text-monastery-lantern shrink-0" />}
28+
<span className="text-xs font-mono text-monastery-text-primary truncate flex-1">{change.path}</span>
29+
{isNew && <span className="text-[11px] text-green-400 shrink-0">new</span>}
30+
{diff.added > 0 && <span className="text-[11px] text-green-400 shrink-0">+{diff.added}</span>}
31+
{diff.removed > 0 && <span className="text-[11px] text-red-400 shrink-0">{diff.removed}</span>}
32+
</button>
33+
{open && (
34+
<pre className="max-h-72 overflow-auto bg-monastery-dark-bg border-t border-monastery-dark-border p-0 text-[11px] leading-relaxed font-mono">
35+
<code className="block min-w-max">
36+
{diff.lines.map((line, i) =>
37+
line.type === 'skip' ? (
38+
<span key={i} className="block px-2.5 text-monastery-text-muted select-none">{line.count} unchanged lines</span>
39+
) : (
40+
<span
41+
key={i}
42+
className={`block px-2.5 whitespace-pre ${
43+
line.type === 'add' ? 'bg-green-400/10 text-green-300'
44+
: line.type === 'del' ? 'bg-red-400/10 text-red-300'
45+
: 'text-monastery-text-secondary'
46+
}`}
47+
>
48+
{line.type === 'add' ? '+' : line.type === 'del' ? '−' : ' '} {line.text}
49+
</span>
50+
)
51+
)}
52+
{diff.truncated && (
53+
<span className="block px-2.5 py-1 text-monastery-text-muted">⋯ diff truncated (large change)</span>
54+
)}
55+
</code>
56+
</pre>
57+
)}
58+
</div>
59+
);
60+
}

packages/web-ui/src/components/GitForgeSetup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ function RepoBrowser({
731731
>
732732
{b.name}
733733
{b.is_default && (
734-
<span className="ml-1 text-[10px] opacity-70">(default)</span>
734+
<span className="ml-1 text-[11px] opacity-70">(default)</span>
735735
)}
736736
</button>
737737
))}

packages/web-ui/src/components/PreviewPane.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export function PreviewPane({ projectId }: PreviewPaneProps) {
1919
}
2020
}, [projectId]);
2121

22+
// Auto-refresh whenever the AI (or a manual save) writes files — generation-first flow:
23+
// the running app updates as the code lands, no manual refresh needed.
24+
useEffect(() => {
25+
const handler = () => setPreviewKey(k => k + 1);
26+
window.addEventListener('monastery:files-written', handler);
27+
return () => window.removeEventListener('monastery:files-written', handler);
28+
}, []);
29+
2230
return (
2331
<div className="h-full flex flex-col bg-monastery-dark-bg">
2432
<div className="flex items-center justify-between px-3 py-2 border-b border-monastery-dark-border bg-monastery-dark-surface">

packages/web-ui/src/components/SelfHostWizard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export function SelfHostWizard({ isOpen, onClose, onFixBuildError }: SelfHostWiz
229229
className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-xs transition-colors ${
230230
i === step ? 'bg-monastery-pine/20 text-monastery-pine font-medium' :
231231
i < step ? 'text-monastery-text-muted hover:text-monastery-text-secondary' : 'text-monastery-text-muted/50'}`}>
232-
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-bold ${
232+
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-[11px] font-bold ${
233233
i === step ? 'bg-monastery-pine text-white' : i < step ? 'bg-green-400/20 text-green-400' : 'bg-monastery-dark-border text-monastery-text-muted'}`}>
234234
{i < step ? '✓' : i + 1}</span>
235235
<span className="hidden sm:inline">{label}</span>

packages/web-ui/src/components/SourceShipDrawer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,14 @@ export function SourceShipDrawer({ open, onClose, onCommitComplete, onRestoreCom
197197
<div key={snap.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-monastery-dark-surface transition-colors text-sm text-monastery-text-secondary">
198198
<div className="flex-1 min-w-0">
199199
<div className="text-xs truncate">{snap.name}</div>
200-
<div className="text-[10px] text-monastery-text-muted">
200+
<div className="text-[11px] text-monastery-text-muted">
201201
{new Date(snap.created_at).toLocaleString()} · {snap.files_count} files
202202
</div>
203203
</div>
204204
<button
205205
onClick={() => handleRestoreSnapshot(snap.id)}
206206
disabled={restoringId === snap.id}
207-
className="px-2 py-0.5 text-[10px] bg-monastery-dark-tertiary hover:bg-monastery-lantern hover:text-monastery-dark-bg rounded transition-colors disabled:opacity-50 shrink-0"
207+
className="px-2 py-0.5 text-[11px] bg-monastery-dark-tertiary hover:bg-monastery-lantern hover:text-monastery-dark-bg rounded transition-colors disabled:opacity-50 shrink-0"
208208
title="Revert project to this snapshot"
209209
>
210210
{restoringId === snap.id ? '...' : 'Revert'}

packages/web-ui/src/components/TaskDrawer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,12 @@ export function TaskDrawer({ open, onClose, projectId, workflow, onRunStage, onH
218218
</div>
219219

220220
{verifyOut && (
221-
<pre className="text-[10px] bg-monastery-dark-surface border border-monastery-dark-border rounded-lg p-2 max-h-48 overflow-auto whitespace-pre-wrap">{verifyOut}</pre>
221+
<pre className="text-[11px] bg-monastery-dark-surface border border-monastery-dark-border rounded-lg p-2 max-h-48 overflow-auto whitespace-pre-wrap">{verifyOut}</pre>
222222
)}
223223

224224
{/* Exit-state chain (chain of custody) */}
225225
{activeTask.exit_states.length > 0 && (
226-
<div className="text-[10px] text-monastery-text-muted space-y-0.5">
226+
<div className="text-[11px] text-monastery-text-muted space-y-0.5">
227227
{activeTask.exit_states.map((e, i) => (
228228
<div key={i} className="flex items-center gap-1">
229229
<span className={e.status === 'failed' ? 'text-red-400' : 'text-green-400'}>{e.status === 'failed' ? '✗' : '✓'}</span>
@@ -233,7 +233,7 @@ export function TaskDrawer({ open, onClose, projectId, workflow, onRunStage, onH
233233
))}
234234
</div>
235235
)}
236-
<button onClick={() => refresh()} className="text-[10px] underline text-monastery-text-muted hover:text-monastery-text-primary">refresh</button>
236+
<button onClick={() => refresh()} className="text-[11px] underline text-monastery-text-muted hover:text-monastery-text-primary">refresh</button>
237237
</>
238238
)}
239239

packages/web-ui/src/components/ui/StatusBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const STYLES: Record<Variant, string> = {
1212
/** One vocabulary for connection/health status instead of ad-hoc emoji and color spans. */
1313
export function StatusBadge({ variant, children }: { variant: Variant; children: ReactNode }) {
1414
return (
15-
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${STYLES[variant]}`}>
15+
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium ${STYLES[variant]}`}>
1616
<span className="w-1.5 h-1.5 rounded-full bg-current" />
1717
{children}
1818
</span>

0 commit comments

Comments
 (0)