@pennyfarthing/cyclist 9.2.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/hotspots.d.ts +3 -0
- package/dist/api/hotspots.d.ts.map +1 -0
- package/dist/api/hotspots.js +54 -0
- package/dist/api/hotspots.js.map +1 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/settings.d.ts +1 -1
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +44 -17
- package/dist/api/settings.js.map +1 -1
- package/dist/main.d.ts +4 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +43 -39
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/dist/story-parser.d.ts +17 -0
- package/dist/story-parser.d.ts.map +1 -1
- package/dist/story-parser.js +183 -13
- package/dist/story-parser.js.map +1 -1
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +5 -4
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
- package/src/public/App.tsx +2 -0
- package/src/public/components/ControlBar.tsx +1 -1
- package/src/public/components/DockviewWorkspace.tsx +4 -0
- package/src/public/components/FontPicker/index.tsx +118 -33
- package/src/public/components/FullFileTree.tsx +223 -0
- package/src/public/components/Message.tsx +32 -10
- package/src/public/components/MessageView.tsx +176 -93
- package/src/public/components/PersonaHeader.tsx +45 -15
- package/src/public/components/SubagentSpan.tsx +15 -8
- package/src/public/components/ThemePalette/ThemePalette.css +2 -0
- package/src/public/components/ToolStack.tsx +23 -13
- package/src/public/components/panels/AuditLogPanel.tsx +140 -66
- package/src/public/components/panels/ChangedPanel.tsx +30 -44
- package/src/public/components/panels/HotspotsPanel.tsx +365 -0
- package/src/public/components/panels/MessagePanel.tsx +14 -2
- package/src/public/components/panels/SettingsPanel.tsx +10 -10
- package/src/public/components/panels/WorkflowPanel.tsx +85 -12
- package/src/public/components/panels/index.ts +1 -0
- package/src/public/components/ui/switch.tsx +2 -2
- package/src/public/css/theme-system.css +71 -43
- package/src/public/hooks/useFileBrowser.ts +71 -0
- package/src/public/hooks/useHotspots.ts +113 -0
- package/src/public/hooks/useStory.ts +12 -3
- package/src/public/images/cyclist-dark.png +0 -0
- package/src/public/images/cyclist-light.png +0 -0
- package/src/public/styles/tailwind.css +428 -69
- package/src/public/types/message.ts +4 -0
- package/src/public/utils/slash-commands.ts +1 -1
- package/src/public/utils/toolStackGrouper.ts +4 -5
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* - Render messages list with proper roles
|
|
9
9
|
* - Handle streaming content display
|
|
10
10
|
* - Markdown rendering with syntax highlighting
|
|
11
|
-
* - Tool call blocks
|
|
11
|
+
* - Tool call blocks with stacking
|
|
12
12
|
* - Subagent span grouping
|
|
13
|
+
* - Turn-based grouping with speaker labels
|
|
13
14
|
* - Auto-scroll behavior
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import React, { useRef, useState, useCallback, useMemo } from 'react';
|
|
18
|
+
import { Badge } from '@/components/ui/badge';
|
|
17
19
|
import { Button } from '@/components/ui/button';
|
|
18
20
|
import MessageList, { MessageListHandle } from './MessageList';
|
|
19
21
|
import Message from './Message';
|
|
@@ -23,8 +25,23 @@ import SubagentSpan from './SubagentSpan';
|
|
|
23
25
|
import QuickActions from './QuickActions';
|
|
24
26
|
import { isSkillContent } from '../utils/messageFilters';
|
|
25
27
|
import { groupToolsIntoStacks, ToolStackData } from '../utils/toolStackGrouper';
|
|
28
|
+
import { usePersona } from '../hooks/usePersona';
|
|
29
|
+
import { useStatsStrip } from '../hooks/useStatsStrip';
|
|
26
30
|
import type { MessageData } from '../types/message';
|
|
27
31
|
|
|
32
|
+
// Agent colors matching CLI statusbar (from PersonaHeader)
|
|
33
|
+
const AGENT_COLORS: Record<string, string> = {
|
|
34
|
+
pm: '#a78bfa', sm: '#60a5fa', dev: '#4ade80', tea: '#2dd4bf',
|
|
35
|
+
reviewer: '#f87171', architect: '#fb923c', devops: '#22d3ee',
|
|
36
|
+
'ux-designer': '#f0abfc', 'tech-writer': '#e5e5e5', orchestrator: '#e879f9',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const AGENT_ABBREV: Record<string, string> = {
|
|
40
|
+
pm: 'PM', sm: 'SM', dev: 'DEV', tea: 'TEA', reviewer: 'REV',
|
|
41
|
+
architect: 'ARC', devops: 'OPS', 'ux-designer': 'UX', 'tech-writer': 'TW',
|
|
42
|
+
orchestrator: 'ORC',
|
|
43
|
+
};
|
|
44
|
+
|
|
28
45
|
interface MessageViewProps {
|
|
29
46
|
messages: MessageData[];
|
|
30
47
|
}
|
|
@@ -41,9 +58,36 @@ interface ToolStackGroup {
|
|
|
41
58
|
stack: ToolStackData;
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
type RenderItem = MessageData | SubagentGroup | ToolStackGroup;
|
|
62
|
+
|
|
63
|
+
interface Turn {
|
|
64
|
+
speaker: 'user' | 'agent';
|
|
65
|
+
items: RenderItem[];
|
|
66
|
+
timestamp: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatTurnTime(timestamp: number): string {
|
|
70
|
+
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Classify an item as 'user' or 'agent' for turn grouping.
|
|
75
|
+
* Tools, subagents, and stacks are all part of the agent's turn.
|
|
76
|
+
*/
|
|
77
|
+
function speakerOf(item: RenderItem): 'user' | 'agent' {
|
|
78
|
+
if ('isToolStack' in item || 'messages' in item) return 'agent';
|
|
79
|
+
const msg = item as MessageData;
|
|
80
|
+
return (msg.type === 'user' || msg.type === 'bell_injected') ? 'user' : 'agent';
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
export default function MessageView({ messages }: MessageViewProps): React.ReactElement {
|
|
45
84
|
const messageListRef = useRef<MessageListHandle>(null);
|
|
46
85
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
86
|
+
const { persona } = usePersona();
|
|
87
|
+
const { projectInfo } = useStatsStrip();
|
|
88
|
+
|
|
89
|
+
// Persist subagent collapsed state across re-renders/remounts
|
|
90
|
+
const subagentCollapsedRef = useRef<Map<string, boolean>>(new Map());
|
|
47
91
|
|
|
48
92
|
const handleScrollChange = useCallback((atBottom: boolean) => {
|
|
49
93
|
setIsAtBottom(atBottom);
|
|
@@ -53,134 +97,133 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
53
97
|
messageListRef.current?.scrollToBottom('smooth');
|
|
54
98
|
}, []);
|
|
55
99
|
|
|
56
|
-
// Find the last assistant message for QuickActions
|
|
100
|
+
// Find the last non-streaming assistant message for QuickActions
|
|
57
101
|
const lastAssistantMessage = useMemo(() => {
|
|
58
102
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
59
|
-
if (messages[i].type === 'agent' && !messages[i].isStreaming)
|
|
60
|
-
return messages[i];
|
|
61
|
-
}
|
|
103
|
+
if (messages[i].type === 'agent' && !messages[i].isStreaming) return messages[i];
|
|
62
104
|
}
|
|
63
105
|
return null;
|
|
64
106
|
}, [messages]);
|
|
65
107
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
108
|
+
// Single memo: messages → flat render items → turns
|
|
109
|
+
const { turns, toolResults, lastAgentItemIndex } = useMemo(() => {
|
|
110
|
+
// 1. Index tool results by ID
|
|
111
|
+
const results = new Map<string, MessageData>();
|
|
112
|
+
for (const msg of messages) {
|
|
113
|
+
if (msg.type === 'tool_result' && msg.tool_id) results.set(msg.tool_id, msg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Filter and collect subagent groups in one pass
|
|
117
|
+
const filtered: MessageData[] = [];
|
|
69
118
|
const subagentGroups = new Map<string, SubagentGroup>();
|
|
70
119
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (msg.type === 'tool_result' && msg.tool_id) {
|
|
75
|
-
toolResults.set(msg.tool_id, msg);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Second pass: filter messages and group by subagent (excluding tool_use for now)
|
|
80
|
-
const filteredMessages: MessageData[] = [];
|
|
81
|
-
messages.forEach(msg => {
|
|
82
|
-
// Filter out skill content from user messages (MSSCI-12783)
|
|
83
|
-
if (msg.type === 'user' && isSkillContent(msg.content)) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// Skip tool_result - rendered with tool_use
|
|
87
|
-
if (msg.type === 'tool_result') {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
// Skip messages with parent_id (subagent messages handled separately)
|
|
120
|
+
for (const msg of messages) {
|
|
121
|
+
if (msg.type === 'tool_result') continue;
|
|
122
|
+
if (msg.type === 'user' && isSkillContent(msg.content)) continue;
|
|
91
123
|
if (msg.parent_id) {
|
|
92
124
|
let group = subagentGroups.get(msg.parent_id);
|
|
93
125
|
if (!group) {
|
|
94
|
-
group = {
|
|
95
|
-
parent_id: msg.parent_id,
|
|
96
|
-
type: msg.subagent_type || 'unknown',
|
|
97
|
-
name: msg.subagent_name || 'unnamed',
|
|
98
|
-
messages: [],
|
|
99
|
-
};
|
|
126
|
+
group = { parent_id: msg.parent_id, type: msg.subagent_type || 'unknown', name: msg.subagent_name || 'unnamed', messages: [] };
|
|
100
127
|
subagentGroups.set(msg.parent_id, group);
|
|
101
128
|
}
|
|
102
129
|
group.messages.push(msg);
|
|
103
|
-
|
|
130
|
+
continue;
|
|
104
131
|
}
|
|
105
|
-
|
|
106
|
-
}
|
|
132
|
+
filtered.push(msg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Build flat render list, replacing consecutive tool_use runs with stacks
|
|
136
|
+
const stacks = groupToolsIntoStacks(filtered);
|
|
137
|
+
const stackByToolId = new Map<string, ToolStackData>();
|
|
138
|
+
for (const stack of stacks) {
|
|
139
|
+
for (const tool of stack.tools) stackByToolId.set(tool.tool_id, stack);
|
|
140
|
+
}
|
|
107
141
|
|
|
108
|
-
|
|
109
|
-
const
|
|
142
|
+
const items: RenderItem[] = [];
|
|
143
|
+
const emittedStacks = new Set<string>();
|
|
144
|
+
const emittedSubagents = new Set<string>();
|
|
110
145
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
for (const msg of filtered) {
|
|
147
|
+
if (msg.type === 'tool_use' && msg.tool_id && stackByToolId.has(msg.tool_id)) {
|
|
148
|
+
const stack = stackByToolId.get(msg.tool_id)!;
|
|
149
|
+
if (!emittedStacks.has(stack.stackId)) {
|
|
150
|
+
emittedStacks.add(stack.stackId);
|
|
151
|
+
items.push({ isToolStack: true, stack });
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
items.push(msg);
|
|
116
155
|
}
|
|
117
|
-
}
|
|
156
|
+
}
|
|
118
157
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
// Insert subagent groups at the position of their first parent_id occurrence
|
|
159
|
+
// (They appear in the agent's turn, after the Task tool_use that spawned them)
|
|
160
|
+
// For now, just append them — they'll naturally land in the agent turn
|
|
161
|
+
for (const [, group] of subagentGroups) {
|
|
162
|
+
if (!emittedSubagents.has(group.parent_id)) {
|
|
163
|
+
emittedSubagents.add(group.parent_id);
|
|
164
|
+
items.push(group);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
122
167
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Skip individual rendering - handled by ToolStack
|
|
141
|
-
} else if (msg.type === 'tool_use') {
|
|
142
|
-
// Single tool - render normally
|
|
143
|
-
result.push(msg);
|
|
144
|
-
pendingStack = null;
|
|
168
|
+
// 4. Find last agent message index (for throb control)
|
|
169
|
+
let lastAgent = -1;
|
|
170
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
171
|
+
const item = items[i];
|
|
172
|
+
if (!('isToolStack' in item) && !('messages' in item) && (item as MessageData).type === 'agent') {
|
|
173
|
+
lastAgent = i;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 5. Group into turns
|
|
179
|
+
const turnList: Turn[] = [];
|
|
180
|
+
for (const item of items) {
|
|
181
|
+
const speaker = speakerOf(item);
|
|
182
|
+
const last = turnList[turnList.length - 1];
|
|
183
|
+
if (last && last.speaker === speaker) {
|
|
184
|
+
last.items.push(item);
|
|
145
185
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
186
|
+
turnList.push({
|
|
187
|
+
speaker,
|
|
188
|
+
items: [item],
|
|
189
|
+
timestamp: ('timestamp' in item) ? (item as MessageData).timestamp : Date.now(),
|
|
190
|
+
});
|
|
149
191
|
}
|
|
150
|
-
}
|
|
192
|
+
}
|
|
151
193
|
|
|
152
|
-
return {
|
|
194
|
+
return { turns: turnList, toolResults: results, lastAgentItemIndex: lastAgent };
|
|
153
195
|
}, [messages]);
|
|
154
196
|
|
|
155
|
-
const renderItem = (item:
|
|
156
|
-
|
|
157
|
-
if ('isToolStack' in item && item.isToolStack) {
|
|
197
|
+
const renderItem = (item: RenderItem, globalIndex: number, isFirstInTurn: boolean) => {
|
|
198
|
+
if ('isToolStack' in item) {
|
|
158
199
|
return (
|
|
159
200
|
<ToolStack
|
|
160
201
|
key={`stack-${item.stack.stackId}`}
|
|
161
202
|
stack={item.stack}
|
|
162
|
-
toolResults={
|
|
203
|
+
toolResults={toolResults as Map<string, { type: 'tool_result'; tool_id: string; content: string; timestamp: number }>}
|
|
163
204
|
/>
|
|
164
205
|
);
|
|
165
206
|
}
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
208
|
+
if ('messages' in item && 'parent_id' in item) {
|
|
209
|
+
const group = item as SubagentGroup;
|
|
210
|
+
const collapsed = subagentCollapsedRef.current.get(group.parent_id) ?? true;
|
|
169
211
|
return (
|
|
170
212
|
<SubagentSpan
|
|
171
|
-
key={`subagent-${
|
|
172
|
-
type={
|
|
173
|
-
name={
|
|
174
|
-
messages={
|
|
213
|
+
key={`subagent-${group.parent_id}`}
|
|
214
|
+
type={group.type}
|
|
215
|
+
name={group.name}
|
|
216
|
+
messages={group.messages as any}
|
|
217
|
+
defaultCollapsed={collapsed}
|
|
218
|
+
onCollapseChange={(c) => subagentCollapsedRef.current.set(group.parent_id, c)}
|
|
175
219
|
/>
|
|
176
220
|
);
|
|
177
221
|
}
|
|
178
222
|
|
|
179
|
-
// It's a regular message
|
|
180
223
|
const msg = item as MessageData;
|
|
181
224
|
|
|
182
225
|
if (msg.type === 'tool_use' && msg.tool_name && msg.tool_id) {
|
|
183
|
-
const result =
|
|
226
|
+
const result = toolResults.get(msg.tool_id);
|
|
184
227
|
return (
|
|
185
228
|
<ToolCallBlock
|
|
186
229
|
key={`tool-${msg.tool_id}`}
|
|
@@ -205,13 +248,15 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
205
248
|
|
|
206
249
|
return (
|
|
207
250
|
<Message
|
|
208
|
-
key={`msg-${
|
|
251
|
+
key={`msg-${globalIndex}-${msg.timestamp}`}
|
|
209
252
|
message={msg}
|
|
253
|
+
isLastAgentMessage={globalIndex === lastAgentItemIndex}
|
|
254
|
+
isFirstInTurn={isFirstInTurn}
|
|
210
255
|
/>
|
|
211
256
|
);
|
|
212
257
|
};
|
|
213
258
|
|
|
214
|
-
//
|
|
259
|
+
// Empty state
|
|
215
260
|
if (messages.length === 0) {
|
|
216
261
|
return (
|
|
217
262
|
<div data-testid="message-view" className="message-view">
|
|
@@ -230,6 +275,9 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
230
275
|
);
|
|
231
276
|
}
|
|
232
277
|
|
|
278
|
+
// Track a running global index across turns for lastAgentItemIndex matching
|
|
279
|
+
let globalIdx = 0;
|
|
280
|
+
|
|
233
281
|
return (
|
|
234
282
|
<div data-testid="message-view" className="message-view" role="log" aria-live="polite">
|
|
235
283
|
<MessageList
|
|
@@ -237,15 +285,51 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
237
285
|
onScrollChange={handleScrollChange}
|
|
238
286
|
autoScroll={isAtBottom}
|
|
239
287
|
>
|
|
240
|
-
{
|
|
288
|
+
{turns.map((turn, turnIndex) => {
|
|
289
|
+
// Track which items in this turn are "first message" (non-tool, non-stack)
|
|
290
|
+
let seenMessage = false;
|
|
291
|
+
|
|
292
|
+
const agentName = persona?.character || 'Agent';
|
|
293
|
+
const role = persona?.role || null;
|
|
294
|
+
const roleAbbrev = role ? (AGENT_ABBREV[role] || role) : null;
|
|
295
|
+
const roleColor = role ? (AGENT_COLORS[role] || '#e879f9') : undefined;
|
|
296
|
+
const userName = projectInfo?.githubUsername || 'You';
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div key={`turn-${turnIndex}`} className={`turn-group turn-${turn.speaker}`}>
|
|
300
|
+
<div className="turn-label">
|
|
301
|
+
<span className="turn-speaker">
|
|
302
|
+
{turn.speaker === 'user' ? userName : agentName}
|
|
303
|
+
</span>
|
|
304
|
+
<span className="turn-timestamp">
|
|
305
|
+
{formatTurnTime(turn.timestamp)}
|
|
306
|
+
</span>
|
|
307
|
+
{turn.speaker === 'agent' && roleAbbrev && (
|
|
308
|
+
<Badge
|
|
309
|
+
variant="default"
|
|
310
|
+
className="turn-role-badge"
|
|
311
|
+
style={{ backgroundColor: roleColor }}
|
|
312
|
+
>
|
|
313
|
+
{roleAbbrev}
|
|
314
|
+
</Badge>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
{turn.items.map((item) => {
|
|
318
|
+
const idx = globalIdx++;
|
|
319
|
+
const isMessage = !('isToolStack' in item) && !('messages' in item) && (item as MessageData).type !== 'tool_use';
|
|
320
|
+
const isFirst = isMessage && !seenMessage;
|
|
321
|
+
if (isMessage) seenMessage = true;
|
|
322
|
+
return renderItem(item, idx, isFirst);
|
|
323
|
+
})}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
241
327
|
</MessageList>
|
|
242
328
|
|
|
243
|
-
{/* Quick Actions - dedicated area outside message scroll */}
|
|
244
329
|
{lastAssistantMessage && (
|
|
245
330
|
<QuickActions message={lastAssistantMessage} />
|
|
246
331
|
)}
|
|
247
332
|
|
|
248
|
-
{/* Auto-scroll indicator */}
|
|
249
333
|
<div
|
|
250
334
|
data-testid="auto-scroll-indicator"
|
|
251
335
|
data-active={isAtBottom.toString()}
|
|
@@ -253,7 +337,6 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
253
337
|
style={{ display: 'none' }}
|
|
254
338
|
/>
|
|
255
339
|
|
|
256
|
-
{/* Scroll to bottom button */}
|
|
257
340
|
<Button
|
|
258
341
|
variant="ghost"
|
|
259
342
|
size="icon"
|
|
@@ -35,6 +35,20 @@ const AGENT_COLORS: Record<string, string> = {
|
|
|
35
35
|
orchestrator: '#e879f9', // Magenta - coordination
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
// Abbreviated role names for compact badge display
|
|
39
|
+
const AGENT_ABBREV: Record<string, string> = {
|
|
40
|
+
pm: 'PM',
|
|
41
|
+
sm: 'SM',
|
|
42
|
+
dev: 'DEV',
|
|
43
|
+
tea: 'TEA',
|
|
44
|
+
reviewer: 'REV',
|
|
45
|
+
architect: 'ARC',
|
|
46
|
+
devops: 'OPS',
|
|
47
|
+
'ux-designer': 'UX',
|
|
48
|
+
'tech-writer': 'TW',
|
|
49
|
+
orchestrator: 'ORC',
|
|
50
|
+
};
|
|
51
|
+
|
|
38
52
|
// Convert kebab-case theme name to Title Case (e.g., "princess-bride" -> "Princess Bride")
|
|
39
53
|
function humanizeTheme(theme: string): string {
|
|
40
54
|
return theme
|
|
@@ -47,6 +61,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
47
61
|
const { persona } = usePersona();
|
|
48
62
|
const [portraitError, setPortraitError] = useState(false);
|
|
49
63
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
64
|
+
const [isCompact, setIsCompact] = useState(false);
|
|
50
65
|
|
|
51
66
|
const character = persona?.character || 'Agent';
|
|
52
67
|
const theme = persona?.theme || 'default';
|
|
@@ -74,7 +89,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
74
89
|
<>
|
|
75
90
|
<TooltipProvider delayDuration={300}>
|
|
76
91
|
<div
|
|
77
|
-
className=
|
|
92
|
+
className={`persona-header clickable${isCompact ? ' compact' : ''}`}
|
|
78
93
|
data-testid="persona-header"
|
|
79
94
|
role="button"
|
|
80
95
|
tabIndex={0}
|
|
@@ -87,7 +102,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
87
102
|
<div className="persona-portrait" data-testid="persona-portrait">
|
|
88
103
|
{slug && theme && !portraitError ? (
|
|
89
104
|
<img
|
|
90
|
-
src={`/portraits/${theme}/
|
|
105
|
+
src={`/portraits/${theme}/medium/${slug}.png`}
|
|
91
106
|
alt={character}
|
|
92
107
|
className="portrait-image"
|
|
93
108
|
onError={() => setPortraitError(true)}
|
|
@@ -96,22 +111,22 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
96
111
|
<span className="portrait-fallback">🤖</span>
|
|
97
112
|
)}
|
|
98
113
|
</div>
|
|
99
|
-
<Tooltip>
|
|
100
|
-
<TooltipTrigger asChild>
|
|
101
|
-
<Badge
|
|
102
|
-
variant="default"
|
|
103
|
-
className="persona-role"
|
|
104
|
-
data-testid="persona-role"
|
|
105
|
-
style={{ backgroundColor: roleColor }}
|
|
106
|
-
>
|
|
107
|
-
{role}
|
|
108
|
-
</Badge>
|
|
109
|
-
</TooltipTrigger>
|
|
110
|
-
<TooltipContent>{role}</TooltipContent>
|
|
111
|
-
</Tooltip>
|
|
112
114
|
</div>
|
|
113
115
|
<div className="persona-info">
|
|
114
116
|
<div className="persona-name-row">
|
|
117
|
+
<Tooltip>
|
|
118
|
+
<TooltipTrigger asChild>
|
|
119
|
+
<Badge
|
|
120
|
+
variant="default"
|
|
121
|
+
className="persona-role"
|
|
122
|
+
data-testid="persona-role"
|
|
123
|
+
style={{ backgroundColor: roleColor }}
|
|
124
|
+
>
|
|
125
|
+
{AGENT_ABBREV[role] || role}
|
|
126
|
+
</Badge>
|
|
127
|
+
</TooltipTrigger>
|
|
128
|
+
<TooltipContent>{role}</TooltipContent>
|
|
129
|
+
</Tooltip>
|
|
115
130
|
<Tooltip>
|
|
116
131
|
<TooltipTrigger asChild>
|
|
117
132
|
<span
|
|
@@ -149,6 +164,21 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
149
164
|
</Tooltip>
|
|
150
165
|
)}
|
|
151
166
|
</div>
|
|
167
|
+
<img
|
|
168
|
+
src="/images/cyclist-dark.png"
|
|
169
|
+
alt="Cyclist"
|
|
170
|
+
className="persona-branding"
|
|
171
|
+
/>
|
|
172
|
+
<button
|
|
173
|
+
className="persona-collapse-toggle"
|
|
174
|
+
onClick={(e) => {
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
setIsCompact(!isCompact);
|
|
177
|
+
}}
|
|
178
|
+
aria-label={isCompact ? 'Expand header' : 'Collapse header'}
|
|
179
|
+
>
|
|
180
|
+
{isCompact ? '▼' : '▲'}
|
|
181
|
+
</button>
|
|
152
182
|
</div>
|
|
153
183
|
</TooltipProvider>
|
|
154
184
|
|
|
@@ -20,6 +20,7 @@ interface SubagentSpanProps {
|
|
|
20
20
|
name: string;
|
|
21
21
|
messages: SubagentMessage[];
|
|
22
22
|
defaultCollapsed?: boolean;
|
|
23
|
+
onCollapseChange?: (collapsed: boolean) => void;
|
|
23
24
|
// MSSCI-12776: Theme-aware helper display (optional - can be overridden by props for testing)
|
|
24
25
|
helperName?: string | null;
|
|
25
26
|
helperStyle?: string | null;
|
|
@@ -30,7 +31,8 @@ export default function SubagentSpan({
|
|
|
30
31
|
type,
|
|
31
32
|
name,
|
|
32
33
|
messages,
|
|
33
|
-
defaultCollapsed =
|
|
34
|
+
defaultCollapsed = true,
|
|
35
|
+
onCollapseChange,
|
|
34
36
|
helperName: propHelperName,
|
|
35
37
|
helperStyle: propHelperStyle,
|
|
36
38
|
friendlyMessage: propFriendlyMessage,
|
|
@@ -55,6 +57,9 @@ export default function SubagentSpan({
|
|
|
55
57
|
const displayName = helperName || type;
|
|
56
58
|
const displayMessage = friendlyMessage || name;
|
|
57
59
|
|
|
60
|
+
// Count non-result messages for the collapsed summary
|
|
61
|
+
const messageCount = messages.filter(m => m.type !== 'tool_result').length;
|
|
62
|
+
|
|
58
63
|
// Group tool_use and tool_result by tool_id
|
|
59
64
|
const toolResults = new Map<string, SubagentMessage>();
|
|
60
65
|
messages.forEach(msg => {
|
|
@@ -92,16 +97,16 @@ export default function SubagentSpan({
|
|
|
92
97
|
return null;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
// Subagent prompts (user messages within subagent)
|
|
100
|
+
// Subagent prompts (user messages within subagent) — truncated single line
|
|
96
101
|
if (msg.type === 'user') {
|
|
102
|
+
const truncated = (msg.content || '').slice(0, 120).replace(/\n/g, ' ');
|
|
97
103
|
return (
|
|
98
104
|
<div
|
|
99
105
|
key={`subagent-prompt-${index}`}
|
|
100
106
|
data-testid="subagent-prompt"
|
|
101
107
|
className="message message-subagent-prompt"
|
|
102
108
|
>
|
|
103
|
-
<div className="message-
|
|
104
|
-
<div className="message-content">{msg.content}</div>
|
|
109
|
+
<div className="message-content">{truncated}{(msg.content || '').length > 120 ? '...' : ''}</div>
|
|
105
110
|
</div>
|
|
106
111
|
);
|
|
107
112
|
}
|
|
@@ -127,7 +132,11 @@ export default function SubagentSpan({
|
|
|
127
132
|
<div
|
|
128
133
|
data-testid="subagent-span-header"
|
|
129
134
|
className="subagent-header"
|
|
130
|
-
onClick={() =>
|
|
135
|
+
onClick={() => {
|
|
136
|
+
const next = !isCollapsed;
|
|
137
|
+
setIsCollapsed(next);
|
|
138
|
+
onCollapseChange?.(next);
|
|
139
|
+
}}
|
|
131
140
|
>
|
|
132
141
|
<span className="subagent-toggle">{isCollapsed ? '▶' : '▼'}</span>
|
|
133
142
|
|
|
@@ -159,9 +168,7 @@ export default function SubagentSpan({
|
|
|
159
168
|
{type}
|
|
160
169
|
</Badge>
|
|
161
170
|
|
|
162
|
-
{
|
|
163
|
-
<span className="subagent-count">{messages.length} messages</span>
|
|
164
|
-
)}
|
|
171
|
+
<span className="subagent-count">{messageCount}</span>
|
|
165
172
|
</div>
|
|
166
173
|
{!isCollapsed && (
|
|
167
174
|
<div className="subagent-content">
|
|
@@ -43,19 +43,25 @@ export default function ToolStack({ stack, toolResults }: ToolStackProps): React
|
|
|
43
43
|
prevIsActiveRef.current = stack.isActive;
|
|
44
44
|
}, [stack.isActive]);
|
|
45
45
|
|
|
46
|
-
//
|
|
47
|
-
const countText =
|
|
46
|
+
// Count display - just the number
|
|
47
|
+
const countText = `${stack.count}`;
|
|
48
|
+
|
|
49
|
+
// Active tool summary - show what's happening RIGHT NOW
|
|
50
|
+
const activeSummary = useMemo(() => {
|
|
51
|
+
if (stack.isActive && stack.tools.length > 0) {
|
|
52
|
+
// Show the most recent (last) tool's summary
|
|
53
|
+
const lastTool = stack.tools[stack.tools.length - 1];
|
|
54
|
+
return generateToolIntentSummary(lastTool.tool_name, lastTool.input);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}, [stack.tools, stack.isActive]);
|
|
48
58
|
|
|
49
59
|
// Generate summary of tool intents for collapsed view
|
|
50
60
|
const collapsedSummary = useMemo(() => {
|
|
51
|
-
// Show
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
if (stack.tools.length > 2) {
|
|
56
|
-
return summaries.join(', ') + '...';
|
|
57
|
-
}
|
|
58
|
-
return summaries.join(', ');
|
|
61
|
+
// Show last tool's summary as the main description
|
|
62
|
+
if (stack.tools.length === 0) return '';
|
|
63
|
+
const lastTool = stack.tools[stack.tools.length - 1];
|
|
64
|
+
return generateToolIntentSummary(lastTool.tool_name, lastTool.input);
|
|
59
65
|
}, [stack.tools]);
|
|
60
66
|
|
|
61
67
|
// Compute tool type counts for mini badges
|
|
@@ -133,11 +139,15 @@ export default function ToolStack({ stack, toolResults }: ToolStackProps): React
|
|
|
133
139
|
</span>
|
|
134
140
|
<span
|
|
135
141
|
data-testid="tool-stack-count"
|
|
136
|
-
className="tool-stack-count"
|
|
142
|
+
className="tool-stack-count-badge"
|
|
137
143
|
>
|
|
138
|
-
{countText}
|
|
144
|
+
{countText}
|
|
139
145
|
</span>
|
|
140
|
-
{
|
|
146
|
+
{stack.isActive && activeSummary ? (
|
|
147
|
+
<span className="tool-stack-active-summary">
|
|
148
|
+
{activeSummary}
|
|
149
|
+
</span>
|
|
150
|
+
) : (
|
|
141
151
|
<>
|
|
142
152
|
<span className="tool-stack-badges">
|
|
143
153
|
<TooltipProvider delayDuration={300}>
|