Skip to content

Commit 8f8f0ce

Browse files
committed
feat: Agent Mode improvements — activity feed, auto-apply, context indicator, enhanced prompt
1. Activity Feed (lib/agent-activity.ts + components/chat/agent-activity-feed.tsx): - Structured tracking of reads, edits, creates, searches, commands - Collapsible timeline with file change summary - Color-coded activity types with file chips - Replaces simple string thinkingTrail when activities present 2. Auto-apply edits in Agent mode: - Edit proposals auto-applied to editor buffer (same as full-access) - Status message shows which files were modified - Works alongside existing full-access auto-apply 3. Context indicator in chat header: - Shows operation count and files changed during streaming - Compact badge: '12 ops · 3 files' 4. Enhanced Agent mode prompt: - Instructs agent to read, edit, run, verify autonomously - Summarize changes after completion - Auto-diagnose and fix failures
1 parent bec3a63 commit 8f8f0ce

13 files changed

+526
-40
lines changed

app/globals.css

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5865,22 +5865,22 @@ p {
58655865
--text-tertiary: #7d7d7d;
58665866
--text-disabled: #646464;
58675867

5868-
--syntax-comment: #666666;
5869-
--syntax-keyword: #d4d4d4;
5870-
--syntax-string: #c7d2fe;
5871-
--syntax-string-escape: #e2e8f0;
5872-
--syntax-number: #f5f5f5;
5873-
--syntax-regexp: #d4d4d4;
5874-
--syntax-type: #cbd5e1;
5875-
--syntax-function: #f1f5f9;
5876-
--syntax-variable: #ededed;
5877-
--syntax-variable-predefined: #cfcfcf;
5878-
--syntax-constant: #e5e7eb;
5879-
--syntax-tag: #d4d4d4;
5880-
--syntax-attribute-name: #cbd5e1;
5881-
--syntax-attribute-value: #f1f5f9;
5882-
--syntax-delimiter: #8a8a8a;
5883-
--syntax-operator: #9a9a9a;
5868+
--syntax-comment: #5c6878;
5869+
--syntax-keyword: #9d81d8;
5870+
--syntax-string: #6abf9b;
5871+
--syntax-string-escape: #46b08c;
5872+
--syntax-number: #d4a264;
5873+
--syntax-regexp: #d47a8a;
5874+
--syntax-type: #5bafd4;
5875+
--syntax-function: #6aace0;
5876+
--syntax-variable: #d6dae2;
5877+
--syntax-variable-predefined: #cc8855;
5878+
--syntax-constant: #d4a264;
5879+
--syntax-tag: #d47070;
5880+
--syntax-attribute-name: #9d81d8;
5881+
--syntax-attribute-value: #6abf9b;
5882+
--syntax-delimiter: #485566;
5883+
--syntax-operator: #6b7c8a;
58845884

58855885
--border: #252525;
58865886
--border-hover: #343434;
@@ -5992,6 +5992,23 @@ p {
59925992
--shimmer-from: var(--bg-elevated);
59935993
--shimmer-via: var(--bg-subtle);
59945994

5995+
--syntax-comment: #8596a8;
5996+
--syntax-keyword: #6d4ebe;
5997+
--syntax-string: #267a5c;
5998+
--syntax-string-escape: #1a6850;
5999+
--syntax-number: #a85c20;
6000+
--syntax-regexp: #a0304c;
6001+
--syntax-type: #1878a8;
6002+
--syntax-function: #1a60b0;
6003+
--syntax-variable: #2a2d38;
6004+
--syntax-variable-predefined: #a04020;
6005+
--syntax-constant: #a85c20;
6006+
--syntax-tag: #a83030;
6007+
--syntax-attribute-name: #6d4ebe;
6008+
--syntax-attribute-value: #267a5c;
6009+
--syntax-delimiter: #556070;
6010+
--syntax-operator: #6a7a8a;
6011+
59956012
--radius-sm: 10px;
59966013
--radius-md: 14px;
59976014
--radius-lg: 20px;

components/agent-panel.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ export function AgentPanel() {
386386
const [sending, setSending] = useState(false)
387387
const [isStreaming, setIsStreaming] = useState(false)
388388
const [thinkingTrail, setThinkingTrail] = useState<string[]>([])
389+
const [agentActivities, setAgentActivities] = useState<import('@/lib/agent-activity').AgentActivity[]>([])
389390
const [activeDiff, setActiveDiff] = useState<{
390391
proposal: EditProposal
391392
messageId: string
@@ -542,6 +543,7 @@ export function AgentPanel() {
542543
setIsStreaming,
543544
setSending,
544545
setThinkingTrail,
546+
setAgentActivities,
545547
setMessages,
546548
getFile,
547549
}
@@ -885,7 +887,7 @@ export function AgentPanel() {
885887
? '[Mode: Ask — discuss and answer questions. Do not make code changes unless explicitly asked.]\n'
886888
: agentMode === 'plan'
887889
? '[Mode: Plan — You MUST respond with a structured plan before making any changes. Format your response as a numbered list where each step has a **bold title** followed by a description and affected files in backticks. Example:\n1. **Update auth module** — Add token refresh logic\n `lib/auth.ts`, `lib/api.ts`\n2. **Add tests** — Cover the new refresh flow\n `tests/auth.test.ts`\nAfter the user approves, execute each step sequentially. Do NOT make changes until approved.]\n'
888-
: '[Mode: Agent — make direct code changes and edits autonomously.]\n'
890+
: '[Mode: Agent — You are an autonomous coding agent. Make direct code changes without asking for permission. Read files to understand context, edit them to implement changes, run commands to verify your work. After making changes, briefly summarize what you did and which files were modified. If a change fails, diagnose and fix it automatically.]\n'
889891
return [modePrefix, context || '', attachCtx].filter(Boolean).join('\n\n')
890892
}, [agentMode, buildAttachmentContext, buildContext])
891893

@@ -1586,6 +1588,8 @@ export function AgentPanel() {
15861588

15871589
setSending(true)
15881590
streamStateRef.current.isSending = true
1591+
setAgentActivities([])
1592+
setThinkingTrail([])
15891593

15901594
// Build visual label for attachments
15911595
const attachLabels = buildAttachmentLabels()
@@ -1828,7 +1832,7 @@ export function AgentPanel() {
18281832
// ─── Auto-apply when full access is enabled ──────────────────
18291833
const autoAppliedRef = useRef(new Set<string>())
18301834
useEffect(() => {
1831-
if (permissions !== 'full') return
1835+
if (permissions !== 'full' && agentMode !== 'agent') return
18321836
const last = messages[messages.length - 1]
18331837
if (!last || last.role !== 'assistant' || !last.editProposals?.length) return
18341838
if (autoAppliedRef.current.has(last.id)) return
@@ -1846,10 +1850,10 @@ export function AgentPanel() {
18461850
id: crypto.randomUUID(),
18471851
role: 'system',
18481852
type: 'tool',
1849-
content: `Auto-applied edits to ${fileNames} (full access mode).`,
1853+
content: `Auto-applied edits to ${fileNames}${agentMode === 'agent' ? ' (agent mode)' : ' (full access mode)'}.`,
18501854
timestamp: Date.now(),
18511855
})
1852-
}, [messages, permissions, getFile, updateFileContent, openFile, appendMessage])
1856+
}, [messages, permissions, agentMode, getFile, updateFileContent, openFile, appendMessage])
18531857

18541858
// ─── Slash command suggestions ────────────────────────────────
18551859
const suggestions = useMemo(() => {
@@ -1971,6 +1975,7 @@ export function AgentPanel() {
19711975
streamStateRef.current.isSending = false
19721976
setIsStreaming(false)
19731977
setThinkingTrail([])
1978+
setAgentActivities([])
19741979
setInput('')
19751980
setContextAttachments([])
19761981
setImageAttachments([])
@@ -2052,6 +2057,8 @@ export function AgentPanel() {
20522057
isStreaming={isStreaming}
20532058
modelName={modelInfo.current || undefined}
20542059
contextTokens={contextTokens}
2060+
activityCount={agentActivities.length}
2061+
filesChanged={agentActivities.filter(a => a.type === 'edit' || a.type === 'write' || a.type === 'create').reduce((acc, a) => { if (a.file) acc.add(a.file); return acc }, new Set<string>()).size}
20552062
/>
20562063
{messages.length > 0 && (
20572064
<div className="flex items-center justify-between border-b border-[var(--border)] bg-[var(--bg-elevated)] px-2.5 py-1 shrink-0">
@@ -2145,6 +2152,7 @@ export function AgentPanel() {
21452152
streamBuffer={streamBuffer}
21462153
isStreaming={isStreaming}
21472154
thinkingTrail={thinkingTrail}
2155+
agentActivities={agentActivities}
21482156
agentMode={agentMode}
21492157
onShowDiff={handleShowDiff}
21502158
onQuickApply={handleQuickApply}

components/chat-header.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface Props {
1111
modelName?: string
1212
contextTokens?: number
1313
maxContextTokens?: number
14+
activityCount?: number
15+
filesChanged?: number
1416
}
1517

1618
export function ChatHeader({
@@ -20,6 +22,8 @@ export function ChatHeader({
2022
modelName,
2123
contextTokens = 0,
2224
maxContextTokens = 128000,
25+
activityCount = 0,
26+
filesChanged = 0,
2327
}: Props) {
2428
const { repo } = useRepo()
2529
const local = useLocal()
@@ -92,6 +96,15 @@ export function ChatHeader({
9296
</span>
9397
</span>
9498
)}
99+
{isStreaming && activityCount > 0 && (
100+
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-md border border-[var(--border)] bg-[var(--bg-subtle)] px-1.5 py-0.5 text-[9px] text-[var(--text-disabled)]">
101+
<Icon icon="lucide:activity" width={9} height={9} className="text-[var(--brand)]" />
102+
{activityCount} ops
103+
{filesChanged > 0 && (
104+
<span className="text-amber-400">· {filesChanged} files</span>
105+
)}
106+
</span>
107+
)}
95108
<span className="whitespace-nowrap text-[11px] text-[var(--text-disabled)]">
96109
{messageCount} messages
97110
</span>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Icon } from '@iconify/react'
5+
import {
6+
type AgentActivity,
7+
summarizeActivities,
8+
activityIcon,
9+
activityColor,
10+
} from '@/lib/agent-activity'
11+
12+
interface Props {
13+
activities: AgentActivity[]
14+
isRunning: boolean
15+
}
16+
17+
export function AgentActivityFeed({ activities, isRunning }: Props) {
18+
const [expanded, setExpanded] = useState(false)
19+
const summary = summarizeActivities(activities)
20+
21+
if (activities.length === 0) return null
22+
23+
const lastActivity = activities[activities.length - 1]
24+
25+
return (
26+
<div className="rounded-xl border border-[var(--border)] bg-[var(--bg-subtle)] overflow-hidden my-1.5">
27+
{/* Compact summary bar */}
28+
<button
29+
onClick={() => setExpanded(v => !v)}
30+
className="w-full flex items-center gap-2 px-3 py-2 text-left cursor-pointer hover:bg-[color-mix(in_srgb,var(--text-primary)_3%,transparent)] transition-colors"
31+
>
32+
{isRunning && (
33+
<span className="relative flex h-2 w-2 shrink-0">
34+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--brand)] opacity-50" />
35+
<span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--brand)]" />
36+
</span>
37+
)}
38+
{!isRunning && (
39+
<Icon icon="lucide:check-circle" width={12} className="text-[var(--color-additions)] shrink-0" />
40+
)}
41+
42+
{/* Current action or summary */}
43+
<span className="text-[11px] text-[var(--text-secondary)] flex-1 truncate">
44+
{isRunning
45+
? lastActivity.label
46+
: `${summary.totalActions} actions`}
47+
</span>
48+
49+
{/* File change badges */}
50+
<div className="flex items-center gap-1.5">
51+
{summary.filesEdited.length > 0 && (
52+
<span className="inline-flex items-center gap-0.5 text-[9px] font-medium text-amber-400">
53+
<Icon icon="lucide:file-pen-line" width={10} />
54+
{summary.filesEdited.length}
55+
</span>
56+
)}
57+
{summary.filesCreated.length > 0 && (
58+
<span className="inline-flex items-center gap-0.5 text-[9px] font-medium text-green-400">
59+
<Icon icon="lucide:file-plus" width={10} />
60+
{summary.filesCreated.length}
61+
</span>
62+
)}
63+
{summary.filesRead.length > 0 && (
64+
<span className="inline-flex items-center gap-0.5 text-[9px] font-medium text-blue-400">
65+
<Icon icon="lucide:file-search" width={10} />
66+
{summary.filesRead.length}
67+
</span>
68+
)}
69+
{summary.commandsRun > 0 && (
70+
<span className="inline-flex items-center gap-0.5 text-[9px] font-medium text-cyan-400">
71+
<Icon icon="lucide:terminal" width={10} />
72+
{summary.commandsRun}
73+
</span>
74+
)}
75+
</div>
76+
77+
<Icon
78+
icon={expanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
79+
width={11}
80+
className="text-[var(--text-disabled)] shrink-0"
81+
/>
82+
</button>
83+
84+
{/* Expanded timeline */}
85+
{expanded && (
86+
<div className="border-t border-[var(--border)] px-3 py-2 max-h-[240px] overflow-y-auto">
87+
<div className="relative flex flex-col gap-0">
88+
{/* Timeline line */}
89+
<div className="absolute left-[5px] top-2 bottom-2 w-px bg-[color-mix(in_srgb,var(--brand)_15%,var(--border))]" />
90+
91+
{activities.map((act, i) => {
92+
const isLast = i === activities.length - 1
93+
return (
94+
<div
95+
key={act.id}
96+
className="flex items-start gap-2.5 py-1 relative"
97+
>
98+
{/* Timeline dot */}
99+
<div className="relative z-[1] shrink-0 mt-0.5">
100+
{isLast && isRunning ? (
101+
<span className="relative flex h-[9px] w-[9px]">
102+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--brand)] opacity-50" />
103+
<span className="relative inline-flex rounded-full h-[9px] w-[9px] bg-[var(--brand)]" />
104+
</span>
105+
) : (
106+
<span className={`block w-[9px] h-[9px] rounded-full border-2 ${
107+
act.status === 'error'
108+
? 'border-[var(--color-deletions)] bg-[var(--color-deletions)]'
109+
: 'border-[color-mix(in_srgb,var(--brand)_40%,var(--border))] bg-[var(--bg-subtle)]'
110+
}`} />
111+
)}
112+
</div>
113+
114+
{/* Activity content */}
115+
<div className="flex items-center gap-1.5 min-w-0 flex-1">
116+
<Icon
117+
icon={activityIcon(act.type)}
118+
width={11}
119+
className={`shrink-0 ${activityColor(act.type)}`}
120+
/>
121+
<span className={`text-[10px] truncate ${
122+
isLast && isRunning ? 'text-[var(--text-primary)] font-medium' : 'text-[var(--text-disabled)]'
123+
}`}>
124+
{act.label}
125+
</span>
126+
</div>
127+
128+
{/* File chip */}
129+
{act.file && (
130+
<span className="text-[8px] font-mono text-[var(--text-disabled)] truncate max-w-[100px]">
131+
{act.file.split('/').pop()}
132+
</span>
133+
)}
134+
</div>
135+
)
136+
})}
137+
</div>
138+
139+
{/* Changed files summary */}
140+
{!isRunning && summary.filesEdited.length > 0 && (
141+
<div className="mt-2 pt-2 border-t border-[var(--border)]">
142+
<p className="text-[9px] uppercase tracking-wider text-[var(--text-disabled)] font-medium mb-1">Changed Files</p>
143+
<div className="flex flex-wrap gap-1">
144+
{[...summary.filesEdited, ...summary.filesCreated].map(f => (
145+
<span
146+
key={f}
147+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-mono bg-[color-mix(in_srgb,var(--brand)_6%,transparent)] border border-[color-mix(in_srgb,var(--brand)_20%,var(--border))] text-[var(--text-secondary)]"
148+
>
149+
<Icon icon={summary.filesCreated.includes(f) ? 'lucide:file-plus' : 'lucide:file-pen-line'} width={9} />
150+
{f.split('/').pop()}
151+
</span>
152+
))}
153+
</div>
154+
</div>
155+
)}
156+
</div>
157+
)}
158+
</div>
159+
)
160+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client'
2+
3+
import { Icon } from '@iconify/react'
4+
import { type AgentActivity, summarizeActivities } from '@/lib/agent-activity'
5+
6+
interface Props {
7+
activities: AgentActivity[]
8+
isStreaming: boolean
9+
messageCount: number
10+
}
11+
12+
/**
13+
* Compact context indicator shown during agent runs.
14+
* Displays file/action counts as a slim inline badge.
15+
*/
16+
export function ContextIndicator({ activities, isStreaming, messageCount }: Props) {
17+
if (!isStreaming && activities.length === 0) return null
18+
19+
const summary = summarizeActivities(activities)
20+
const totalFiles = summary.filesRead.length + summary.filesEdited.length + summary.filesCreated.length
21+
22+
return (
23+
<div className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-[color-mix(in_srgb,var(--brand)_8%,transparent)] border border-[color-mix(in_srgb,var(--brand)_15%,var(--border))] text-[9px] text-[var(--text-secondary)]">
24+
{isStreaming && (
25+
<span className="relative flex h-1.5 w-1.5 shrink-0">
26+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--brand)] opacity-60" />
27+
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[var(--brand)]" />
28+
</span>
29+
)}
30+
{totalFiles > 0 && (
31+
<span className="inline-flex items-center gap-0.5">
32+
<Icon icon="lucide:files" width={9} />
33+
{totalFiles}
34+
</span>
35+
)}
36+
{summary.totalActions > 0 && (
37+
<span className="inline-flex items-center gap-0.5">
38+
<Icon icon="lucide:zap" width={9} />
39+
{summary.totalActions}
40+
</span>
41+
)}
42+
{messageCount > 0 && (
43+
<span className="inline-flex items-center gap-0.5">
44+
<Icon icon="lucide:message-square" width={9} />
45+
{messageCount}
46+
</span>
47+
)}
48+
</div>
49+
)
50+
}

0 commit comments

Comments
 (0)