@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 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-presets.js","sourceRoot":"","sources":["../src/workflow-presets.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAyC1D;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAC1C,WAAmC;IAEnC,OAAO,WAAW,CAAC,WAAW,IAAI,EAAE,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAmC;IAEnC,MAAM,aAAa,GAAG,SAAS,EAAE,CAAC;IAClC,MAAM,OAAO,GAA+B,EAAE,CAAC;IAC/C,MAAM,OAAO,GAA+B,EAAE,CAAC;IAE/C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CACjC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAC1D,CAAC;QACF,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO;QACL,UAAU,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;QAChC,OAAO;QACP,OAAO;KACR,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,+BAA+B,CAC7C,WAAuC,EACvC,OAAkE;IAElE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7B,IAAI,EAAE,0BAA0B;QAChC,WAAW;KACZ,CAAC,CAAC;IACH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAuC,EACvC,YAAoB;IAEpB,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,YAAY;QACZ,WAAW;KACZ,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAAuC,EACvC,UAAyC;IAEzC,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,QAAQ,CAAC;YACP,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,UAAU;YACtB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,yCAAyC;KAClD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2BAA2B,CACzC,OAAe;IAEf,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pennyfarthing/cyclist",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.0",
|
|
4
4
|
"description": "Visual terminal interface for Claude Code",
|
|
5
5
|
"author": "1898andCo",
|
|
6
6
|
"type": "module",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"build": {
|
|
116
116
|
"appId": "com.cyclist.app",
|
|
117
117
|
"productName": "Cyclist",
|
|
118
|
-
"copyright": "Copyright
|
|
118
|
+
"copyright": "Copyright \u00a9 2026",
|
|
119
119
|
"asar": true,
|
|
120
120
|
"directories": {
|
|
121
121
|
"output": "release",
|
package/src/public/App.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Persists layout changes to config.local.yaml.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import React, { useEffect, useCallback } from 'react';
|
|
12
|
+
import React, { useEffect, useCallback, useState } from 'react';
|
|
13
13
|
import {
|
|
14
14
|
DockviewWorkspace,
|
|
15
15
|
registerPanelComponent,
|
|
@@ -20,7 +20,11 @@ import { ClaudeProvider } from './contexts/ClaudeContext';
|
|
|
20
20
|
import { MessageQueueProvider } from './contexts/MessageQueueContext';
|
|
21
21
|
import { useLayoutPersistence } from './hooks/useLayoutPersistence';
|
|
22
22
|
import { loadFontSettings, applyFontSettings } from './utils/font-presets';
|
|
23
|
+
import { loadPresetFromProject, applyPreset } from './utils/color-presets';
|
|
23
24
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
25
|
+
import ApprovalModal, { useApprovalModal } from './components/ApprovalModal';
|
|
26
|
+
import { subscribeToPermissionRequests, sendPermissionResponse, createApprovalResponse } from './components/ApprovalModal';
|
|
27
|
+
import type { ApprovalRequest, GrantScope } from './components/ApprovalModal';
|
|
24
28
|
|
|
25
29
|
// Import all panel components
|
|
26
30
|
// Note: ProgressPanel split into Workflow/AC/Todo panels (MSSCI-14188)
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
SettingsPanel,
|
|
39
43
|
AuditLogPanel,
|
|
40
44
|
TTYPanel,
|
|
45
|
+
HotspotsPanel,
|
|
41
46
|
} from './components/panels';
|
|
42
47
|
|
|
43
48
|
// =============================================================================
|
|
@@ -63,6 +68,7 @@ registerPanelComponent(PANEL_INVENTORY.AC, ACPanel);
|
|
|
63
68
|
registerPanelComponent(PANEL_INVENTORY.TODO, TodoPanel);
|
|
64
69
|
registerPanelComponent(PANEL_INVENTORY.BACKGROUND, BackgroundPanel);
|
|
65
70
|
registerPanelComponent(PANEL_INVENTORY.GIT, GitPanel);
|
|
71
|
+
registerPanelComponent(PANEL_INVENTORY.HOTSPOTS, HotspotsPanel);
|
|
66
72
|
registerPanelComponent(PANEL_INVENTORY.SETTINGS, SettingsPanel);
|
|
67
73
|
|
|
68
74
|
// =============================================================================
|
|
@@ -206,6 +212,50 @@ export default function App(): React.ReactElement {
|
|
|
206
212
|
});
|
|
207
213
|
}, []);
|
|
208
214
|
|
|
215
|
+
// Load and apply color preset on startup
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
loadPresetFromProject().then(presetId => {
|
|
218
|
+
applyPreset(presetId);
|
|
219
|
+
});
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
// ApprovalModal state management (MSSCI-14322)
|
|
223
|
+
const [requestQueue, setRequestQueue] = useState<ApprovalRequest[]>([]);
|
|
224
|
+
const { request, isOpen, show, hide } = useApprovalModal();
|
|
225
|
+
|
|
226
|
+
// Subscribe to permission requests via WebSocket
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const unsub = subscribeToPermissionRequests((incoming: ApprovalRequest) => {
|
|
229
|
+
setRequestQueue((prev) => [...prev, incoming]);
|
|
230
|
+
});
|
|
231
|
+
return unsub;
|
|
232
|
+
}, []);
|
|
233
|
+
|
|
234
|
+
// Show the next queued request when the current one is resolved
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!isOpen && requestQueue.length > 0) {
|
|
237
|
+
const [next, ...rest] = requestQueue;
|
|
238
|
+
setRequestQueue(rest);
|
|
239
|
+
show(next);
|
|
240
|
+
}
|
|
241
|
+
}, [isOpen, requestQueue, show]);
|
|
242
|
+
|
|
243
|
+
const handleApprove = useCallback((grantScope: GrantScope) => {
|
|
244
|
+
if (request) {
|
|
245
|
+
const response = createApprovalResponse(request.toolId, true, grantScope);
|
|
246
|
+
sendPermissionResponse(response);
|
|
247
|
+
}
|
|
248
|
+
hide();
|
|
249
|
+
}, [request, hide]);
|
|
250
|
+
|
|
251
|
+
const handleReject = useCallback(() => {
|
|
252
|
+
if (request) {
|
|
253
|
+
const response = createApprovalResponse(request.toolId, false);
|
|
254
|
+
sendPermissionResponse(response);
|
|
255
|
+
}
|
|
256
|
+
hide();
|
|
257
|
+
}, [request, hide]);
|
|
258
|
+
|
|
209
259
|
return (
|
|
210
260
|
<ErrorBoundary fallback={<RootErrorFallback />} panelName="App">
|
|
211
261
|
<ClaudeProvider>
|
|
@@ -240,6 +290,16 @@ export default function App(): React.ReactElement {
|
|
|
240
290
|
{/* Message input target (for skip link) */}
|
|
241
291
|
<div id="message-input" tabIndex={-1} style={{ display: 'contents' }} aria-hidden="true" />
|
|
242
292
|
</div>
|
|
293
|
+
|
|
294
|
+
{/* ApprovalModal - renders via Radix Portal outside the React tree (MSSCI-14322) */}
|
|
295
|
+
<ApprovalModal
|
|
296
|
+
isOpen={isOpen}
|
|
297
|
+
toolName={request?.toolName ?? ''}
|
|
298
|
+
toolId={request?.toolId ?? ''}
|
|
299
|
+
input={request?.input ?? {}}
|
|
300
|
+
onApprove={handleApprove}
|
|
301
|
+
onReject={handleReject}
|
|
302
|
+
/>
|
|
243
303
|
</CommandPaletteProvider>
|
|
244
304
|
</MessageQueueProvider>
|
|
245
305
|
</ClaudeProvider>
|
|
@@ -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"
|
|
@@ -168,31 +168,30 @@ export function ControlBar({
|
|
|
168
168
|
aria-pressed={relayMode}
|
|
169
169
|
aria-label="Relay mode - auto-handoff to next agent"
|
|
170
170
|
>
|
|
171
|
-
<span className="toggle-icon"
|
|
171
|
+
<span className="toggle-icon">✋</span>
|
|
172
172
|
</Button>
|
|
173
173
|
</TooltipTrigger>
|
|
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 */}
|
|
@@ -49,6 +49,7 @@ export const PANEL_INVENTORY = {
|
|
|
49
49
|
TODO: 'todo',
|
|
50
50
|
BACKGROUND: 'background',
|
|
51
51
|
GIT: 'git',
|
|
52
|
+
HOTSPOTS: 'hotspots',
|
|
52
53
|
SETTINGS: 'settings',
|
|
53
54
|
} as const;
|
|
54
55
|
|
|
@@ -90,6 +91,7 @@ export const RIGHT_SIDEBAR_PANELS = [
|
|
|
90
91
|
PANEL_INVENTORY.TODO,
|
|
91
92
|
PANEL_INVENTORY.BACKGROUND,
|
|
92
93
|
PANEL_INVENTORY.GIT,
|
|
94
|
+
PANEL_INVENTORY.HOTSPOTS,
|
|
93
95
|
PANEL_INVENTORY.SETTINGS,
|
|
94
96
|
] as const;
|
|
95
97
|
|
|
@@ -105,8 +107,9 @@ const PANEL_TITLES: Record<string, string> = {
|
|
|
105
107
|
workflow: 'Workflow',
|
|
106
108
|
ac: 'AC',
|
|
107
109
|
todo: 'Todo',
|
|
108
|
-
background: '
|
|
110
|
+
background: 'Subagents',
|
|
109
111
|
git: 'Git',
|
|
112
|
+
hotspots: 'Hotspots',
|
|
110
113
|
settings: 'Settings',
|
|
111
114
|
};
|
|
112
115
|
|
|
@@ -254,6 +257,7 @@ export function createDefaultDockviewLayout(): SerializedDockview {
|
|
|
254
257
|
views: [PANEL_INVENTORY.MESSAGE],
|
|
255
258
|
activeView: PANEL_INVENTORY.MESSAGE,
|
|
256
259
|
id: 'center',
|
|
260
|
+
hideHeader: true,
|
|
257
261
|
},
|
|
258
262
|
size: 600, // Center takes remaining space
|
|
259
263
|
},
|
|
@@ -430,10 +434,11 @@ export function DockviewWorkspace({
|
|
|
430
434
|
try {
|
|
431
435
|
api.fromJSON(initialLayout);
|
|
432
436
|
|
|
433
|
-
// After restoring, lock the message panel's group
|
|
437
|
+
// After restoring, lock the message panel's group and hide its tab bar
|
|
434
438
|
const messagePanel = api.getPanel(PANEL_INVENTORY.MESSAGE);
|
|
435
439
|
if (messagePanel?.group) {
|
|
436
440
|
messagePanel.group.locked = 'no-drop-target';
|
|
441
|
+
messagePanel.group.model.header.hidden = true;
|
|
437
442
|
}
|
|
438
443
|
|
|
439
444
|
setIsReady(true);
|
|
@@ -500,8 +505,10 @@ export function DockviewWorkspace({
|
|
|
500
505
|
}
|
|
501
506
|
|
|
502
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
|
|
503
509
|
if (messagePanel?.group) {
|
|
504
510
|
messagePanel.group.locked = 'no-drop-target';
|
|
511
|
+
messagePanel.group.model.header.hidden = true;
|
|
505
512
|
}
|
|
506
513
|
|
|
507
514
|
// Set initial sidebar sizes
|
|
@@ -552,9 +559,9 @@ export function DockviewWorkspace({
|
|
|
552
559
|
handleLayoutChange();
|
|
553
560
|
}),
|
|
554
561
|
api.onDidRemovePanel((e) => {
|
|
555
|
-
// Track closed panels
|
|
562
|
+
// Track closed panels for restoration
|
|
556
563
|
const panelId = e?.panel?.id;
|
|
557
|
-
if (panelId
|
|
564
|
+
if (panelId) {
|
|
558
565
|
closedPanels.add(panelId);
|
|
559
566
|
updateClosedPanelsList();
|
|
560
567
|
}
|
|
@@ -619,6 +626,32 @@ export function DockviewWorkspace({
|
|
|
619
626
|
};
|
|
620
627
|
}, []);
|
|
621
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
|
+
|
|
622
655
|
// Component map for Dockview
|
|
623
656
|
const components = {
|
|
624
657
|
PanelAdapter,
|
|
@@ -635,8 +668,9 @@ export function DockviewWorkspace({
|
|
|
635
668
|
workflow: 'Workflow',
|
|
636
669
|
ac: 'AC',
|
|
637
670
|
todo: 'Todo',
|
|
638
|
-
background: '
|
|
671
|
+
background: 'Subagents',
|
|
639
672
|
git: 'Git',
|
|
673
|
+
hotspots: 'Hotspots',
|
|
640
674
|
settings: 'Settings',
|
|
641
675
|
};
|
|
642
676
|
|
|
@@ -58,41 +58,99 @@ interface SystemFont {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// =============================================================================
|
|
61
|
-
// Monospace Detection
|
|
61
|
+
// Monospace Detection via OpenType `post` table
|
|
62
62
|
// =============================================================================
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Parse SFNT table directory to find a table's offset and length.
|
|
66
|
+
* SFNT header: version(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2) = 12
|
|
67
|
+
* Each table record: tag(4) + checksum(4) + offset(4) + length(4) = 16
|
|
68
|
+
*/
|
|
69
|
+
function findSfntTable(view: DataView, tag: string): { offset: number; length: number } | null {
|
|
70
|
+
const numTables = view.getUint16(4);
|
|
71
|
+
for (let i = 0; i < numTables; i++) {
|
|
72
|
+
const recordOffset = 12 + i * 16;
|
|
73
|
+
const tableTag = String.fromCharCode(
|
|
74
|
+
view.getUint8(recordOffset),
|
|
75
|
+
view.getUint8(recordOffset + 1),
|
|
76
|
+
view.getUint8(recordOffset + 2),
|
|
77
|
+
view.getUint8(recordOffset + 3),
|
|
78
|
+
);
|
|
79
|
+
if (tableTag === tag) {
|
|
80
|
+
return {
|
|
81
|
+
offset: view.getUint32(recordOffset + 8),
|
|
82
|
+
length: view.getUint32(recordOffset + 12),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
69
85
|
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Read `isFixedPitch` from the `post` table.
|
|
91
|
+
* post layout: version(4) + italicAngle(4) + underlinePosition(2) + underlineThickness(2) = offset 12
|
|
92
|
+
* isFixedPitch is uint32 at offset 12: 0 = proportional, non-zero = monospace.
|
|
93
|
+
*/
|
|
94
|
+
async function detectMonospaceFromBlob(fontData: { blob: () => Promise<Blob> }): Promise<boolean> {
|
|
95
|
+
try {
|
|
96
|
+
const blob = await fontData.blob();
|
|
97
|
+
const buffer = await blob.arrayBuffer();
|
|
98
|
+
const view = new DataView(buffer);
|
|
99
|
+
|
|
100
|
+
const post = findSfntTable(view, 'post');
|
|
101
|
+
if (post) {
|
|
102
|
+
const isFixedPitch = view.getUint32(post.offset + 12);
|
|
103
|
+
return isFixedPitch !== 0;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
} catch {
|
|
75
107
|
return false;
|
|
76
108
|
}
|
|
77
|
-
|
|
78
|
-
ctx.font = `16px "${fontFamily}", monospace`;
|
|
79
|
-
const wideChar = ctx.measureText('W').width;
|
|
80
|
-
const narrowChar = ctx.measureText('i').width;
|
|
81
|
-
const isMono = Math.abs(wideChar - narrowChar) < 1;
|
|
82
|
-
|
|
83
|
-
monoCache.set(fontFamily, isMono);
|
|
84
|
-
return isMono;
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
// =============================================================================
|
|
88
112
|
// System Font Discovery
|
|
89
113
|
// =============================================================================
|
|
90
114
|
|
|
91
|
-
|
|
115
|
+
interface FontDataEntry {
|
|
116
|
+
family: string;
|
|
117
|
+
fullName: string;
|
|
118
|
+
postscriptName: string;
|
|
119
|
+
style: string;
|
|
120
|
+
blob: () => Promise<Blob>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const FONT_CACHE_KEY = 'cyclist-system-fonts';
|
|
124
|
+
|
|
125
|
+
interface FontCacheData {
|
|
126
|
+
fonts: SystemFont[];
|
|
127
|
+
count: number; // number of font families — if it changes, fonts were installed/removed
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function loadCachedFonts(): SystemFont[] | null {
|
|
131
|
+
try {
|
|
132
|
+
const raw = localStorage.getItem(FONT_CACHE_KEY);
|
|
133
|
+
if (!raw) return null;
|
|
134
|
+
const data: FontCacheData = JSON.parse(raw);
|
|
135
|
+
if (data.fonts?.length > 0) return data.fonts;
|
|
136
|
+
return null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function saveCachedFonts(fonts: SystemFont[], count: number): void {
|
|
143
|
+
try {
|
|
144
|
+
const data: FontCacheData = { fonts, count };
|
|
145
|
+
localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(data));
|
|
146
|
+
} catch {
|
|
147
|
+
// localStorage full or unavailable — not critical
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
92
151
|
let systemFontsPromise: Promise<SystemFont[]> | null = null;
|
|
93
152
|
|
|
94
153
|
async function getSystemFonts(): Promise<SystemFont[]> {
|
|
95
|
-
if (systemFontsCache) return systemFontsCache;
|
|
96
154
|
if (systemFontsPromise) return systemFontsPromise;
|
|
97
155
|
|
|
98
156
|
systemFontsPromise = (async () => {
|
|
@@ -101,24 +159,48 @@ async function getSystemFonts(): Promise<SystemFont[]> {
|
|
|
101
159
|
}
|
|
102
160
|
|
|
103
161
|
try {
|
|
104
|
-
const fonts = await (window as unknown as { queryLocalFonts: () => Promise<
|
|
162
|
+
const fonts: FontDataEntry[] = await (window as unknown as { queryLocalFonts: () => Promise<FontDataEntry[]> }).queryLocalFonts();
|
|
105
163
|
|
|
106
|
-
// Deduplicate by family name
|
|
107
|
-
const
|
|
164
|
+
// Deduplicate by family name, keep one FontData per family for mono detection
|
|
165
|
+
const familyMap = new Map<string, FontDataEntry>();
|
|
108
166
|
for (const font of fonts) {
|
|
109
|
-
|
|
167
|
+
if (!familyMap.has(font.family)) {
|
|
168
|
+
familyMap.set(font.family, font);
|
|
169
|
+
}
|
|
110
170
|
}
|
|
111
171
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
const familyCount = familyMap.size;
|
|
173
|
+
|
|
174
|
+
// Check localStorage cache — reuse if font count hasn't changed
|
|
175
|
+
const cached = loadCachedFonts();
|
|
176
|
+
if (cached && cached.length > 0) {
|
|
177
|
+
// Load raw cached data to check count
|
|
178
|
+
try {
|
|
179
|
+
const raw = localStorage.getItem(FONT_CACHE_KEY);
|
|
180
|
+
if (raw) {
|
|
181
|
+
const data: FontCacheData = JSON.parse(raw);
|
|
182
|
+
if (data.count === familyCount) {
|
|
183
|
+
return cached;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
// Fall through to re-detect
|
|
188
|
+
}
|
|
118
189
|
}
|
|
119
190
|
|
|
191
|
+
// Detect monospace via post table in parallel
|
|
192
|
+
const entries = Array.from(familyMap.entries());
|
|
193
|
+
const monoResults = await Promise.all(
|
|
194
|
+
entries.map(([, fontData]) => detectMonospaceFromBlob(fontData))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const result: SystemFont[] = entries.map(([family], i) => ({
|
|
198
|
+
family,
|
|
199
|
+
isMonospace: monoResults[i],
|
|
200
|
+
}));
|
|
201
|
+
|
|
120
202
|
result.sort((a, b) => a.family.localeCompare(b.family));
|
|
121
|
-
|
|
203
|
+
saveCachedFonts(result, familyCount);
|
|
122
204
|
return result;
|
|
123
205
|
} catch {
|
|
124
206
|
// Permission denied or API error
|
|
@@ -161,10 +243,13 @@ export function FontPicker({
|
|
|
161
243
|
}
|
|
162
244
|
}, [fontsLoaded]);
|
|
163
245
|
|
|
164
|
-
// Filter system fonts
|
|
246
|
+
// Filter system fonts: English-only (Latin names), monospace-only for code
|
|
165
247
|
const filteredSystemFonts = useMemo(() => {
|
|
166
248
|
let fonts = systemFonts;
|
|
167
249
|
|
|
250
|
+
// Filter to fonts with Latin-script names (excludes CJK, Arabic, Devanagari, etc.)
|
|
251
|
+
fonts = fonts.filter(f => /^[\x20-\x7E\u00C0-\u024F]+$/.test(f.family));
|
|
252
|
+
|
|
168
253
|
// For code fonts, only show monospace
|
|
169
254
|
if (type === 'code') {
|
|
170
255
|
fonts = fonts.filter(f => f.isMonospace);
|
|
@@ -237,7 +322,7 @@ export function FontPicker({
|
|
|
237
322
|
>
|
|
238
323
|
<SelectValue placeholder="Select font..." />
|
|
239
324
|
</SelectTrigger>
|
|
240
|
-
<SelectContent>
|
|
325
|
+
<SelectContent className="max-h-[300px]">
|
|
241
326
|
{/* Presets section */}
|
|
242
327
|
{displayPresets.length > 0 && (
|
|
243
328
|
<SelectGroup>
|