@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.
Files changed (78) hide show
  1. package/LICENSE +14 -0
  2. package/dist/api/hook-request.d.ts +11 -0
  3. package/dist/api/hook-request.d.ts.map +1 -1
  4. package/dist/api/hook-request.js +126 -28
  5. package/dist/api/hook-request.js.map +1 -1
  6. package/dist/api/index.d.ts +1 -0
  7. package/dist/api/index.d.ts.map +1 -1
  8. package/dist/api/index.js +2 -0
  9. package/dist/api/index.js.map +1 -1
  10. package/dist/api/permissions.d.ts +16 -0
  11. package/dist/api/permissions.d.ts.map +1 -0
  12. package/dist/api/permissions.js +67 -0
  13. package/dist/api/permissions.js.map +1 -0
  14. package/dist/api/theme-agents.d.ts +4 -0
  15. package/dist/api/theme-agents.d.ts.map +1 -1
  16. package/dist/api/theme-agents.js +3 -0
  17. package/dist/api/theme-agents.js.map +1 -1
  18. package/dist/approval-gate.d.ts +3 -75
  19. package/dist/approval-gate.d.ts.map +1 -1
  20. package/dist/approval-gate.js +4 -121
  21. package/dist/approval-gate.js.map +1 -1
  22. package/dist/hooks/cyclist-pretooluse-hook.d.ts +60 -0
  23. package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +1 -0
  24. package/dist/hooks/cyclist-pretooluse-hook.js +57 -0
  25. package/dist/hooks/cyclist-pretooluse-hook.js.map +1 -0
  26. package/dist/hooks/pretooluse-hook.d.ts +89 -0
  27. package/dist/hooks/pretooluse-hook.d.ts.map +1 -0
  28. package/dist/hooks/pretooluse-hook.js +235 -0
  29. package/dist/hooks/pretooluse-hook.js.map +1 -0
  30. package/dist/main.d.ts +1 -134
  31. package/dist/main.d.ts.map +1 -1
  32. package/dist/main.js +42 -373
  33. package/dist/main.js.map +1 -1
  34. package/dist/menu-builder.d.ts +7 -1
  35. package/dist/menu-builder.d.ts.map +1 -1
  36. package/dist/menu-builder.js +36 -1
  37. package/dist/menu-builder.js.map +1 -1
  38. package/dist/otlp-receiver.d.ts.map +1 -1
  39. package/dist/otlp-receiver.js +6 -0
  40. package/dist/otlp-receiver.js.map +1 -1
  41. package/dist/public/css/react.css +1 -1
  42. package/dist/public/js/react/react.js +41 -41
  43. package/dist/server.d.ts.map +1 -1
  44. package/dist/server.js +14 -3
  45. package/dist/server.js.map +1 -1
  46. package/dist/settings-store.d.ts +3 -1
  47. package/dist/settings-store.d.ts.map +1 -1
  48. package/dist/settings-store.js +18 -9
  49. package/dist/settings-store.js.map +1 -1
  50. package/dist/websocket.d.ts +1 -0
  51. package/dist/websocket.d.ts.map +1 -1
  52. package/dist/websocket.js +48 -5
  53. package/dist/websocket.js.map +1 -1
  54. package/dist/workflow-presets.d.ts +72 -0
  55. package/dist/workflow-presets.d.ts.map +1 -0
  56. package/dist/workflow-presets.js +93 -0
  57. package/dist/workflow-presets.js.map +1 -0
  58. package/package.json +31 -32
  59. package/src/public/App.tsx +59 -1
  60. package/src/public/components/ApprovalModal/index.tsx +31 -1
  61. package/src/public/components/AskUserQuestionBlock.tsx +162 -0
  62. package/src/public/components/ControlBar.tsx +18 -19
  63. package/src/public/components/DockviewWorkspace.tsx +35 -5
  64. package/src/public/components/Message.tsx +58 -2
  65. package/src/public/components/MessageView.tsx +47 -3
  66. package/src/public/components/PersonaHeader.tsx +3 -1
  67. package/src/public/components/panels/BackgroundPanel.tsx +1 -1
  68. package/src/public/components/panels/MessagePanel.tsx +66 -4
  69. package/src/public/components/panels/SettingsPanel.tsx +3 -28
  70. package/src/public/components/panels/WorkflowPanel.tsx +25 -3
  71. package/src/public/contexts/ClaudeContext.tsx +16 -1
  72. package/src/public/hooks/useColorScheme.ts +27 -0
  73. package/src/public/hooks/usePlanModeExit.ts +105 -0
  74. package/src/public/styles/dockview-theme.css +31 -33
  75. package/src/public/styles/tailwind.css +199 -18
  76. package/src/public/types/message.ts +2 -1
  77. package/src/public/utils/askUserQuestion.ts +21 -0
  78. 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 at 50%+ context, warning at 70%+ */}
178
- {contextPercent >= 50 && currentAgent && (
179
- <Tooltip>
180
- <TooltipTrigger asChild>
181
- <Button
182
- variant="ghost"
183
- size="icon"
184
- type="button"
185
- className={`btn-toggle pump-toggle ${contextPercent >= 70 ? 'warning' : ''}`}
186
- data-testid="pump-toggle"
187
- onClick={onTirePump}
188
- aria-label="TirePump: Clear context and reload agent"
189
- >
190
- <span className="toggle-icon">🫧</span>
191
- </Button>
192
- </TooltipTrigger>
193
- <TooltipContent>{`TirePump: Clear context (${contextPercent}%) and reload ${currentAgent}`}</TooltipContent>
194
- </Tooltip>
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: '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 (except message which can't be closed)
562
+ // Track closed panels for restoration
559
563
  const panelId = e?.panel?.id;
560
- if (panelId && panelId !== PANEL_INVENTORY.MESSAGE) {
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: '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
- <div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>🚴</div>
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="/images/cyclist-dark.png"
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 background tasks</div>
74
+ <div className="placeholder">No subagent tasks</div>
75
75
  </div>
76
76
  );
77
77
  }