@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.
Files changed (101) hide show
  1. package/dist/api/hook-request.d.ts +11 -0
  2. package/dist/api/hook-request.d.ts.map +1 -1
  3. package/dist/api/hook-request.js +126 -28
  4. package/dist/api/hook-request.js.map +1 -1
  5. package/dist/api/hotspots.d.ts +3 -0
  6. package/dist/api/hotspots.d.ts.map +1 -0
  7. package/dist/api/hotspots.js +54 -0
  8. package/dist/api/hotspots.js.map +1 -0
  9. package/dist/api/index.d.ts +2 -0
  10. package/dist/api/index.d.ts.map +1 -1
  11. package/dist/api/index.js +3 -0
  12. package/dist/api/index.js.map +1 -1
  13. package/dist/api/permissions.d.ts +16 -0
  14. package/dist/api/permissions.d.ts.map +1 -0
  15. package/dist/api/permissions.js +67 -0
  16. package/dist/api/permissions.js.map +1 -0
  17. package/dist/api/settings.d.ts +1 -1
  18. package/dist/api/settings.d.ts.map +1 -1
  19. package/dist/api/settings.js +44 -17
  20. package/dist/api/settings.js.map +1 -1
  21. package/dist/api/theme-agents.d.ts +4 -0
  22. package/dist/api/theme-agents.d.ts.map +1 -1
  23. package/dist/api/theme-agents.js +3 -0
  24. package/dist/api/theme-agents.js.map +1 -1
  25. package/dist/approval-gate.d.ts +3 -75
  26. package/dist/approval-gate.d.ts.map +1 -1
  27. package/dist/approval-gate.js +4 -121
  28. package/dist/approval-gate.js.map +1 -1
  29. package/dist/hooks/cyclist-pretooluse-hook.d.ts +60 -0
  30. package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +1 -0
  31. package/dist/hooks/cyclist-pretooluse-hook.js +57 -0
  32. package/dist/hooks/cyclist-pretooluse-hook.js.map +1 -0
  33. package/dist/hooks/pretooluse-hook.d.ts +89 -0
  34. package/dist/hooks/pretooluse-hook.d.ts.map +1 -0
  35. package/dist/hooks/pretooluse-hook.js +235 -0
  36. package/dist/hooks/pretooluse-hook.js.map +1 -0
  37. package/dist/main.d.ts +1 -134
  38. package/dist/main.d.ts.map +1 -1
  39. package/dist/main.js +42 -373
  40. package/dist/main.js.map +1 -1
  41. package/dist/menu-builder.d.ts +7 -1
  42. package/dist/menu-builder.d.ts.map +1 -1
  43. package/dist/menu-builder.js +36 -1
  44. package/dist/menu-builder.js.map +1 -1
  45. package/dist/otlp-receiver.d.ts.map +1 -1
  46. package/dist/otlp-receiver.js +6 -0
  47. package/dist/otlp-receiver.js.map +1 -1
  48. package/dist/public/css/react.css +1 -1
  49. package/dist/public/js/react/react.js +42 -42
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +16 -3
  52. package/dist/server.js.map +1 -1
  53. package/dist/settings-store.d.ts +3 -1
  54. package/dist/settings-store.d.ts.map +1 -1
  55. package/dist/settings-store.js +18 -9
  56. package/dist/settings-store.js.map +1 -1
  57. package/dist/story-parser.d.ts +17 -0
  58. package/dist/story-parser.d.ts.map +1 -1
  59. package/dist/story-parser.js +183 -13
  60. package/dist/story-parser.js.map +1 -1
  61. package/dist/websocket.d.ts +1 -0
  62. package/dist/websocket.d.ts.map +1 -1
  63. package/dist/websocket.js +48 -5
  64. package/dist/websocket.js.map +1 -1
  65. package/dist/workflow-presets.d.ts +72 -0
  66. package/dist/workflow-presets.d.ts.map +1 -0
  67. package/dist/workflow-presets.js +93 -0
  68. package/dist/workflow-presets.js.map +1 -0
  69. package/package.json +2 -2
  70. package/src/public/App.tsx +61 -1
  71. package/src/public/components/ApprovalModal/index.tsx +31 -1
  72. package/src/public/components/ControlBar.tsx +19 -20
  73. package/src/public/components/DockviewWorkspace.tsx +39 -5
  74. package/src/public/components/FontPicker/index.tsx +118 -33
  75. package/src/public/components/FullFileTree.tsx +223 -0
  76. package/src/public/components/Message.tsx +89 -11
  77. package/src/public/components/MessageView.tsx +206 -93
  78. package/src/public/components/PersonaHeader.tsx +47 -15
  79. package/src/public/components/SubagentSpan.tsx +15 -8
  80. package/src/public/components/panels/BackgroundPanel.tsx +1 -1
  81. package/src/public/components/panels/ChangedPanel.tsx +30 -44
  82. package/src/public/components/panels/HotspotsPanel.tsx +365 -0
  83. package/src/public/components/panels/MessagePanel.tsx +79 -5
  84. package/src/public/components/panels/SettingsPanel.tsx +3 -28
  85. package/src/public/components/panels/WorkflowPanel.tsx +108 -13
  86. package/src/public/components/panels/index.ts +1 -0
  87. package/src/public/contexts/ClaudeContext.tsx +16 -1
  88. package/src/public/css/theme-system.css +46 -38
  89. package/src/public/hooks/useColorScheme.ts +27 -0
  90. package/src/public/hooks/useFileBrowser.ts +71 -0
  91. package/src/public/hooks/useHotspots.ts +113 -0
  92. package/src/public/hooks/usePlanModeExit.ts +105 -0
  93. package/src/public/hooks/useStory.ts +12 -3
  94. package/src/public/images/cyclist-dark.png +0 -0
  95. package/src/public/images/cyclist-light.png +0 -0
  96. package/src/public/styles/dockview-theme.css +31 -33
  97. package/src/public/styles/tailwind.css +417 -58
  98. package/src/public/types/message.ts +6 -1
  99. package/src/public/utils/markdown.ts +2 -2
  100. package/src/public/utils/slash-commands.ts +1 -1
  101. package/src/public/utils/toolStackGrouper.ts +5 -6
package/dist/main.js CHANGED
@@ -8,13 +8,12 @@
8
8
  * This module exports testable functions and constants for unit testing,
9
9
  * while the Electron-specific runtime code only executes in Electron context.
10
10
  */
11
- import { createServer as createHttpServer } from 'http';
12
11
  import { fileURLToPath } from 'url';
13
12
  import { dirname, join, basename } from 'path';
14
13
  import { getCurrentPersona, detectPennyfarthingProject, watchAgentChanges } from './pennyfarthing.js';
15
- import { getStoryInfo, getAllReposGitInfoAsync, writePortFile, cleanupPortFile, writePidFile, cleanupPidFile, readPidFile, isProcessRunning, getOtelConfig, writeApprovalPortFile, cleanupApprovalPortFile } from './server.js';
14
+ import { getStoryInfo, getAllReposGitInfoAsync, writePortFile, cleanupPortFile, writePidFile, cleanupPidFile, readPidFile, isProcessRunning, getOtelConfig } from './server.js';
16
15
  import { parseToolStats, createEmptyStats } from './tool-stats.js';
17
- import { getTokenStats, setTokenStatsCallback, setToolEventCallback, resetTokenStats, resetEventStore, getToolEventsFiltered, getToolTypes, exportAuditLogAsJSON, exportAuditLogAsCSV, getAuditLogStats, getUserEmail, setUserEmailCallback, setBackgroundTaskCallback, setBackgroundTaskStartCallback, trackBackgroundTask, getBackgroundTaskByToolId, getBackgroundTasks, } from './otlp-receiver.js';
16
+ import { getTokenStats, setTokenStatsCallback, setToolEventCallback, resetTokenStats, resetEventStore, getToolEventsFiltered, getToolTypes, exportAuditLogAsJSON, exportAuditLogAsCSV, getAuditLogStats, getUserEmail, setUserEmailCallback, setBackgroundTaskCallback, setBackgroundTaskStartCallback, trackBackgroundTask, completeBackgroundTask, getBackgroundTaskByToolId, getBackgroundTasks, } from './otlp-receiver.js';
18
17
  import { ClaudeService } from './claude-service.js';
19
18
  import { selectContextTier, getPrimeContextJson } from './prime.js';
20
19
  import { isTodoWriteMessage, extractTodos } from './todos.js';
@@ -27,10 +26,8 @@ import { getVerboseMode, setVerboseMode } from './settings-store.js';
27
26
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
28
27
  import { getCurrentSettings, saveUserSettings, initializeSettings, loadGrants, saveGrants, } from './settings.js';
29
28
  import { broadcastBackgroundTaskEvent } from './api/background-tasks.js';
30
- import { setStoryUpdateCallback, setGitUpdateCallback, broadcastClaudeMessage, setClaudeSendCallback, setClaudeAbortCallback, setClaudeClearCallback, setClaudeSetModeCallback, setClaudeGetModeCallback, setClaudeClearAndReloadCallback, broadcastTodosUpdate, broadcastContextUpdate } from './websocket.js';
31
- import { initializeGrants, setGrantsPersistCallback } from './settings-store.js';
32
- // Story 33-7: Import approval gate functions for tool execution pipeline
33
- import { interceptToolUse, requestApproval, createRejectionError, } from './approval-gate.js';
29
+ import { setStoryUpdateCallback, setGitUpdateCallback, broadcastClaudeMessage, setClaudeSendCallback, setClaudeAbortCallback, setClaudeClearCallback, setClaudeSetModeCallback, setClaudeGetModeCallback, setClaudeClearAndReloadCallback, broadcastTodosUpdate, broadcastContextUpdate, broadcastPanelToggle } from './websocket.js';
30
+ import { initializeGrants, setGrantsPersistCallback, clearSessionGrants } from './settings-store.js';
34
31
  import { openSettingsWindow, setMainWindowRef, setBrowserWindowRef } from './settings-window.js';
35
32
  import { setBellMode } from './bell-mode.js';
36
33
  import { IPC_DATA_CHANNELS, IPC_CLAUDE_CHANNELS, IPC_AGENT_CHANNELS, IPC_SETTINGS_CHANNELS, IPC_AUDIT_LOG_CHANNELS, IPC_FILE_BROWSER_CHANNELS, IPC_COMMAND_CHANNELS, IPC_BACKGROUND_TASK_CHANNELS, IPC_SKILL_CHANNELS, IPC_LAYOUT_CHANNELS, IPC_AVATAR_CHANNELS, } from './ipc-channels.js';
@@ -65,8 +62,9 @@ catch {
65
62
  // Re-export IPC channels from dedicated module
66
63
  export { IPC_DATA_CHANNELS, IPC_CLAUDE_CHANNELS, IPC_AGENT_CHANNELS, IPC_DIFF_CHANNELS, IPC_SETTINGS_CHANNELS, IPC_AUDIT_LOG_CHANNELS, IPC_FILE_BROWSER_CHANNELS, IPC_COMMAND_CHANNELS, IPC_BACKGROUND_TASK_CHANNELS, IPC_SKILL_CHANNELS, IPC_CONTEXT_CLEAR_CHANNELS, IPC_LAYOUT_CHANNELS, IPC_AVATAR_CHANNELS, } from './ipc-channels.js';
67
64
  // Re-export menu builders from dedicated module
68
- export { AGENT_DEFINITIONS, WORKFLOW_DEFINITIONS, buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, getMenuTemplate, } from './menu-builder.js';
69
- import { buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, } from './menu-builder.js';
65
+ export { AGENT_DEFINITIONS, WORKFLOW_DEFINITIONS, buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, getMenuTemplate, setPanelToggleBroadcast, } from './menu-builder.js';
66
+ // Local imports for menu building
67
+ import { buildAgentMenu, buildWorkflowMenu, buildToolsMenu, buildViewMenu, setPanelToggleBroadcast, } from './menu-builder.js';
70
68
  /**
71
69
  * Get list of registered data IPC channels (for testing)
72
70
  * Returns the data channels that setupDataIPCHandlers will register
@@ -794,6 +792,31 @@ export function startProjectWatchers() {
794
792
  // MSSCI-14190: Process tool_use messages for diff tracking (same as IPC handler)
795
793
  // This was missing from the WebSocket callback path!
796
794
  processToolUseFromMessage(message);
795
+ // Complete subagent tasks when tool_result arrives
796
+ // CLI format: discrete tool_result message
797
+ if (message.type === 'tool_result') {
798
+ const msg = message;
799
+ if (msg.tool_id) {
800
+ const task = getBackgroundTaskByToolId(msg.tool_id);
801
+ if (task) {
802
+ completeBackgroundTask(msg.tool_id, !msg.is_error, msg.is_error ? undefined : msg.output?.slice(0, 500), msg.is_error ? (msg.output?.slice(0, 500) || 'Task failed') : undefined);
803
+ }
804
+ }
805
+ }
806
+ // SDK format: tool_result nested in user message content
807
+ if (message.type === 'user') {
808
+ const userMsg = message;
809
+ if (userMsg.message?.content) {
810
+ for (const block of userMsg.message.content) {
811
+ if (block.type === 'tool_result' && block.tool_use_id) {
812
+ const task = getBackgroundTaskByToolId(block.tool_use_id);
813
+ if (task) {
814
+ completeBackgroundTask(block.tool_use_id, !block.is_error, block.is_error ? undefined : (typeof block.content === 'string' ? block.content.slice(0, 500) : undefined), block.is_error ? (typeof block.content === 'string' ? block.content.slice(0, 500) : 'Task failed') : undefined);
815
+ }
816
+ }
817
+ }
818
+ }
819
+ }
797
820
  }
798
821
  onComplete();
799
822
  broadcastToRenderer(IPC_CLAUDE_CHANNELS.CLAUDE_COMPLETE, null);
@@ -951,16 +974,18 @@ function processToolUseFromMessage(message) {
951
974
  // Store for OTEL correlation
952
975
  if (toolId && toolInput) {
953
976
  storePendingToolInput(toolId, toolName, toolInput);
954
- // MSSCI-14210: Track background Task tools
955
- if (toolName === 'Task' && toolInput.run_in_background === true) {
956
- const description = toolInput.description || toolInput.prompt?.substring(0, 50) || 'Background task';
977
+ // Track all Task tool subagents (background and foreground)
978
+ // All must be tracked so enrichMessageWithSubagentContext can look them up
979
+ if (toolName === 'Task') {
980
+ const description = toolInput.description || toolInput.prompt?.substring(0, 50) || 'Subagent task';
957
981
  const subagentType = toolInput.subagent_type || 'general-purpose';
982
+ const isBackground = toolInput.run_in_background === true;
958
983
  trackBackgroundTask({
959
984
  taskId: toolId,
960
985
  description,
961
986
  subagentType,
962
987
  startedAt: Date.now(),
963
- isBackground: true,
988
+ isBackground,
964
989
  });
965
990
  }
966
991
  }
@@ -1506,363 +1531,6 @@ export function setupCommandIPCHandlers(ipcMain) {
1506
1531
  registeredCommandChannels = [IPC_COMMAND_CHANNELS.EXECUTE];
1507
1532
  console.log('Command IPC handlers registered');
1508
1533
  }
1509
- // Dependency injection for testing
1510
- let ipcSender = null;
1511
- let toolExecutor = null;
1512
- let errorInjector = null;
1513
- /**
1514
- * Set the IPC sender function (for testing)
1515
- */
1516
- export function setIPCSender(sender) {
1517
- ipcSender = sender;
1518
- }
1519
- /**
1520
- * Set the tool executor function (for testing)
1521
- */
1522
- export function setToolExecutor(executor) {
1523
- toolExecutor = executor;
1524
- }
1525
- /**
1526
- * Set the error injector function (for testing)
1527
- */
1528
- export function setErrorInjector(injector) {
1529
- errorInjector = injector;
1530
- }
1531
- /**
1532
- * Send an approval request to the renderer via IPC
1533
- */
1534
- export function sendApprovalRequest(toolId, toolName, context) {
1535
- const sender = ipcSender || broadcastToRenderer;
1536
- sender('permission-request', {
1537
- toolId,
1538
- toolName,
1539
- context,
1540
- });
1541
- }
1542
- /**
1543
- * Handle permission response from renderer
1544
- * Called by IPC handler when user responds to approval modal
1545
- */
1546
- export function handlePermissionResponse(response) {
1547
- // Story 33-7: First try to resolve hook approval (from PreToolUse hook)
1548
- // This is the path that actually controls tool execution
1549
- resolveHookApproval(response.toolId, response.approved, response.grantScope);
1550
- // Also resolve approval-gate.js pending approvals for backwards compatibility
1551
- // (This was the old observer-only path)
1552
- import('./approval-gate.js').then(({ resolveApproval }) => {
1553
- resolveApproval(response.toolId, response.approved, response.grantScope);
1554
- });
1555
- }
1556
- /**
1557
- * Process a tool_use message with approval gate check
1558
- * This is the main integration point for story 33-7
1559
- *
1560
- * @param message - The tool_use message to process
1561
- * @returns ApprovalResult indicating whether approval is needed and outcome
1562
- */
1563
- export async function processToolUseWithApproval(message) {
1564
- // Check if this tool_use needs approval
1565
- const interceptResult = interceptToolUse(message);
1566
- // If gate is disabled or grant exists, pass through immediately
1567
- if (!interceptResult.shouldApprove) {
1568
- // Execute tool if executor is set
1569
- if (toolExecutor) {
1570
- toolExecutor(message);
1571
- }
1572
- return {
1573
- needsApproval: false,
1574
- passThrough: true,
1575
- };
1576
- }
1577
- // Need approval - send IPC request and wait for response
1578
- sendApprovalRequest(interceptResult.toolId, interceptResult.toolName, interceptResult.context);
1579
- // Get the command for Bash tools, or use context for other tools
1580
- const command = interceptResult.toolName === 'Bash'
1581
- ? interceptResult.context.command || ''
1582
- : JSON.stringify(interceptResult.context);
1583
- // Wait for user response
1584
- const approved = await requestApproval(command, interceptResult.toolId);
1585
- if (approved) {
1586
- // User approved - execute tool
1587
- if (toolExecutor) {
1588
- toolExecutor(message);
1589
- }
1590
- return {
1591
- needsApproval: true,
1592
- passThrough: true,
1593
- approved: true,
1594
- };
1595
- }
1596
- else {
1597
- // User rejected - create and inject error
1598
- const errorMessage = createRejectionError(interceptResult.toolId);
1599
- if (errorInjector) {
1600
- errorInjector(errorMessage);
1601
- }
1602
- return {
1603
- needsApproval: true,
1604
- passThrough: false,
1605
- approved: false,
1606
- rejected: true,
1607
- errorMessage,
1608
- };
1609
- }
1610
- }
1611
- /**
1612
- * Set up IPC handlers for approval gate
1613
- * Story 33-7: Handles permission request/response flow
1614
- */
1615
- export function setupApprovalIPCHandlers(ipcMain) {
1616
- // Handle permission response from renderer
1617
- if (ipcMain.on) {
1618
- ipcMain.on('permission-response', (_event, response) => {
1619
- handlePermissionResponse(response);
1620
- });
1621
- }
1622
- if (ipcMain.handle) {
1623
- ipcMain.handle('permission-response', async (_event, response) => {
1624
- handlePermissionResponse(response);
1625
- return { success: true };
1626
- });
1627
- }
1628
- console.log('Approval gate IPC handlers registered');
1629
- }
1630
- // =============================================================================
1631
- // Story 33-7: Approval Hook Server
1632
- // =============================================================================
1633
- // HTTP server that receives approval requests from the PreToolUse hook script.
1634
- // The hook runs in Claude Code's process, sends requests here, we show modal,
1635
- // user decides, we respond, hook tells Claude Code to allow/deny.
1636
- //
1637
- // Multi-instance support: Uses dynamic port selection with .cyclist-approval-port
1638
- // discovery file to prevent cross-instance interference when multiple Cyclist
1639
- // windows are open for different projects.
1640
- let approvalServer = null;
1641
- let approvalServerPort = null;
1642
- // Pending approval requests from hooks, keyed by toolId
1643
- // MSSCI-11947: Extended to support data field for interactive tools
1644
- const pendingHookApprovals = new Map();
1645
- /**
1646
- * Handle incoming approval request from hook script
1647
- */
1648
- async function handleHookApprovalRequest(toolName, toolId, input) {
1649
- // Check if gate is enabled
1650
- const { getBashApprovalGate, checkGrant, isAllowlisted } = await import('./settings-store.js');
1651
- if (!getBashApprovalGate()) {
1652
- return { decision: 'allow', reason: 'Approval gate disabled' };
1653
- }
1654
- // Check allowlist and grants
1655
- if (toolName === 'Bash') {
1656
- const command = input.command || '';
1657
- if (isAllowlisted(command) || checkGrant('Bash', command)) {
1658
- return { decision: 'allow', reason: 'Matched allowlist or existing grant' };
1659
- }
1660
- }
1661
- // Need user approval - send to renderer and wait
1662
- return new Promise((resolve) => {
1663
- pendingHookApprovals.set(toolId, { resolve, toolName, input });
1664
- // Send approval request to renderer
1665
- broadcastToRenderer('permission-request', {
1666
- toolId,
1667
- toolName,
1668
- context: input,
1669
- source: 'hook', // Indicate this came from hook, not observation
1670
- });
1671
- });
1672
- }
1673
- /**
1674
- * Resolve a pending hook approval (called when user responds to modal)
1675
- */
1676
- export function resolveHookApproval(toolId, approved, grantScope) {
1677
- const pending = pendingHookApprovals.get(toolId);
1678
- if (pending) {
1679
- // Add grant if approved with scope
1680
- if (approved && grantScope && pending.toolName === 'Bash') {
1681
- import('./settings-store.js').then(({ addGrant, extractPattern }) => {
1682
- const command = pending.input.command || '';
1683
- const pattern = extractPattern(command);
1684
- addGrant({
1685
- tool: 'Bash',
1686
- scope: pattern,
1687
- grant_type: grantScope,
1688
- granted_at: new Date().toISOString(),
1689
- });
1690
- });
1691
- }
1692
- pending.resolve({
1693
- decision: approved ? 'allow' : 'deny',
1694
- reason: approved ? `Approved by user (${grantScope || 'once'})` : 'Rejected by user',
1695
- });
1696
- pendingHookApprovals.delete(toolId);
1697
- }
1698
- }
1699
- // =============================================================================
1700
- // MSSCI-11947: Hook Response Data Channel
1701
- // =============================================================================
1702
- // Functions for handling interactive tools (AskUserQuestion, ExitPlanMode)
1703
- // that need to return structured data back to Claude, not just allow/deny.
1704
- /**
1705
- * Check if a tool_use is an interactive tool that needs data return
1706
- * MSSCI-11947: AC1 - Detect AskUserQuestion and ExitPlanMode
1707
- */
1708
- export function isInteractiveToolUse(toolUse) {
1709
- const interactiveTools = ['AskUserQuestion', 'ExitPlanMode'];
1710
- return toolUse.type === 'tool_use' && interactiveTools.includes(toolUse.tool_name || '');
1711
- }
1712
- /**
1713
- * Process an interactive tool_use and wait for user response with data
1714
- * MSSCI-11947: AC1 - Handle interactive tool approval flow
1715
- */
1716
- export function processInteractiveToolUse(toolUse) {
1717
- const toolId = toolUse.tool_id || `interactive-${Date.now()}`;
1718
- const toolName = toolUse.tool_name || 'Unknown';
1719
- const input = toolUse.input || {};
1720
- return new Promise((resolve) => {
1721
- pendingHookApprovals.set(toolId, { resolve, toolName, input });
1722
- // Send approval request to renderer with full tool input
1723
- broadcastToRenderer('permission-request', {
1724
- toolId,
1725
- toolName,
1726
- context: input,
1727
- source: 'hook',
1728
- });
1729
- });
1730
- }
1731
- /**
1732
- * Resolve a pending hook approval with data (for interactive tools)
1733
- * MSSCI-11947: AC1 - Extended resolve that includes data field
1734
- */
1735
- export function resolveHookApprovalWithData(toolId, approved, grantScope, data) {
1736
- const pending = pendingHookApprovals.get(toolId);
1737
- if (pending) {
1738
- // For interactive tools, we don't create grants (they're one-time responses)
1739
- pending.resolve({
1740
- decision: approved ? 'allow' : 'deny',
1741
- reason: approved ? `Approved by user (${grantScope || 'once'})` : 'Rejected by user',
1742
- data: data || {},
1743
- });
1744
- pendingHookApprovals.delete(toolId);
1745
- }
1746
- }
1747
- /**
1748
- * Format hook response with optional data field
1749
- * MSSCI-11947: AC4 - Format response for hook output
1750
- */
1751
- export function formatHookResponseWithData(decision, reason, data) {
1752
- const response = {
1753
- decision,
1754
- reason,
1755
- };
1756
- if (data !== undefined) {
1757
- response.data = data;
1758
- }
1759
- return response;
1760
- }
1761
- /**
1762
- * Format answers as updatedInput for AskUserQuestion
1763
- * MSSCI-11947: AC4 - Format for hook updatedInput
1764
- */
1765
- export function formatUpdatedInputForAskUserQuestion(answers) {
1766
- return { answers };
1767
- }
1768
- /**
1769
- * Format plan response as updatedInput for ExitPlanMode
1770
- * MSSCI-11947: AC4 - Format for hook updatedInput
1771
- */
1772
- export function formatUpdatedInputForExitPlanMode(response) {
1773
- return response;
1774
- }
1775
- /**
1776
- * Serialize approval data for transmission
1777
- * MSSCI-11947: AC1 - JSON serialization helper
1778
- */
1779
- export function serializeApprovalData(data) {
1780
- return JSON.stringify(data);
1781
- }
1782
- /**
1783
- * Deserialize approval data from transmission
1784
- * MSSCI-11947: AC1 - JSON deserialization helper
1785
- */
1786
- export function deserializeApprovalData(data) {
1787
- return JSON.parse(data);
1788
- }
1789
- /**
1790
- * Start the approval hook server with dynamic port selection
1791
- * Uses port 0 to let OS assign an available port (avoids race conditions)
1792
- * Writes port to .cyclist-approval-port for hook discovery
1793
- */
1794
- export async function startApprovalServer() {
1795
- if (approvalServer) {
1796
- console.log('Approval server already running');
1797
- return;
1798
- }
1799
- const projectDir = getProjectDirectory();
1800
- if (!projectDir) {
1801
- console.warn('No project directory set, cannot start approval server');
1802
- return;
1803
- }
1804
- approvalServer = createHttpServer(async (req, res) => {
1805
- if (req.method === 'POST' && req.url === '/approval-request') {
1806
- let body = '';
1807
- req.on('data', (chunk) => { body += chunk; });
1808
- req.on('end', async () => {
1809
- try {
1810
- const data = JSON.parse(body);
1811
- const { toolName, toolId, input } = data;
1812
- const response = await handleHookApprovalRequest(toolName, toolId, input);
1813
- res.writeHead(200, { 'Content-Type': 'application/json' });
1814
- res.end(JSON.stringify(response));
1815
- }
1816
- catch {
1817
- res.writeHead(400, { 'Content-Type': 'application/json' });
1818
- res.end(JSON.stringify({ error: 'Invalid request' }));
1819
- }
1820
- });
1821
- }
1822
- else {
1823
- res.writeHead(404);
1824
- res.end('Not found');
1825
- }
1826
- });
1827
- // Use port 0 to let OS assign an available port (avoids race conditions)
1828
- approvalServer.listen(0, '127.0.0.1', () => {
1829
- const addr = approvalServer.address();
1830
- approvalServerPort = typeof addr === 'object' && addr ? addr.port : null;
1831
- if (approvalServerPort) {
1832
- console.log(`Approval hook server running on http://127.0.0.1:${approvalServerPort}`);
1833
- // Write port file for hook discovery
1834
- writeApprovalPortFile(projectDir, approvalServerPort);
1835
- console.log(`[33-7] Wrote .cyclist-approval-port file to ${projectDir}`);
1836
- }
1837
- });
1838
- approvalServer.on('error', (err) => {
1839
- console.error('Approval server error:', err);
1840
- approvalServerPort = null;
1841
- });
1842
- }
1843
- /**
1844
- * Stop the approval hook server and clean up port file
1845
- */
1846
- export function stopApprovalServer() {
1847
- if (approvalServer) {
1848
- approvalServer.close();
1849
- approvalServer = null;
1850
- approvalServerPort = null;
1851
- // Clean up port file
1852
- const projectDir = getProjectDirectory();
1853
- if (projectDir) {
1854
- cleanupApprovalPortFile(projectDir);
1855
- console.log('[33-7] Cleaned up .cyclist-approval-port file');
1856
- }
1857
- console.log('Approval hook server stopped');
1858
- }
1859
- }
1860
- /**
1861
- * Get the current approval server port (for testing)
1862
- */
1863
- export function getApprovalServerPort() {
1864
- return approvalServerPort;
1865
- }
1866
1534
  // =============================================================================
1867
1535
  // Session Persistence (E7-3: AC4)
1868
1536
  // =============================================================================
@@ -2074,8 +1742,6 @@ if (isElectron) {
2074
1742
  setupAuditLogIPCHandlers(ipcMain);
2075
1743
  setupCommandIPCHandlers(ipcMain); // 23-3: Command execution
2076
1744
  setupSkillIPCHandlers(ipcMain); // 35-12: Skill invocation tracking
2077
- setupApprovalIPCHandlers(ipcMain); // 33-7: Approval gate wiring
2078
- // NOTE: startApprovalServer() moved to app.whenReady() - needs project directory
2079
1745
  /**
2080
1746
  * Kill orphaned Claude CLI process from previous Cyclist session in THIS project.
2081
1747
  * B-24 fix: Only kills the specific PID from .cyclist-pid, not all Claude processes.
@@ -2225,12 +1891,13 @@ if (isElectron) {
2225
1891
  // In Electron mode, Claude messages are broadcast from main.ts, not per-connection ClaudeService
2226
1892
  process.env.CYCLIST_ELECTRON_MODE = '1';
2227
1893
  await startServer();
2228
- await startApprovalServer(); // 33-7: Start after project dir set
2229
1894
  createWindow();
2230
1895
  if (mainWindow && projectDir) {
2231
1896
  mainWindow.setTitle(`Cyclist - ${basename(projectDir)}`);
2232
1897
  }
2233
1898
  // B-23: Wire agent and workflow menus to Electron menu bar
1899
+ // Wire panel toggle to WebSocket broadcast
1900
+ setPanelToggleBroadcast(broadcastPanelToggle);
2234
1901
  // Use standard macOS menu roles instead of reconstructing existing menu
2235
1902
  // (reconstructing fails on nested submenus like Window)
2236
1903
  // 22-5: Custom View menu with Verbose Mode toggle
@@ -2298,6 +1965,8 @@ if (isElectron) {
2298
1965
  if (claudeServiceInstance) {
2299
1966
  claudeServiceInstance.abort();
2300
1967
  }
1968
+ // MSSCI-14324: Clear session/once grants on shutdown
1969
+ clearSessionGrants();
2301
1970
  // B-24 fix: Clean up PID file on graceful shutdown
2302
1971
  const projectDir = getProjectDirectory();
2303
1972
  if (projectDir) {