@pennyfarthing/cyclist 9.4.0 → 10.0.1
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/LICENSE +14 -0
- 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/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -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/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 +41 -41
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -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/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 +31 -32
- package/src/public/App.tsx +59 -1
- package/src/public/components/ApprovalModal/index.tsx +31 -1
- package/src/public/components/AskUserQuestionBlock.tsx +162 -0
- package/src/public/components/ControlBar.tsx +18 -19
- package/src/public/components/DockviewWorkspace.tsx +35 -5
- package/src/public/components/Message.tsx +58 -2
- package/src/public/components/MessageView.tsx +47 -3
- package/src/public/components/PersonaHeader.tsx +3 -1
- package/src/public/components/panels/BackgroundPanel.tsx +1 -1
- package/src/public/components/panels/MessagePanel.tsx +66 -4
- package/src/public/components/panels/SettingsPanel.tsx +3 -28
- package/src/public/components/panels/WorkflowPanel.tsx +25 -3
- package/src/public/contexts/ClaudeContext.tsx +16 -1
- package/src/public/hooks/useColorScheme.ts +27 -0
- package/src/public/hooks/usePlanModeExit.ts +105 -0
- package/src/public/styles/dockview-theme.css +31 -33
- package/src/public/styles/tailwind.css +199 -18
- package/src/public/types/message.ts +2 -1
- package/src/public/utils/askUserQuestion.ts +21 -0
- package/src/public/utils/markdown.ts +2 -2
|
@@ -40,6 +40,7 @@ export const TOOL_NAME_TESTID = 'tool-name';
|
|
|
40
40
|
export const APPROVE_BUTTON_TESTID = 'approve-button';
|
|
41
41
|
export const REJECT_BUTTON_TESTID = 'reject-button';
|
|
42
42
|
export const ALWAYS_ALLOW_TESTID = 'always-allow-checkbox';
|
|
43
|
+
export const WARNING_TESTID = 'approval-modal-warning';
|
|
43
44
|
|
|
44
45
|
// ============================================================================
|
|
45
46
|
// Constants - Keyboard Shortcuts
|
|
@@ -120,6 +121,9 @@ export interface ApprovalRequest {
|
|
|
120
121
|
toolName: string;
|
|
121
122
|
input: ToolInput;
|
|
122
123
|
reason?: string;
|
|
124
|
+
severity?: ActionSeverity;
|
|
125
|
+
warning?: string;
|
|
126
|
+
agent?: string;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
export interface ApprovalResponse {
|
|
@@ -145,6 +149,12 @@ export interface ApprovalModalProps {
|
|
|
145
149
|
onDismiss?: () => void;
|
|
146
150
|
/** Additional CSS class name */
|
|
147
151
|
className?: string;
|
|
152
|
+
/** Server-provided severity classification (MSSCI-14323) */
|
|
153
|
+
severity?: ActionSeverity;
|
|
154
|
+
/** Server-provided warning text for destructive operations (MSSCI-14323) */
|
|
155
|
+
warning?: string;
|
|
156
|
+
/** Agent name requesting permission (MSSCI-14392) */
|
|
157
|
+
agent?: string;
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
interface UseApprovalModalResult {
|
|
@@ -361,6 +371,9 @@ interface HookRequestMessage {
|
|
|
361
371
|
toolId: string;
|
|
362
372
|
toolName: string;
|
|
363
373
|
input: Record<string, unknown>;
|
|
374
|
+
severity?: 'safe' | 'normal' | 'destructive';
|
|
375
|
+
warning?: string;
|
|
376
|
+
agent?: string;
|
|
364
377
|
context?: {
|
|
365
378
|
percentage: number;
|
|
366
379
|
isHigh: boolean;
|
|
@@ -417,6 +430,9 @@ export function subscribeToPermissionRequests(
|
|
|
417
430
|
toolId: msg.toolId,
|
|
418
431
|
toolName: msg.toolName,
|
|
419
432
|
input: msg.input as ToolInput,
|
|
433
|
+
severity: msg.severity as ActionSeverity | undefined,
|
|
434
|
+
warning: msg.warning,
|
|
435
|
+
agent: msg.agent,
|
|
420
436
|
});
|
|
421
437
|
}
|
|
422
438
|
} catch (err) {
|
|
@@ -487,6 +503,9 @@ export default function ApprovalModal({
|
|
|
487
503
|
onReject,
|
|
488
504
|
onDismiss,
|
|
489
505
|
className = '',
|
|
506
|
+
severity: serverSeverity,
|
|
507
|
+
warning,
|
|
508
|
+
agent,
|
|
490
509
|
}: ApprovalModalProps): React.ReactElement {
|
|
491
510
|
const [alwaysAllow, setAlwaysAllow] = useState(false);
|
|
492
511
|
|
|
@@ -506,7 +525,7 @@ export default function ApprovalModal({
|
|
|
506
525
|
return () => document.removeEventListener('keydown', handleKey);
|
|
507
526
|
}, [isOpen, alwaysAllow, onApprove]);
|
|
508
527
|
|
|
509
|
-
const severity = classifyActionSeverity(toolName, input);
|
|
528
|
+
const severity = serverSeverity ?? classifyActionSeverity(toolName, input);
|
|
510
529
|
const severityClass = SEVERITY_CLASSNAMES[severity];
|
|
511
530
|
const preview = formatCommandPreview(toolName, input);
|
|
512
531
|
const icon = getToolIcon(toolName);
|
|
@@ -546,6 +565,8 @@ export default function ApprovalModal({
|
|
|
546
565
|
<div>
|
|
547
566
|
<div className="flex items-center gap-2 mb-3 text-sm text-muted-foreground">
|
|
548
567
|
<span className="approval-modal__icon" data-icon={icon} />
|
|
568
|
+
{agent && <span data-testid="agent-name" className="font-medium">{agent}</span>}
|
|
569
|
+
{agent && <span className="text-muted-foreground/50">/</span>}
|
|
549
570
|
<span data-testid={TOOL_NAME_TESTID}>{toolName}</span>
|
|
550
571
|
</div>
|
|
551
572
|
|
|
@@ -564,6 +585,15 @@ export default function ApprovalModal({
|
|
|
564
585
|
</DialogDescription>
|
|
565
586
|
</DialogHeader>
|
|
566
587
|
|
|
588
|
+
{warning && (
|
|
589
|
+
<div
|
|
590
|
+
data-testid={WARNING_TESTID}
|
|
591
|
+
className="text-sm text-destructive font-medium"
|
|
592
|
+
>
|
|
593
|
+
{warning}
|
|
594
|
+
</div>
|
|
595
|
+
)}
|
|
596
|
+
|
|
567
597
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
568
598
|
<Checkbox
|
|
569
599
|
id="always-allow"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestionBlock Component
|
|
3
|
+
*
|
|
4
|
+
* Renders interactive buttons for AskUserQuestion tool_use messages.
|
|
5
|
+
* Uses the existing ClaudeContext to send responses back via WebSocket.
|
|
6
|
+
*
|
|
7
|
+
* Story: MSSCI-14395 - Render AskUserQuestion tool via Reflector QuickActions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useCallback } from 'react';
|
|
11
|
+
import { Button } from '@/components/ui/button';
|
|
12
|
+
import { Badge } from '@/components/ui/badge';
|
|
13
|
+
import { useClaudeContext } from '../contexts/ClaudeContext';
|
|
14
|
+
|
|
15
|
+
interface QuestionOption {
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Question {
|
|
21
|
+
question: string;
|
|
22
|
+
header: string;
|
|
23
|
+
options: QuestionOption[];
|
|
24
|
+
multiSelect: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AskUserQuestionToolUse {
|
|
28
|
+
type: 'tool_use';
|
|
29
|
+
tool_name: string;
|
|
30
|
+
tool_id: string;
|
|
31
|
+
input: {
|
|
32
|
+
questions: Question[];
|
|
33
|
+
};
|
|
34
|
+
timestamp: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface AskUserQuestionBlockProps {
|
|
38
|
+
toolUse: AskUserQuestionToolUse;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function SingleSelectQuestion({ question, onSubmit, disabled }: {
|
|
42
|
+
question: Question;
|
|
43
|
+
onSubmit: (answer: string) => void;
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="ask-question-group">
|
|
48
|
+
<Badge variant="secondary" className="ask-question-header">{question.header}</Badge>
|
|
49
|
+
<p className="ask-question-text">{question.question}</p>
|
|
50
|
+
<div className="ask-question-options">
|
|
51
|
+
{question.options.map((opt) => (
|
|
52
|
+
<Button
|
|
53
|
+
key={opt.label}
|
|
54
|
+
variant="secondary"
|
|
55
|
+
size="sm"
|
|
56
|
+
className="ask-question-option"
|
|
57
|
+
onClick={() => onSubmit(opt.label)}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
>
|
|
60
|
+
<span className="ask-option-label">{opt.label}</span>
|
|
61
|
+
<span className="ask-option-desc">{opt.description}</span>
|
|
62
|
+
</Button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function MultiSelectQuestion({ question, onSubmit, disabled }: {
|
|
70
|
+
question: Question;
|
|
71
|
+
onSubmit: (answers: string[]) => void;
|
|
72
|
+
disabled: boolean;
|
|
73
|
+
}) {
|
|
74
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
75
|
+
|
|
76
|
+
const toggleOption = useCallback((label: string) => {
|
|
77
|
+
setSelected(prev => {
|
|
78
|
+
const next = new Set(prev);
|
|
79
|
+
if (next.has(label)) {
|
|
80
|
+
next.delete(label);
|
|
81
|
+
} else {
|
|
82
|
+
next.add(label);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const handleSubmit = useCallback(() => {
|
|
89
|
+
onSubmit(Array.from(selected));
|
|
90
|
+
}, [selected, onSubmit]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="ask-question-group">
|
|
94
|
+
<Badge variant="secondary" className="ask-question-header">{question.header}</Badge>
|
|
95
|
+
<p className="ask-question-text">{question.question}</p>
|
|
96
|
+
<div className="ask-question-options">
|
|
97
|
+
{question.options.map((opt) => (
|
|
98
|
+
<Button
|
|
99
|
+
key={opt.label}
|
|
100
|
+
variant="secondary"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="ask-question-option"
|
|
103
|
+
onClick={() => toggleOption(opt.label)}
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
aria-pressed={selected.has(opt.label)}
|
|
106
|
+
>
|
|
107
|
+
<span className="ask-option-label">{opt.label}</span>
|
|
108
|
+
<span className="ask-option-desc">{opt.description}</span>
|
|
109
|
+
</Button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
<Button
|
|
113
|
+
variant="default"
|
|
114
|
+
size="sm"
|
|
115
|
+
className="ask-question-submit"
|
|
116
|
+
onClick={handleSubmit}
|
|
117
|
+
disabled={disabled || selected.size === 0}
|
|
118
|
+
aria-label="Confirm"
|
|
119
|
+
>
|
|
120
|
+
Confirm
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function AskUserQuestionBlock({ toolUse }: AskUserQuestionBlockProps): React.ReactElement {
|
|
127
|
+
const { send } = useClaudeContext();
|
|
128
|
+
const [isDisabled, setIsDisabled] = useState(false);
|
|
129
|
+
const questions = toolUse.input?.questions || [];
|
|
130
|
+
|
|
131
|
+
const handleSingleSelect = useCallback((answer: string) => {
|
|
132
|
+
setIsDisabled(true);
|
|
133
|
+
send(answer, []);
|
|
134
|
+
}, [send]);
|
|
135
|
+
|
|
136
|
+
const handleMultiSelect = useCallback((answers: string[]) => {
|
|
137
|
+
setIsDisabled(true);
|
|
138
|
+
send(answers.join(', '), []);
|
|
139
|
+
}, [send]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="ask-user-question-block">
|
|
143
|
+
{questions.map((q, i) => (
|
|
144
|
+
q.multiSelect ? (
|
|
145
|
+
<MultiSelectQuestion
|
|
146
|
+
key={i}
|
|
147
|
+
question={q}
|
|
148
|
+
onSubmit={handleMultiSelect}
|
|
149
|
+
disabled={isDisabled}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
<SingleSelectQuestion
|
|
153
|
+
key={i}
|
|
154
|
+
question={q}
|
|
155
|
+
onSubmit={handleSingleSelect}
|
|
156
|
+
disabled={isDisabled}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -174,25 +174,24 @@ export function ControlBar({
|
|
|
174
174
|
<TooltipContent>Relay Mode: Auto-handoff to next agent (Cmd+4)</TooltipContent>
|
|
175
175
|
</Tooltip>
|
|
176
176
|
|
|
177
|
-
{/* TirePump Button - visible
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)}
|
|
177
|
+
{/* TirePump Button - always visible, warning style at 70%+ */}
|
|
178
|
+
<Tooltip>
|
|
179
|
+
<TooltipTrigger asChild>
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="icon"
|
|
183
|
+
type="button"
|
|
184
|
+
className={`btn-toggle pump-toggle ${contextPercent >= 70 ? 'warning' : ''}`}
|
|
185
|
+
data-testid="pump-toggle"
|
|
186
|
+
onClick={onTirePump}
|
|
187
|
+
disabled={!currentAgent}
|
|
188
|
+
aria-label="TirePump: Clear context and reload agent"
|
|
189
|
+
>
|
|
190
|
+
<span className="toggle-icon">⬆️</span>
|
|
191
|
+
</Button>
|
|
192
|
+
</TooltipTrigger>
|
|
193
|
+
<TooltipContent>{currentAgent ? `TirePump: Clear context (${contextPercent}%) and reload ${currentAgent}` : 'TirePump: No agent loaded'}</TooltipContent>
|
|
194
|
+
</Tooltip>
|
|
196
195
|
</div>
|
|
197
196
|
|
|
198
197
|
{/* Stop button - always visible, disabled when not running */}
|
|
@@ -107,7 +107,7 @@ const PANEL_TITLES: Record<string, string> = {
|
|
|
107
107
|
workflow: 'Workflow',
|
|
108
108
|
ac: 'AC',
|
|
109
109
|
todo: 'Todo',
|
|
110
|
-
background: '
|
|
110
|
+
background: 'Subagents',
|
|
111
111
|
git: 'Git',
|
|
112
112
|
hotspots: 'Hotspots',
|
|
113
113
|
settings: 'Settings',
|
|
@@ -257,6 +257,7 @@ export function createDefaultDockviewLayout(): SerializedDockview {
|
|
|
257
257
|
views: [PANEL_INVENTORY.MESSAGE],
|
|
258
258
|
activeView: PANEL_INVENTORY.MESSAGE,
|
|
259
259
|
id: 'center',
|
|
260
|
+
hideHeader: true,
|
|
260
261
|
},
|
|
261
262
|
size: 600, // Center takes remaining space
|
|
262
263
|
},
|
|
@@ -433,10 +434,11 @@ export function DockviewWorkspace({
|
|
|
433
434
|
try {
|
|
434
435
|
api.fromJSON(initialLayout);
|
|
435
436
|
|
|
436
|
-
// After restoring, lock the message panel's group
|
|
437
|
+
// After restoring, lock the message panel's group and hide its tab bar
|
|
437
438
|
const messagePanel = api.getPanel(PANEL_INVENTORY.MESSAGE);
|
|
438
439
|
if (messagePanel?.group) {
|
|
439
440
|
messagePanel.group.locked = 'no-drop-target';
|
|
441
|
+
messagePanel.group.model.header.hidden = true;
|
|
440
442
|
}
|
|
441
443
|
|
|
442
444
|
setIsReady(true);
|
|
@@ -503,8 +505,10 @@ export function DockviewWorkspace({
|
|
|
503
505
|
}
|
|
504
506
|
|
|
505
507
|
// Lock the center group - MessagePanel cannot be closed or moved
|
|
508
|
+
// Hide the tab bar so users can't accidentally close the message tab
|
|
506
509
|
if (messagePanel?.group) {
|
|
507
510
|
messagePanel.group.locked = 'no-drop-target';
|
|
511
|
+
messagePanel.group.model.header.hidden = true;
|
|
508
512
|
}
|
|
509
513
|
|
|
510
514
|
// Set initial sidebar sizes
|
|
@@ -555,9 +559,9 @@ export function DockviewWorkspace({
|
|
|
555
559
|
handleLayoutChange();
|
|
556
560
|
}),
|
|
557
561
|
api.onDidRemovePanel((e) => {
|
|
558
|
-
// Track closed panels
|
|
562
|
+
// Track closed panels for restoration
|
|
559
563
|
const panelId = e?.panel?.id;
|
|
560
|
-
if (panelId
|
|
564
|
+
if (panelId) {
|
|
561
565
|
closedPanels.add(panelId);
|
|
562
566
|
updateClosedPanelsList();
|
|
563
567
|
}
|
|
@@ -622,6 +626,32 @@ export function DockviewWorkspace({
|
|
|
622
626
|
};
|
|
623
627
|
}, []);
|
|
624
628
|
|
|
629
|
+
// Listen for panel toggle commands via WebSocket (View menu integration)
|
|
630
|
+
useEffect(() => {
|
|
631
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
632
|
+
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/settings`);
|
|
633
|
+
|
|
634
|
+
ws.onmessage = (event) => {
|
|
635
|
+
try {
|
|
636
|
+
const data = JSON.parse(event.data);
|
|
637
|
+
if (data.type === 'panel:toggle' && data.panelId) {
|
|
638
|
+
const api = dockviewApiRef;
|
|
639
|
+
if (!api) return;
|
|
640
|
+
const existing = api.getPanel(data.panelId);
|
|
641
|
+
if (existing) {
|
|
642
|
+
existing.api.close();
|
|
643
|
+
} else {
|
|
644
|
+
restorePanel(data.panelId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
// ignore parse errors
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
return () => ws.close();
|
|
653
|
+
}, []);
|
|
654
|
+
|
|
625
655
|
// Component map for Dockview
|
|
626
656
|
const components = {
|
|
627
657
|
PanelAdapter,
|
|
@@ -638,7 +668,7 @@ export function DockviewWorkspace({
|
|
|
638
668
|
workflow: 'Workflow',
|
|
639
669
|
ac: 'AC',
|
|
640
670
|
todo: 'Todo',
|
|
641
|
-
background: '
|
|
671
|
+
background: 'Subagents',
|
|
642
672
|
git: 'Git',
|
|
643
673
|
hotspots: 'Hotspots',
|
|
644
674
|
settings: 'Settings',
|
|
@@ -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';
|
|
@@ -75,7 +75,63 @@ function UserAvatar(): React.ReactElement {
|
|
|
75
75
|
return <span className="avatar-emoji">👤</span>;
|
|
76
76
|
}
|
|
77
77
|
|
|
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
|
+
|
|
78
132
|
export default function Message({ message, isLastAgentMessage, isFirstInTurn = true }: MessageProps): React.ReactElement {
|
|
133
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
useSortableTables(contentRef);
|
|
79
135
|
const roleClass = `message-${message.type}`;
|
|
80
136
|
const testId = `message-${message.type}`;
|
|
81
137
|
const continuationClass = !isFirstInTurn ? ' continuation' : '';
|
|
@@ -139,7 +195,7 @@ export default function Message({ message, isLastAgentMessage, isFirstInTurn = t
|
|
|
139
195
|
const html = message.content ? parseMarkdown(message.content) : '';
|
|
140
196
|
|
|
141
197
|
return (
|
|
142
|
-
<div data-testid={testId} className={`message ${roleClass}${continuationClass}`}>
|
|
198
|
+
<div data-testid={testId} className={`message ${roleClass}${continuationClass}`} ref={contentRef}>
|
|
143
199
|
<div data-testid="avatar" className="message-avatar">
|
|
144
200
|
{message.type === 'user' ? <UserAvatar /> : (
|
|
145
201
|
<AssistantAvatar
|
|
@@ -20,12 +20,15 @@ import { Button } from '@/components/ui/button';
|
|
|
20
20
|
import MessageList, { MessageListHandle } from './MessageList';
|
|
21
21
|
import Message from './Message';
|
|
22
22
|
import ToolCallBlock from './ToolCallBlock';
|
|
23
|
+
import { AskUserQuestionBlock } from './AskUserQuestionBlock';
|
|
23
24
|
import ToolStack from './ToolStack';
|
|
24
25
|
import SubagentSpan from './SubagentSpan';
|
|
25
26
|
import QuickActions from './QuickActions';
|
|
27
|
+
import { Separator } from '@/components/ui/separator';
|
|
26
28
|
import { isSkillContent } from '../utils/messageFilters';
|
|
27
29
|
import { groupToolsIntoStacks, ToolStackData } from '../utils/toolStackGrouper';
|
|
28
30
|
import { usePersona } from '../hooks/usePersona';
|
|
31
|
+
import { useColorScheme } from '../hooks/useColorScheme';
|
|
29
32
|
import { useStatsStrip } from '../hooks/useStatsStrip';
|
|
30
33
|
import type { MessageData } from '../types/message';
|
|
31
34
|
|
|
@@ -61,7 +64,7 @@ interface ToolStackGroup {
|
|
|
61
64
|
type RenderItem = MessageData | SubagentGroup | ToolStackGroup;
|
|
62
65
|
|
|
63
66
|
interface Turn {
|
|
64
|
-
speaker: 'user' | 'agent';
|
|
67
|
+
speaker: 'user' | 'agent' | 'system';
|
|
65
68
|
items: RenderItem[];
|
|
66
69
|
timestamp: number;
|
|
67
70
|
}
|
|
@@ -74,9 +77,10 @@ function formatTurnTime(timestamp: number): string {
|
|
|
74
77
|
* Classify an item as 'user' or 'agent' for turn grouping.
|
|
75
78
|
* Tools, subagents, and stacks are all part of the agent's turn.
|
|
76
79
|
*/
|
|
77
|
-
function speakerOf(item: RenderItem): 'user' | 'agent' {
|
|
80
|
+
function speakerOf(item: RenderItem): 'user' | 'agent' | 'system' {
|
|
78
81
|
if ('isToolStack' in item || 'messages' in item) return 'agent';
|
|
79
82
|
const msg = item as MessageData;
|
|
83
|
+
if (msg.type === 'context_cleared') return 'system';
|
|
80
84
|
return (msg.type === 'user' || msg.type === 'bell_injected') ? 'user' : 'agent';
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -84,6 +88,7 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
84
88
|
const messageListRef = useRef<MessageListHandle>(null);
|
|
85
89
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
86
90
|
const { persona } = usePersona();
|
|
91
|
+
const colorScheme = useColorScheme();
|
|
87
92
|
const { projectInfo } = useStatsStrip();
|
|
88
93
|
|
|
89
94
|
// Persist subagent collapsed state across re-renders/remounts
|
|
@@ -223,6 +228,21 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
223
228
|
const msg = item as MessageData;
|
|
224
229
|
|
|
225
230
|
if (msg.type === 'tool_use' && msg.tool_name && msg.tool_id) {
|
|
231
|
+
// MSSCI-14395: Render AskUserQuestion as interactive buttons
|
|
232
|
+
if (msg.tool_name === 'AskUserQuestion') {
|
|
233
|
+
return (
|
|
234
|
+
<AskUserQuestionBlock
|
|
235
|
+
key={`ask-${msg.tool_id}`}
|
|
236
|
+
toolUse={{
|
|
237
|
+
type: 'tool_use',
|
|
238
|
+
tool_name: msg.tool_name,
|
|
239
|
+
tool_id: msg.tool_id,
|
|
240
|
+
input: (msg.input || {}) as { questions: Array<{ question: string; header: string; options: Array<{ label: string; description: string }>; multiSelect: boolean }> },
|
|
241
|
+
timestamp: msg.timestamp,
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
226
246
|
const result = toolResults.get(msg.tool_id);
|
|
227
247
|
return (
|
|
228
248
|
<ToolCallBlock
|
|
@@ -262,7 +282,11 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
262
282
|
<div data-testid="message-view" className="message-view">
|
|
263
283
|
<div className="message-view-empty">
|
|
264
284
|
<div>
|
|
265
|
-
<
|
|
285
|
+
<img
|
|
286
|
+
src={colorScheme === 'dark' ? '/images/cyclist-dark.png' : '/images/cyclist-light.png'}
|
|
287
|
+
alt="Cyclist"
|
|
288
|
+
style={{ height: '2.5rem', marginBottom: '0.5rem', opacity: 0.6 }}
|
|
289
|
+
/>
|
|
266
290
|
<div>Type <code style={{
|
|
267
291
|
background: 'var(--bg-tertiary, #0f0f1a)',
|
|
268
292
|
padding: '2px 6px',
|
|
@@ -286,6 +310,26 @@ export default function MessageView({ messages }: MessageViewProps): React.React
|
|
|
286
310
|
autoScroll={isAtBottom}
|
|
287
311
|
>
|
|
288
312
|
{turns.map((turn, turnIndex) => {
|
|
313
|
+
// System turns (context_cleared) render as a divider bar
|
|
314
|
+
if (turn.speaker === 'system') {
|
|
315
|
+
// Still increment globalIdx for system items
|
|
316
|
+
turn.items.forEach(() => globalIdx++);
|
|
317
|
+
return (
|
|
318
|
+
<div key={`turn-${turnIndex}`} className="turn-group turn-system">
|
|
319
|
+
<div className="context-cleared-bar">
|
|
320
|
+
<Separator className="context-cleared-line" />
|
|
321
|
+
<span className="context-cleared-label">
|
|
322
|
+
Context cleared
|
|
323
|
+
</span>
|
|
324
|
+
<span className="context-cleared-time">
|
|
325
|
+
{formatTurnTime(turn.timestamp)}
|
|
326
|
+
</span>
|
|
327
|
+
<Separator className="context-cleared-line" />
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
289
333
|
// Track which items in this turn are "first message" (non-tool, non-stack)
|
|
290
334
|
let seenMessage = false;
|
|
291
335
|
|
|
@@ -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)
|
|
@@ -59,6 +60,7 @@ function humanizeTheme(theme: string): string {
|
|
|
59
60
|
|
|
60
61
|
export default function PersonaHeader(): React.ReactElement {
|
|
61
62
|
const { persona } = usePersona();
|
|
63
|
+
const colorScheme = useColorScheme();
|
|
62
64
|
const [portraitError, setPortraitError] = useState(false);
|
|
63
65
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
|
64
66
|
const [isCompact, setIsCompact] = useState(false);
|
|
@@ -165,7 +167,7 @@ export default function PersonaHeader(): React.ReactElement {
|
|
|
165
167
|
)}
|
|
166
168
|
</div>
|
|
167
169
|
<img
|
|
168
|
-
src=
|
|
170
|
+
src={colorScheme === 'dark' ? '/images/cyclist-dark.png' : '/images/cyclist-light.png'}
|
|
169
171
|
alt="Cyclist"
|
|
170
172
|
className="persona-branding"
|
|
171
173
|
/>
|
|
@@ -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
|
}
|