@pennyfarthing/cyclist 9.3.0 → 10.0.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/hook-request.d.ts +11 -0
- package/dist/api/hook-request.d.ts.map +1 -1
- package/dist/api/hook-request.js +126 -28
- package/dist/api/hook-request.js.map +1 -1
- 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 +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/permissions.d.ts +16 -0
- package/dist/api/permissions.d.ts.map +1 -0
- package/dist/api/permissions.js +67 -0
- package/dist/api/permissions.js.map +1 -0
- 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/api/theme-agents.d.ts +4 -0
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +3 -0
- package/dist/api/theme-agents.js.map +1 -1
- package/dist/approval-gate.d.ts +3 -75
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +4 -121
- package/dist/approval-gate.js.map +1 -1
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +60 -0
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/cyclist-pretooluse-hook.js +57 -0
- package/dist/hooks/cyclist-pretooluse-hook.js.map +1 -0
- package/dist/hooks/pretooluse-hook.d.ts +89 -0
- package/dist/hooks/pretooluse-hook.d.ts.map +1 -0
- package/dist/hooks/pretooluse-hook.js +235 -0
- package/dist/hooks/pretooluse-hook.js.map +1 -0
- package/dist/main.d.ts +1 -134
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +42 -373
- package/dist/main.js.map +1 -1
- package/dist/menu-builder.d.ts +7 -1
- package/dist/menu-builder.d.ts.map +1 -1
- package/dist/menu-builder.js +36 -1
- package/dist/menu-builder.js.map +1 -1
- package/dist/otlp-receiver.d.ts.map +1 -1
- package/dist/otlp-receiver.js +6 -0
- package/dist/otlp-receiver.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +42 -42
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -3
- package/dist/server.js.map +1 -1
- package/dist/settings-store.d.ts +3 -1
- package/dist/settings-store.d.ts.map +1 -1
- package/dist/settings-store.js +18 -9
- package/dist/settings-store.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 +1 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +48 -5
- package/dist/websocket.js.map +1 -1
- package/dist/workflow-presets.d.ts +72 -0
- package/dist/workflow-presets.d.ts.map +1 -0
- package/dist/workflow-presets.js +93 -0
- package/dist/workflow-presets.js.map +1 -0
- package/package.json +2 -2
- package/src/public/App.tsx +61 -1
- package/src/public/components/ApprovalModal/index.tsx +31 -1
- package/src/public/components/ControlBar.tsx +19 -20
- package/src/public/components/DockviewWorkspace.tsx +39 -5
- package/src/public/components/FontPicker/index.tsx +118 -33
- package/src/public/components/FullFileTree.tsx +223 -0
- package/src/public/components/Message.tsx +89 -11
- package/src/public/components/MessageView.tsx +206 -93
- package/src/public/components/PersonaHeader.tsx +47 -15
- package/src/public/components/SubagentSpan.tsx +15 -8
- package/src/public/components/panels/BackgroundPanel.tsx +1 -1
- 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 +79 -5
- package/src/public/components/panels/SettingsPanel.tsx +3 -28
- package/src/public/components/panels/WorkflowPanel.tsx +108 -13
- package/src/public/components/panels/index.ts +1 -0
- package/src/public/contexts/ClaudeContext.tsx +16 -1
- package/src/public/css/theme-system.css +46 -38
- package/src/public/hooks/useColorScheme.ts +27 -0
- package/src/public/hooks/useFileBrowser.ts +71 -0
- package/src/public/hooks/useHotspots.ts +113 -0
- package/src/public/hooks/usePlanModeExit.ts +105 -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/dockview-theme.css +31 -33
- package/src/public/styles/tailwind.css +417 -58
- package/src/public/types/message.ts +6 -1
- package/src/public/utils/markdown.ts +2 -2
- package/src/public/utils/slash-commands.ts +1 -1
- package/src/public/utils/toolStackGrouper.ts +5 -6
|
@@ -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';
|
|
@@ -21,10 +23,27 @@ import ToolCallBlock from './ToolCallBlock';
|
|
|
21
23
|
import ToolStack from './ToolStack';
|
|
22
24
|
import SubagentSpan from './SubagentSpan';
|
|
23
25
|
import QuickActions from './QuickActions';
|
|
26
|
+
import { Separator } from '@/components/ui/separator';
|
|
24
27
|
import { isSkillContent } from '../utils/messageFilters';
|
|
25
28
|
import { groupToolsIntoStacks, ToolStackData } from '../utils/toolStackGrouper';
|
|
29
|
+
import { usePersona } from '../hooks/usePersona';
|
|
30
|
+
import { useColorScheme } from '../hooks/useColorScheme';
|
|
31
|
+
import { useStatsStrip } from '../hooks/useStatsStrip';
|
|
26
32
|
import type { MessageData } from '../types/message';
|
|
27
33
|
|
|
34
|
+
// Agent colors matching CLI statusbar (from PersonaHeader)
|
|
35
|
+
const AGENT_COLORS: Record<string, string> = {
|
|
36
|
+
pm: '#a78bfa', sm: '#60a5fa', dev: '#4ade80', tea: '#2dd4bf',
|
|
37
|
+
reviewer: '#f87171', architect: '#fb923c', devops: '#22d3ee',
|
|
38
|
+
'ux-designer': '#f0abfc', 'tech-writer': '#e5e5e5', orchestrator: '#e879f9',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const AGENT_ABBREV: Record<string, string> = {
|
|
42
|
+
pm: 'PM', sm: 'SM', dev: 'DEV', tea: 'TEA', reviewer: 'REV',
|
|
43
|
+
architect: 'ARC', devops: 'OPS', 'ux-designer': 'UX', 'tech-writer': 'TW',
|
|
44
|
+
orchestrator: 'ORC',
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
interface MessageViewProps {
|
|
29
48
|
messages: MessageData[];
|
|
30
49
|
}
|
|
@@ -41,9 +60,38 @@ interface ToolStackGroup {
|
|
|
41
60
|
stack: ToolStackData;
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
type RenderItem = MessageData | SubagentGroup | ToolStackGroup;
|
|
64
|
+
|
|
65
|
+
interface Turn {
|
|
66
|
+
speaker: 'user' | 'agent' | 'system';
|
|
67
|
+
items: RenderItem[];
|
|
68
|
+
timestamp: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatTurnTime(timestamp: number): string {
|
|
72
|
+
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classify an item as 'user' or 'agent' for turn grouping.
|
|
77
|
+
* Tools, subagents, and stacks are all part of the agent's turn.
|
|
78
|
+
*/
|
|
79
|
+
function speakerOf(item: RenderItem): 'user' | 'agent' | 'system' {
|
|
80
|
+
if ('isToolStack' in item || 'messages' in item) return 'agent';
|
|
81
|
+
const msg = item as MessageData;
|
|
82
|
+
if (msg.type === 'context_cleared') return 'system';
|
|
83
|
+
return (msg.type === 'user' || msg.type === 'bell_injected') ? 'user' : 'agent';
|
|
84
|
+
}
|
|
85
|
+
|
|
44
86
|
export default function MessageView({ messages }: MessageViewProps): React.ReactElement {
|
|
45
87
|
const messageListRef = useRef<MessageListHandle>(null);
|
|
46
88
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
89
|
+
const { persona } = usePersona();
|
|
90
|
+
const colorScheme = useColorScheme();
|
|
91
|
+
const { projectInfo } = useStatsStrip();
|
|
92
|
+
|
|
93
|
+
// Persist subagent collapsed state across re-renders/remounts
|
|
94
|
+
const subagentCollapsedRef = useRef<Map<string, boolean>>(new Map());
|
|
47
95
|
|
|
48
96
|
const handleScrollChange = useCallback((atBottom: boolean) => {
|
|
49
97
|
setIsAtBottom(atBottom);
|
|
@@ -53,132 +101,133 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
53
101
|
messageListRef.current?.scrollToBottom('smooth');
|
|
54
102
|
}, []);
|
|
55
103
|
|
|
56
|
-
// Find the last assistant message for QuickActions
|
|
104
|
+
// Find the last non-streaming assistant message for QuickActions
|
|
57
105
|
const lastAssistantMessage = useMemo(() => {
|
|
58
106
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
59
|
-
if (messages[i].type === 'agent' && !messages[i].isStreaming)
|
|
60
|
-
return messages[i];
|
|
61
|
-
}
|
|
107
|
+
if (messages[i].type === 'agent' && !messages[i].isStreaming) return messages[i];
|
|
62
108
|
}
|
|
63
109
|
return null;
|
|
64
110
|
}, [messages]);
|
|
65
111
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
112
|
+
// Single memo: messages → flat render items → turns
|
|
113
|
+
const { turns, toolResults, lastAgentItemIndex } = useMemo(() => {
|
|
114
|
+
// 1. Index tool results by ID
|
|
115
|
+
const results = new Map<string, MessageData>();
|
|
116
|
+
for (const msg of messages) {
|
|
117
|
+
if (msg.type === 'tool_result' && msg.tool_id) results.set(msg.tool_id, msg);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Filter and collect subagent groups in one pass
|
|
121
|
+
const filtered: MessageData[] = [];
|
|
69
122
|
const subagentGroups = new Map<string, SubagentGroup>();
|
|
70
123
|
|
|
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)
|
|
124
|
+
for (const msg of messages) {
|
|
125
|
+
if (msg.type === 'tool_result') continue;
|
|
126
|
+
if (msg.type === 'user' && isSkillContent(msg.content)) continue;
|
|
91
127
|
if (msg.parent_id) {
|
|
92
128
|
let group = subagentGroups.get(msg.parent_id);
|
|
93
129
|
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
|
-
};
|
|
130
|
+
group = { parent_id: msg.parent_id, type: msg.subagent_type || 'unknown', name: msg.subagent_name || 'unnamed', messages: [] };
|
|
100
131
|
subagentGroups.set(msg.parent_id, group);
|
|
101
132
|
}
|
|
102
133
|
group.messages.push(msg);
|
|
103
|
-
|
|
134
|
+
continue;
|
|
104
135
|
}
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Third pass: group consecutive tool_use messages into stacks
|
|
109
|
-
const toolStacks = groupToolsIntoStacks(filteredMessages);
|
|
136
|
+
filtered.push(msg);
|
|
137
|
+
}
|
|
110
138
|
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
// 3. Build flat render list, replacing consecutive tool_use runs with stacks
|
|
140
|
+
const stacks = groupToolsIntoStacks(filtered);
|
|
141
|
+
const stackByToolId = new Map<string, ToolStackData>();
|
|
142
|
+
for (const stack of stacks) {
|
|
143
|
+
for (const tool of stack.tools) stackByToolId.set(tool.tool_id, stack);
|
|
144
|
+
}
|
|
116
145
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
const items: RenderItem[] = [];
|
|
147
|
+
const emittedStacks = new Set<string>();
|
|
148
|
+
const emittedSubagents = new Set<string>();
|
|
120
149
|
|
|
121
|
-
|
|
122
|
-
if (msg.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
for (const msg of filtered) {
|
|
151
|
+
if (msg.type === 'tool_use' && msg.tool_id && stackByToolId.has(msg.tool_id)) {
|
|
152
|
+
const stack = stackByToolId.get(msg.tool_id)!;
|
|
153
|
+
if (!emittedStacks.has(stack.stackId)) {
|
|
154
|
+
emittedStacks.add(stack.stackId);
|
|
155
|
+
items.push({ isToolStack: true, stack });
|
|
127
156
|
}
|
|
128
|
-
} else if (msg.type === 'tool_use' && msg.tool_id && stackedToolIds.has(msg.tool_id)) {
|
|
129
|
-
// This tool belongs to a stack
|
|
130
|
-
const stack = toolStacks.find(s =>
|
|
131
|
-
s.tools.some(t => t.tool_id === msg.tool_id)
|
|
132
|
-
);
|
|
133
|
-
if (stack && (!pendingStack || pendingStack.stackId !== stack.stackId)) {
|
|
134
|
-
// New stack - add it
|
|
135
|
-
result.push({ isToolStack: true, stack });
|
|
136
|
-
pendingStack = stack;
|
|
137
|
-
}
|
|
138
|
-
// Skip individual rendering - handled by ToolStack
|
|
139
|
-
} else if (msg.type === 'tool_use') {
|
|
140
|
-
// Single tool - render normally
|
|
141
|
-
result.push(msg);
|
|
142
|
-
pendingStack = null;
|
|
143
157
|
} else {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
items.push(msg);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Insert subagent groups at the position of their first parent_id occurrence
|
|
163
|
+
// (They appear in the agent's turn, after the Task tool_use that spawned them)
|
|
164
|
+
// For now, just append them — they'll naturally land in the agent turn
|
|
165
|
+
for (const [, group] of subagentGroups) {
|
|
166
|
+
if (!emittedSubagents.has(group.parent_id)) {
|
|
167
|
+
emittedSubagents.add(group.parent_id);
|
|
168
|
+
items.push(group);
|
|
147
169
|
}
|
|
148
|
-
}
|
|
170
|
+
}
|
|
149
171
|
|
|
150
|
-
|
|
172
|
+
// 4. Find last agent message index (for throb control)
|
|
173
|
+
let lastAgent = -1;
|
|
174
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
175
|
+
const item = items[i];
|
|
176
|
+
if (!('isToolStack' in item) && !('messages' in item) && (item as MessageData).type === 'agent') {
|
|
177
|
+
lastAgent = i;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5. Group into turns
|
|
183
|
+
const turnList: Turn[] = [];
|
|
184
|
+
for (const item of items) {
|
|
185
|
+
const speaker = speakerOf(item);
|
|
186
|
+
const last = turnList[turnList.length - 1];
|
|
187
|
+
if (last && last.speaker === speaker) {
|
|
188
|
+
last.items.push(item);
|
|
189
|
+
} else {
|
|
190
|
+
turnList.push({
|
|
191
|
+
speaker,
|
|
192
|
+
items: [item],
|
|
193
|
+
timestamp: ('timestamp' in item) ? (item as MessageData).timestamp : Date.now(),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { turns: turnList, toolResults: results, lastAgentItemIndex: lastAgent };
|
|
151
199
|
}, [messages]);
|
|
152
200
|
|
|
153
|
-
const renderItem = (item:
|
|
154
|
-
|
|
155
|
-
if ('isToolStack' in item && item.isToolStack) {
|
|
201
|
+
const renderItem = (item: RenderItem, globalIndex: number, isFirstInTurn: boolean) => {
|
|
202
|
+
if ('isToolStack' in item) {
|
|
156
203
|
return (
|
|
157
204
|
<ToolStack
|
|
158
205
|
key={`stack-${item.stack.stackId}`}
|
|
159
206
|
stack={item.stack}
|
|
160
|
-
toolResults={
|
|
207
|
+
toolResults={toolResults as Map<string, { type: 'tool_result'; tool_id: string; content: string; timestamp: number }>}
|
|
161
208
|
/>
|
|
162
209
|
);
|
|
163
210
|
}
|
|
164
211
|
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
if ('messages' in item && 'parent_id' in item) {
|
|
213
|
+
const group = item as SubagentGroup;
|
|
214
|
+
const collapsed = subagentCollapsedRef.current.get(group.parent_id) ?? true;
|
|
167
215
|
return (
|
|
168
216
|
<SubagentSpan
|
|
169
|
-
key={`subagent-${
|
|
170
|
-
type={
|
|
171
|
-
name={
|
|
172
|
-
messages={
|
|
217
|
+
key={`subagent-${group.parent_id}`}
|
|
218
|
+
type={group.type}
|
|
219
|
+
name={group.name}
|
|
220
|
+
messages={group.messages as any}
|
|
221
|
+
defaultCollapsed={collapsed}
|
|
222
|
+
onCollapseChange={(c) => subagentCollapsedRef.current.set(group.parent_id, c)}
|
|
173
223
|
/>
|
|
174
224
|
);
|
|
175
225
|
}
|
|
176
226
|
|
|
177
|
-
// It's a regular message
|
|
178
227
|
const msg = item as MessageData;
|
|
179
228
|
|
|
180
229
|
if (msg.type === 'tool_use' && msg.tool_name && msg.tool_id) {
|
|
181
|
-
const result =
|
|
230
|
+
const result = toolResults.get(msg.tool_id);
|
|
182
231
|
return (
|
|
183
232
|
<ToolCallBlock
|
|
184
233
|
key={`tool-${msg.tool_id}`}
|
|
@@ -203,19 +252,25 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
203
252
|
|
|
204
253
|
return (
|
|
205
254
|
<Message
|
|
206
|
-
key={`msg-${
|
|
255
|
+
key={`msg-${globalIndex}-${msg.timestamp}`}
|
|
207
256
|
message={msg}
|
|
257
|
+
isLastAgentMessage={globalIndex === lastAgentItemIndex}
|
|
258
|
+
isFirstInTurn={isFirstInTurn}
|
|
208
259
|
/>
|
|
209
260
|
);
|
|
210
261
|
};
|
|
211
262
|
|
|
212
|
-
//
|
|
263
|
+
// Empty state
|
|
213
264
|
if (messages.length === 0) {
|
|
214
265
|
return (
|
|
215
266
|
<div data-testid="message-view" className="message-view">
|
|
216
267
|
<div className="message-view-empty">
|
|
217
268
|
<div>
|
|
218
|
-
<
|
|
269
|
+
<img
|
|
270
|
+
src={colorScheme === 'dark' ? '/images/cyclist-dark.png' : '/images/cyclist-light.png'}
|
|
271
|
+
alt="Cyclist"
|
|
272
|
+
style={{ height: '2.5rem', marginBottom: '0.5rem', opacity: 0.6 }}
|
|
273
|
+
/>
|
|
219
274
|
<div>Type <code style={{
|
|
220
275
|
background: 'var(--bg-tertiary, #0f0f1a)',
|
|
221
276
|
padding: '2px 6px',
|
|
@@ -228,6 +283,9 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
228
283
|
);
|
|
229
284
|
}
|
|
230
285
|
|
|
286
|
+
// Track a running global index across turns for lastAgentItemIndex matching
|
|
287
|
+
let globalIdx = 0;
|
|
288
|
+
|
|
231
289
|
return (
|
|
232
290
|
<div data-testid="message-view" className="message-view" role="log" aria-live="polite">
|
|
233
291
|
<MessageList
|
|
@@ -235,15 +293,71 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
235
293
|
onScrollChange={handleScrollChange}
|
|
236
294
|
autoScroll={isAtBottom}
|
|
237
295
|
>
|
|
238
|
-
{
|
|
296
|
+
{turns.map((turn, turnIndex) => {
|
|
297
|
+
// System turns (context_cleared) render as a divider bar
|
|
298
|
+
if (turn.speaker === 'system') {
|
|
299
|
+
// Still increment globalIdx for system items
|
|
300
|
+
turn.items.forEach(() => globalIdx++);
|
|
301
|
+
return (
|
|
302
|
+
<div key={`turn-${turnIndex}`} className="turn-group turn-system">
|
|
303
|
+
<div className="context-cleared-bar">
|
|
304
|
+
<Separator className="context-cleared-line" />
|
|
305
|
+
<span className="context-cleared-label">
|
|
306
|
+
Context cleared
|
|
307
|
+
</span>
|
|
308
|
+
<span className="context-cleared-time">
|
|
309
|
+
{formatTurnTime(turn.timestamp)}
|
|
310
|
+
</span>
|
|
311
|
+
<Separator className="context-cleared-line" />
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Track which items in this turn are "first message" (non-tool, non-stack)
|
|
318
|
+
let seenMessage = false;
|
|
319
|
+
|
|
320
|
+
const agentName = persona?.character || 'Agent';
|
|
321
|
+
const role = persona?.role || null;
|
|
322
|
+
const roleAbbrev = role ? (AGENT_ABBREV[role] || role) : null;
|
|
323
|
+
const roleColor = role ? (AGENT_COLORS[role] || '#e879f9') : undefined;
|
|
324
|
+
const userName = projectInfo?.githubUsername || 'You';
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div key={`turn-${turnIndex}`} className={`turn-group turn-${turn.speaker}`}>
|
|
328
|
+
<div className="turn-label">
|
|
329
|
+
<span className="turn-speaker">
|
|
330
|
+
{turn.speaker === 'user' ? userName : agentName}
|
|
331
|
+
</span>
|
|
332
|
+
<span className="turn-timestamp">
|
|
333
|
+
{formatTurnTime(turn.timestamp)}
|
|
334
|
+
</span>
|
|
335
|
+
{turn.speaker === 'agent' && roleAbbrev && (
|
|
336
|
+
<Badge
|
|
337
|
+
variant="default"
|
|
338
|
+
className="turn-role-badge"
|
|
339
|
+
style={{ backgroundColor: roleColor }}
|
|
340
|
+
>
|
|
341
|
+
{roleAbbrev}
|
|
342
|
+
</Badge>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
{turn.items.map((item) => {
|
|
346
|
+
const idx = globalIdx++;
|
|
347
|
+
const isMessage = !('isToolStack' in item) && !('messages' in item) && (item as MessageData).type !== 'tool_use';
|
|
348
|
+
const isFirst = isMessage && !seenMessage;
|
|
349
|
+
if (isMessage) seenMessage = true;
|
|
350
|
+
return renderItem(item, idx, isFirst);
|
|
351
|
+
})}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
})}
|
|
239
355
|
</MessageList>
|
|
240
356
|
|
|
241
|
-
{/* Quick Actions - dedicated area outside message scroll */}
|
|
242
357
|
{lastAssistantMessage && (
|
|
243
358
|
<QuickActions message={lastAssistantMessage} />
|
|
244
359
|
)}
|
|
245
360
|
|
|
246
|
-
{/* Auto-scroll indicator */}
|
|
247
361
|
<div
|
|
248
362
|
data-testid="auto-scroll-indicator"
|
|
249
363
|
data-active={isAtBottom.toString()}
|
|
@@ -251,7 +365,6 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
251
365
|
style={{ display: 'none' }}
|
|
252
366
|
/>
|
|
253
367
|
|
|
254
|
-
{/* Scroll to bottom button */}
|
|
255
368
|
<Button
|
|
256
369
|
variant="ghost"
|
|
257
370
|
size="icon"
|
|
@@ -19,6 +19,7 @@ import React, { useState, useCallback } from 'react';
|
|
|
19
19
|
import { Badge } from '@/components/ui/badge';
|
|
20
20
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
21
21
|
import { usePersona } from '../hooks/usePersona';
|
|
22
|
+
import { useColorScheme } from '../hooks/useColorScheme';
|
|
22
23
|
import { AgentPopup } from './AgentPopup';
|
|
23
24
|
|
|
24
25
|
// Agent colors matching CLI statusbar (statusline.sh)
|
|
@@ -35,6 +36,20 @@ const AGENT_COLORS: Record<string, string> = {
|
|
|
35
36
|
orchestrator: '#e879f9', // Magenta - coordination
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
// Abbreviated role names for compact badge display
|
|
40
|
+
const AGENT_ABBREV: Record<string, string> = {
|
|
41
|
+
pm: 'PM',
|
|
42
|
+
sm: 'SM',
|
|
43
|
+
dev: 'DEV',
|
|
44
|
+
tea: 'TEA',
|
|
45
|
+
reviewer: 'REV',
|
|
46
|
+
architect: 'ARC',
|
|
47
|
+
devops: 'OPS',
|
|
48
|
+
'ux-designer': 'UX',
|
|
49
|
+
'tech-writer': 'TW',
|
|
50
|
+
orchestrator: 'ORC',
|
|
51
|
+
};
|
|
52
|
+
|
|
38
53
|
// Convert kebab-case theme name to Title Case (e.g., "princess-bride" -> "Princess Bride")
|
|
39
54
|
function humanizeTheme(theme: string): string {
|
|
40
55
|
return theme
|
|
@@ -45,8 +60,10 @@ function humanizeTheme(theme: string): string {
|
|
|
45
60
|
|
|
46
61
|
export default function PersonaHeader(): React.ReactElement {
|
|
47
62
|
const { persona } = usePersona();
|
|
63
|
+
const colorScheme = useColorScheme();
|
|
48
64
|
const [portraitError, setPortraitError] = useState(false);
|
|
49
65
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
66
|
+
const [isCompact, setIsCompact] = useState(false);
|
|
50
67
|
|
|
51
68
|
const character = persona?.character || 'Agent';
|
|
52
69
|
const theme = persona?.theme || 'default';
|
|
@@ -74,7 +91,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
74
91
|
<>
|
|
75
92
|
<TooltipProvider delayDuration={300}>
|
|
76
93
|
<div
|
|
77
|
-
className=
|
|
94
|
+
className={`persona-header clickable${isCompact ? ' compact' : ''}`}
|
|
78
95
|
data-testid="persona-header"
|
|
79
96
|
role="button"
|
|
80
97
|
tabIndex={0}
|
|
@@ -87,7 +104,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
87
104
|
<div className="persona-portrait" data-testid="persona-portrait">
|
|
88
105
|
{slug && theme && !portraitError ? (
|
|
89
106
|
<img
|
|
90
|
-
src={`/portraits/${theme}/
|
|
107
|
+
src={`/portraits/${theme}/medium/${slug}.png`}
|
|
91
108
|
alt={character}
|
|
92
109
|
className="portrait-image"
|
|
93
110
|
onError={() => setPortraitError(true)}
|
|
@@ -96,22 +113,22 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
96
113
|
<span className="portrait-fallback">🤖</span>
|
|
97
114
|
)}
|
|
98
115
|
</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
116
|
</div>
|
|
113
117
|
<div className="persona-info">
|
|
114
118
|
<div className="persona-name-row">
|
|
119
|
+
<Tooltip>
|
|
120
|
+
<TooltipTrigger asChild>
|
|
121
|
+
<Badge
|
|
122
|
+
variant="default"
|
|
123
|
+
className="persona-role"
|
|
124
|
+
data-testid="persona-role"
|
|
125
|
+
style={{ backgroundColor: roleColor }}
|
|
126
|
+
>
|
|
127
|
+
{AGENT_ABBREV[role] || role}
|
|
128
|
+
</Badge>
|
|
129
|
+
</TooltipTrigger>
|
|
130
|
+
<TooltipContent>{role}</TooltipContent>
|
|
131
|
+
</Tooltip>
|
|
115
132
|
<Tooltip>
|
|
116
133
|
<TooltipTrigger asChild>
|
|
117
134
|
<span
|
|
@@ -149,6 +166,21 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
149
166
|
</Tooltip>
|
|
150
167
|
)}
|
|
151
168
|
</div>
|
|
169
|
+
<img
|
|
170
|
+
src={colorScheme === 'dark' ? '/images/cyclist-dark.png' : '/images/cyclist-light.png'}
|
|
171
|
+
alt="Cyclist"
|
|
172
|
+
className="persona-branding"
|
|
173
|
+
/>
|
|
174
|
+
<button
|
|
175
|
+
className="persona-collapse-toggle"
|
|
176
|
+
onClick={(e) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
setIsCompact(!isCompact);
|
|
179
|
+
}}
|
|
180
|
+
aria-label={isCompact ? 'Expand header' : 'Collapse header'}
|
|
181
|
+
>
|
|
182
|
+
{isCompact ? '▼' : '▲'}
|
|
183
|
+
</button>
|
|
152
184
|
</div>
|
|
153
185
|
</TooltipProvider>
|
|
154
186
|
|
|
@@ -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">
|
|
@@ -71,7 +71,7 @@ export function BackgroundPanel(): React.ReactElement {
|
|
|
71
71
|
if (tasks.length === 0) {
|
|
72
72
|
return (
|
|
73
73
|
<div className="background-panel empty" data-testid="background-panel">
|
|
74
|
-
<div className="placeholder">No
|
|
74
|
+
<div className="placeholder">No subagent tasks</div>
|
|
75
75
|
</div>
|
|
76
76
|
);
|
|
77
77
|
}
|