@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FullFileTree - Complete directory tree with changed file highlighting
|
|
3
|
+
*
|
|
4
|
+
* Displays the full project file tree with lazy-loaded directories.
|
|
5
|
+
* Changed files are highlighted with git status colors (created/modified/deleted).
|
|
6
|
+
* Uses /api/files for directory listing and /ws/git for change status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
10
|
+
import { Badge } from '@/components/ui/badge';
|
|
11
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
12
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
13
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
14
|
+
import { useFileBrowser, DirectoryEntry } from '../hooks/useFileBrowser';
|
|
15
|
+
import type { FileStatus } from './FileTree';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
export interface FullFileTreeProps {
|
|
22
|
+
/** Map of file path → git status for highlighting */
|
|
23
|
+
changedFiles: Map<string, FileStatus>;
|
|
24
|
+
/** Callback when a file is clicked */
|
|
25
|
+
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// File Item Component
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
function TreeFileItem({
|
|
33
|
+
entry,
|
|
34
|
+
status,
|
|
35
|
+
depth,
|
|
36
|
+
onFileClick,
|
|
37
|
+
}: {
|
|
38
|
+
entry: DirectoryEntry;
|
|
39
|
+
status?: FileStatus;
|
|
40
|
+
depth: number;
|
|
41
|
+
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
42
|
+
}): React.ReactElement {
|
|
43
|
+
const statusIcon = status === 'created' ? '+' : status === 'modified' ? '~' : status === 'deleted' ? '-' : null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Tooltip>
|
|
47
|
+
<TooltipTrigger asChild>
|
|
48
|
+
<div
|
|
49
|
+
role="treeitem"
|
|
50
|
+
className={`file-item${status ? ` file-${status}` : ''}`}
|
|
51
|
+
style={{ paddingLeft: `${12 + depth * 16}px` }}
|
|
52
|
+
tabIndex={0}
|
|
53
|
+
aria-label={`${entry.name}${status ? `, ${status}` : ''}`}
|
|
54
|
+
onClick={() => onFileClick?.(entry, status)}
|
|
55
|
+
onKeyDown={(e) => {
|
|
56
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
onFileClick?.(entry, status);
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{statusIcon && (
|
|
63
|
+
<span className={`status-icon status-${status}`} aria-hidden="true">
|
|
64
|
+
{statusIcon}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
<span className={`file-name${status === 'deleted' ? '' : ''}`}>{entry.name}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</TooltipTrigger>
|
|
70
|
+
<TooltipContent>{entry.path}</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Directory Node Component (recursive)
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
function TreeDirectoryNode({
|
|
80
|
+
entry,
|
|
81
|
+
depth,
|
|
82
|
+
changedFiles,
|
|
83
|
+
onFileClick,
|
|
84
|
+
fetchDirectory,
|
|
85
|
+
cache,
|
|
86
|
+
loading,
|
|
87
|
+
}: {
|
|
88
|
+
entry: DirectoryEntry;
|
|
89
|
+
depth: number;
|
|
90
|
+
changedFiles: Map<string, FileStatus>;
|
|
91
|
+
onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
|
|
92
|
+
fetchDirectory: (path: string) => Promise<void>;
|
|
93
|
+
cache: Record<string, DirectoryEntry[]>;
|
|
94
|
+
loading: Set<string>;
|
|
95
|
+
}): React.ReactElement {
|
|
96
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
97
|
+
const children = cache[entry.path];
|
|
98
|
+
const isLoading = loading.has(entry.path);
|
|
99
|
+
|
|
100
|
+
// Check if this directory contains any changed files
|
|
101
|
+
const hasChanges = Array.from(changedFiles.keys()).some(
|
|
102
|
+
(filePath) => filePath.startsWith(entry.path + '/')
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const handleToggle = useCallback(() => {
|
|
106
|
+
const willOpen = !isOpen;
|
|
107
|
+
setIsOpen(willOpen);
|
|
108
|
+
if (willOpen && !children) {
|
|
109
|
+
fetchDirectory(entry.path);
|
|
110
|
+
}
|
|
111
|
+
}, [isOpen, children, entry.path, fetchDirectory]);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Collapsible open={isOpen} onOpenChange={handleToggle}>
|
|
115
|
+
<CollapsibleTrigger asChild>
|
|
116
|
+
<div
|
|
117
|
+
className={`directory-header${hasChanges ? ' has-changes' : ''}`}
|
|
118
|
+
style={{ paddingLeft: `${4 + depth * 16}px` }}
|
|
119
|
+
>
|
|
120
|
+
<span className="directory-toggle">
|
|
121
|
+
<span className="toggle-icon">{isOpen ? '▼' : '▶'}</span>
|
|
122
|
+
</span>
|
|
123
|
+
<span className="directory-name">{entry.name}</span>
|
|
124
|
+
</div>
|
|
125
|
+
</CollapsibleTrigger>
|
|
126
|
+
<CollapsibleContent>
|
|
127
|
+
{isLoading && (
|
|
128
|
+
<div
|
|
129
|
+
className="tree-loading"
|
|
130
|
+
style={{ paddingLeft: `${12 + (depth + 1) * 16}px` }}
|
|
131
|
+
>
|
|
132
|
+
Loading...
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{children?.map((child) =>
|
|
136
|
+
child.type === 'directory' ? (
|
|
137
|
+
<TreeDirectoryNode
|
|
138
|
+
key={child.path}
|
|
139
|
+
entry={child}
|
|
140
|
+
depth={depth + 1}
|
|
141
|
+
changedFiles={changedFiles}
|
|
142
|
+
onFileClick={onFileClick}
|
|
143
|
+
fetchDirectory={fetchDirectory}
|
|
144
|
+
cache={cache}
|
|
145
|
+
loading={loading}
|
|
146
|
+
/>
|
|
147
|
+
) : (
|
|
148
|
+
<TreeFileItem
|
|
149
|
+
key={child.path}
|
|
150
|
+
entry={child}
|
|
151
|
+
status={changedFiles.get(child.path)}
|
|
152
|
+
depth={depth + 1}
|
|
153
|
+
onFileClick={onFileClick}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
)}
|
|
157
|
+
</CollapsibleContent>
|
|
158
|
+
</Collapsible>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// FullFileTree Component
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
export function FullFileTree({ changedFiles, onFileClick }: FullFileTreeProps): React.ReactElement {
|
|
167
|
+
const { cache, loading, error, fetchDirectory } = useFileBrowser();
|
|
168
|
+
|
|
169
|
+
// Load root directory on mount
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
fetchDirectory('');
|
|
172
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
173
|
+
|
|
174
|
+
const rootEntries = cache['__root__'];
|
|
175
|
+
const changedCount = changedFiles.size;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<TooltipProvider delayDuration={300}>
|
|
179
|
+
<div role="tree" aria-label="Project files" className="filetree full-filetree">
|
|
180
|
+
{changedCount > 0 && (
|
|
181
|
+
<Badge
|
|
182
|
+
variant="secondary"
|
|
183
|
+
data-testid="file-count-badge"
|
|
184
|
+
className="file-count-badge"
|
|
185
|
+
aria-label={`${changedCount} files changed`}
|
|
186
|
+
>
|
|
187
|
+
{changedCount}
|
|
188
|
+
</Badge>
|
|
189
|
+
)}
|
|
190
|
+
<ScrollArea className="filetree-scroll">
|
|
191
|
+
{error && <div className="tree-error">{error}</div>}
|
|
192
|
+
{!rootEntries && !error && (
|
|
193
|
+
<div className="tree-loading">Loading project files...</div>
|
|
194
|
+
)}
|
|
195
|
+
{rootEntries?.map((entry) =>
|
|
196
|
+
entry.type === 'directory' ? (
|
|
197
|
+
<TreeDirectoryNode
|
|
198
|
+
key={entry.path}
|
|
199
|
+
entry={entry}
|
|
200
|
+
depth={0}
|
|
201
|
+
changedFiles={changedFiles}
|
|
202
|
+
onFileClick={onFileClick}
|
|
203
|
+
fetchDirectory={fetchDirectory}
|
|
204
|
+
cache={cache}
|
|
205
|
+
loading={loading}
|
|
206
|
+
/>
|
|
207
|
+
) : (
|
|
208
|
+
<TreeFileItem
|
|
209
|
+
key={entry.path}
|
|
210
|
+
entry={entry}
|
|
211
|
+
status={changedFiles.get(entry.path)}
|
|
212
|
+
depth={0}
|
|
213
|
+
onFileClick={onFileClick}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
)}
|
|
217
|
+
</ScrollArea>
|
|
218
|
+
</div>
|
|
219
|
+
</TooltipProvider>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default FullFileTree;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Story MSSCI-12777 - User Avatar from GitHub
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useState } from 'react';
|
|
9
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
10
10
|
import { Badge } from '@/components/ui/badge';
|
|
11
11
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
12
12
|
import { parseMarkdown } from '../utils/markdown';
|
|
@@ -17,25 +17,33 @@ import type { MessageData } from '../types/message';
|
|
|
17
17
|
|
|
18
18
|
interface MessageProps {
|
|
19
19
|
message: MessageData;
|
|
20
|
+
isLastAgentMessage?: boolean;
|
|
21
|
+
/** Whether this is the first message in a turn (shows avatar) */
|
|
22
|
+
isFirstInTurn?: boolean;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
interface AssistantAvatarProps {
|
|
23
26
|
isStreaming?: boolean;
|
|
27
|
+
agentSlug?: string;
|
|
28
|
+
agentTheme?: string;
|
|
29
|
+
agentCharacter?: string;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
function AssistantAvatar({ isStreaming }: AssistantAvatarProps): React.ReactElement {
|
|
32
|
+
function AssistantAvatar({ isStreaming, agentSlug, agentTheme, agentCharacter }: AssistantAvatarProps): React.ReactElement {
|
|
27
33
|
const { persona } = usePersona();
|
|
28
34
|
const [imageError, setImageError] = useState(false);
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
const
|
|
36
|
+
// Use per-message persona if available, fall back to current global persona
|
|
37
|
+
const slug = agentSlug || persona?.slug;
|
|
38
|
+
const theme = agentTheme || persona?.theme;
|
|
39
|
+
const character = agentCharacter || persona?.character;
|
|
32
40
|
const avatarClass = isStreaming ? 'avatar-portrait avatar-thinking' : 'avatar-portrait';
|
|
33
41
|
|
|
34
42
|
if (slug && theme && !imageError) {
|
|
35
43
|
return (
|
|
36
44
|
<img
|
|
37
45
|
src={`/portraits/${theme}/small/${slug}.png`}
|
|
38
|
-
alt={
|
|
46
|
+
alt={character || 'Agent'}
|
|
39
47
|
className={avatarClass}
|
|
40
48
|
onError={() => setImageError(true)}
|
|
41
49
|
/>
|
|
@@ -67,9 +75,66 @@ function UserAvatar(): React.ReactElement {
|
|
|
67
75
|
return <span className="avatar-emoji">👤</span>;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
function useSortableTables(contentRef: React.RefObject<HTMLDivElement | null>) {
|
|
79
|
+
const attachSort = useCallback(() => {
|
|
80
|
+
const el = contentRef.current;
|
|
81
|
+
if (!el) return;
|
|
82
|
+
const headers = el.querySelectorAll<HTMLTableCellElement>('th.sortable-th');
|
|
83
|
+
headers.forEach((th) => {
|
|
84
|
+
if (th.dataset.sortBound) return;
|
|
85
|
+
th.dataset.sortBound = '1';
|
|
86
|
+
th.style.cursor = 'pointer';
|
|
87
|
+
th.addEventListener('click', () => {
|
|
88
|
+
const colIdx = parseInt(th.dataset.col || '0', 10);
|
|
89
|
+
const table = th.closest('table');
|
|
90
|
+
if (!table) return;
|
|
91
|
+
const tbody = table.querySelector('tbody');
|
|
92
|
+
if (!tbody) return;
|
|
93
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
94
|
+
const currentDir = th.dataset.sortDir === 'asc' ? 'desc' : 'asc';
|
|
95
|
+
|
|
96
|
+
// Clear all indicators in this table
|
|
97
|
+
table.querySelectorAll<HTMLTableCellElement>('th.sortable-th').forEach((h) => {
|
|
98
|
+
h.dataset.sortDir = '';
|
|
99
|
+
const ind = h.querySelector('.sort-indicator');
|
|
100
|
+
if (ind) ind.textContent = '';
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
th.dataset.sortDir = currentDir;
|
|
104
|
+
const indicator = th.querySelector('.sort-indicator');
|
|
105
|
+
if (indicator) indicator.textContent = currentDir === 'asc' ? ' \u25B2' : ' \u25BC';
|
|
106
|
+
|
|
107
|
+
rows.sort((a, b) => {
|
|
108
|
+
const aText = (a.children[colIdx]?.textContent || '').trim();
|
|
109
|
+
const bText = (b.children[colIdx]?.textContent || '').trim();
|
|
110
|
+
const aNum = parseFloat(aText);
|
|
111
|
+
const bNum = parseFloat(bText);
|
|
112
|
+
// Numeric sort if both parse as numbers
|
|
113
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
114
|
+
return currentDir === 'asc' ? aNum - bNum : bNum - aNum;
|
|
115
|
+
}
|
|
116
|
+
const cmp = aText.localeCompare(bText, undefined, { sensitivity: 'base' });
|
|
117
|
+
return currentDir === 'asc' ? cmp : -cmp;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
tbody.appendChild(row);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}, [contentRef]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
attachSort();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default function Message({ message, isLastAgentMessage, isFirstInTurn = true }: MessageProps): React.ReactElement {
|
|
133
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
useSortableTables(contentRef);
|
|
71
135
|
const roleClass = `message-${message.type}`;
|
|
72
136
|
const testId = `message-${message.type}`;
|
|
137
|
+
const continuationClass = !isFirstInTurn ? ' continuation' : '';
|
|
73
138
|
|
|
74
139
|
// For bell-injected messages (queued messages injected via PostToolUse hook)
|
|
75
140
|
// Show with 🔔 indicator so user knows it was sent mid-turn
|
|
@@ -77,7 +142,7 @@ export default function Message({ message }: MessageProps): React.ReactElement {
|
|
|
77
142
|
const html = message.content ? parseMarkdown(message.content) : '';
|
|
78
143
|
return (
|
|
79
144
|
<TooltipProvider delayDuration={300}>
|
|
80
|
-
<div data-testid="message-bell-injected" className=
|
|
145
|
+
<div data-testid="message-bell-injected" className={`message message-user message-bell-injected${continuationClass}`}>
|
|
81
146
|
<div data-testid="avatar" className="message-avatar">
|
|
82
147
|
<UserAvatar />
|
|
83
148
|
</div>
|
|
@@ -106,11 +171,18 @@ export default function Message({ message }: MessageProps): React.ReactElement {
|
|
|
106
171
|
}
|
|
107
172
|
|
|
108
173
|
// For streaming agent messages, use StreamingContent with throbbing avatar
|
|
174
|
+
// Only throb the last agent message's avatar
|
|
109
175
|
if (message.type === 'agent' && message.isStreaming) {
|
|
176
|
+
const showThrob = isLastAgentMessage !== false;
|
|
110
177
|
return (
|
|
111
|
-
<div data-testid={testId} className={`message ${roleClass}`}>
|
|
178
|
+
<div data-testid={testId} className={`message ${roleClass}${continuationClass}`}>
|
|
112
179
|
<div data-testid="avatar" className="message-avatar">
|
|
113
|
-
<AssistantAvatar
|
|
180
|
+
<AssistantAvatar
|
|
181
|
+
isStreaming={showThrob}
|
|
182
|
+
agentSlug={message.agentSlug}
|
|
183
|
+
agentTheme={message.agentTheme}
|
|
184
|
+
agentCharacter={message.agentCharacter}
|
|
185
|
+
/>
|
|
114
186
|
</div>
|
|
115
187
|
<div className="message-content">
|
|
116
188
|
<StreamingContent content={message.content || ''} isStreaming={message.isStreaming ?? false} />
|
|
@@ -123,9 +195,15 @@ export default function Message({ message }: MessageProps): React.ReactElement {
|
|
|
123
195
|
const html = message.content ? parseMarkdown(message.content) : '';
|
|
124
196
|
|
|
125
197
|
return (
|
|
126
|
-
<div data-testid={testId} className={`message ${roleClass}`}>
|
|
198
|
+
<div data-testid={testId} className={`message ${roleClass}${continuationClass}`} ref={contentRef}>
|
|
127
199
|
<div data-testid="avatar" className="message-avatar">
|
|
128
|
-
{message.type === 'user' ? <UserAvatar /> :
|
|
200
|
+
{message.type === 'user' ? <UserAvatar /> : (
|
|
201
|
+
<AssistantAvatar
|
|
202
|
+
agentSlug={message.agentSlug}
|
|
203
|
+
agentTheme={message.agentTheme}
|
|
204
|
+
agentCharacter={message.agentCharacter}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
129
207
|
</div>
|
|
130
208
|
<div className="message-content">
|
|
131
209
|
<div dangerouslySetInnerHTML={{ __html: html }} />
|