@wong2kim/wmux 1.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 (122) hide show
  1. package/README.md +157 -0
  2. package/assets/icon.ico +0 -0
  3. package/assets/icon.svg +6 -0
  4. package/dist/cli/cli/client.js +102 -0
  5. package/dist/cli/cli/commands/browser.js +137 -0
  6. package/dist/cli/cli/commands/input.js +80 -0
  7. package/dist/cli/cli/commands/notify.js +28 -0
  8. package/dist/cli/cli/commands/pane.js +88 -0
  9. package/dist/cli/cli/commands/surface.js +98 -0
  10. package/dist/cli/cli/commands/system.js +98 -0
  11. package/dist/cli/cli/commands/workspace.js +117 -0
  12. package/dist/cli/cli/index.js +140 -0
  13. package/dist/cli/cli/utils.js +47 -0
  14. package/dist/cli/shared/constants.js +54 -0
  15. package/dist/cli/shared/rpc.js +33 -0
  16. package/dist/cli/shared/types.js +79 -0
  17. package/dist/mcp/mcp/index.js +60 -0
  18. package/dist/mcp/mcp/wmux-client.js +146 -0
  19. package/dist/mcp/shared/constants.js +54 -0
  20. package/dist/mcp/shared/rpc.js +33 -0
  21. package/dist/mcp/shared/types.js +79 -0
  22. package/forge.config.ts +61 -0
  23. package/index.html +12 -0
  24. package/package.json +84 -0
  25. package/postcss.config.js +6 -0
  26. package/src/cli/client.ts +76 -0
  27. package/src/cli/commands/browser.ts +128 -0
  28. package/src/cli/commands/input.ts +72 -0
  29. package/src/cli/commands/notify.ts +29 -0
  30. package/src/cli/commands/pane.ts +90 -0
  31. package/src/cli/commands/surface.ts +102 -0
  32. package/src/cli/commands/system.ts +95 -0
  33. package/src/cli/commands/workspace.ts +116 -0
  34. package/src/cli/index.ts +145 -0
  35. package/src/cli/utils.ts +44 -0
  36. package/src/main/index.ts +86 -0
  37. package/src/main/ipc/handlers/clipboard.handler.ts +20 -0
  38. package/src/main/ipc/handlers/metadata.handler.ts +56 -0
  39. package/src/main/ipc/handlers/pty.handler.ts +69 -0
  40. package/src/main/ipc/handlers/session.handler.ts +17 -0
  41. package/src/main/ipc/handlers/shell.handler.ts +11 -0
  42. package/src/main/ipc/registerHandlers.ts +31 -0
  43. package/src/main/mcp/McpRegistrar.ts +156 -0
  44. package/src/main/metadata/MetadataCollector.ts +58 -0
  45. package/src/main/notification/ToastManager.ts +32 -0
  46. package/src/main/pipe/PipeServer.ts +190 -0
  47. package/src/main/pipe/RpcRouter.ts +46 -0
  48. package/src/main/pipe/handlers/_bridge.ts +40 -0
  49. package/src/main/pipe/handlers/browser.rpc.ts +132 -0
  50. package/src/main/pipe/handlers/input.rpc.ts +120 -0
  51. package/src/main/pipe/handlers/meta.rpc.ts +59 -0
  52. package/src/main/pipe/handlers/notify.rpc.ts +53 -0
  53. package/src/main/pipe/handlers/pane.rpc.ts +39 -0
  54. package/src/main/pipe/handlers/surface.rpc.ts +43 -0
  55. package/src/main/pipe/handlers/system.rpc.ts +36 -0
  56. package/src/main/pipe/handlers/workspace.rpc.ts +52 -0
  57. package/src/main/pty/AgentDetector.ts +247 -0
  58. package/src/main/pty/OscParser.ts +81 -0
  59. package/src/main/pty/PTYBridge.ts +88 -0
  60. package/src/main/pty/PTYManager.ts +104 -0
  61. package/src/main/pty/ShellDetector.ts +63 -0
  62. package/src/main/session/SessionManager.ts +53 -0
  63. package/src/main/updater/AutoUpdater.ts +132 -0
  64. package/src/main/window/createWindow.ts +71 -0
  65. package/src/mcp/README.md +56 -0
  66. package/src/mcp/index.ts +153 -0
  67. package/src/mcp/wmux-client.ts +127 -0
  68. package/src/preload/index.ts +111 -0
  69. package/src/preload/preload.ts +108 -0
  70. package/src/renderer/App.tsx +5 -0
  71. package/src/renderer/components/Browser/BrowserPanel.tsx +219 -0
  72. package/src/renderer/components/Browser/BrowserToolbar.tsx +253 -0
  73. package/src/renderer/components/Company/ApprovalDialog.tsx +3 -0
  74. package/src/renderer/components/Company/CompanyView.tsx +7 -0
  75. package/src/renderer/components/Company/MessageFeedPanel.tsx +3 -0
  76. package/src/renderer/components/Layout/AppLayout.tsx +234 -0
  77. package/src/renderer/components/Notification/NotificationPanel.tsx +129 -0
  78. package/src/renderer/components/Palette/CommandPalette.tsx +409 -0
  79. package/src/renderer/components/Palette/PaletteItem.tsx +55 -0
  80. package/src/renderer/components/Pane/Pane.tsx +122 -0
  81. package/src/renderer/components/Pane/PaneContainer.tsx +41 -0
  82. package/src/renderer/components/Pane/SurfaceTabs.tsx +46 -0
  83. package/src/renderer/components/Settings/SettingsPanel.tsx +886 -0
  84. package/src/renderer/components/Sidebar/MiniSidebar.tsx +67 -0
  85. package/src/renderer/components/Sidebar/Sidebar.tsx +84 -0
  86. package/src/renderer/components/Sidebar/WorkspaceItem.tsx +241 -0
  87. package/src/renderer/components/StatusBar/StatusBar.tsx +93 -0
  88. package/src/renderer/components/Terminal/SearchBar.tsx +126 -0
  89. package/src/renderer/components/Terminal/Terminal.tsx +102 -0
  90. package/src/renderer/components/Terminal/ViCopyMode.tsx +104 -0
  91. package/src/renderer/hooks/useKeyboard.ts +310 -0
  92. package/src/renderer/hooks/useNotificationListener.ts +80 -0
  93. package/src/renderer/hooks/useNotificationSound.ts +75 -0
  94. package/src/renderer/hooks/useRpcBridge.ts +451 -0
  95. package/src/renderer/hooks/useT.ts +11 -0
  96. package/src/renderer/hooks/useTerminal.ts +349 -0
  97. package/src/renderer/hooks/useViCopyMode.ts +320 -0
  98. package/src/renderer/i18n/index.ts +69 -0
  99. package/src/renderer/i18n/locales/en.ts +157 -0
  100. package/src/renderer/i18n/locales/ja.ts +155 -0
  101. package/src/renderer/i18n/locales/ko.ts +155 -0
  102. package/src/renderer/i18n/locales/zh.ts +155 -0
  103. package/src/renderer/index.tsx +6 -0
  104. package/src/renderer/stores/index.ts +19 -0
  105. package/src/renderer/stores/slices/notificationSlice.ts +56 -0
  106. package/src/renderer/stores/slices/paneSlice.ts +141 -0
  107. package/src/renderer/stores/slices/surfaceSlice.ts +122 -0
  108. package/src/renderer/stores/slices/uiSlice.ts +247 -0
  109. package/src/renderer/stores/slices/workspaceSlice.ts +120 -0
  110. package/src/renderer/styles/globals.css +150 -0
  111. package/src/renderer/themes.ts +99 -0
  112. package/src/shared/constants.ts +53 -0
  113. package/src/shared/electron.d.ts +11 -0
  114. package/src/shared/rpc.ts +71 -0
  115. package/src/shared/types.ts +176 -0
  116. package/tailwind.config.js +11 -0
  117. package/tsconfig.cli.json +24 -0
  118. package/tsconfig.json +21 -0
  119. package/tsconfig.mcp.json +25 -0
  120. package/vite.main.config.ts +14 -0
  121. package/vite.preload.config.ts +9 -0
  122. package/vite.renderer.config.ts +6 -0
@@ -0,0 +1,53 @@
1
+ import type { BrowserWindow } from 'electron';
2
+ import type { RpcRouter } from '../RpcRouter';
3
+ import type { NotificationType } from '../../../shared/types';
4
+ import { IPC } from '../../../shared/constants';
5
+ import { ToastManager } from '../../notification/ToastManager';
6
+
7
+ type GetWindow = () => BrowserWindow | null;
8
+
9
+ const VALID_TYPES = new Set<NotificationType>(['info', 'warning', 'error', 'agent']);
10
+
11
+ function isNotificationType(value: unknown): value is NotificationType {
12
+ return typeof value === 'string' && VALID_TYPES.has(value as NotificationType);
13
+ }
14
+
15
+ export const toastManager = new ToastManager();
16
+
17
+ export function registerNotifyRpc(router: RpcRouter, getWindow: GetWindow): void {
18
+ /**
19
+ * notify — delivers a notification to the renderer UI and, when the app is
20
+ * not focused, also shows a Windows Toast notification.
21
+ *
22
+ * params: {
23
+ * title: string
24
+ * body: string
25
+ * type?: 'info' | 'warning' | 'error' | 'agent' (default: 'info')
26
+ * }
27
+ */
28
+ router.register('notify', (params) => {
29
+ if (typeof params['title'] !== 'string' || params['title'].length === 0) {
30
+ throw new Error('notify: missing required param "title"');
31
+ }
32
+ if (typeof params['body'] !== 'string') {
33
+ throw new Error('notify: missing required param "body"');
34
+ }
35
+
36
+ const title = params['title'];
37
+ const body = params['body'];
38
+ const type: NotificationType = isNotificationType(params['type'])
39
+ ? params['type']
40
+ : 'info';
41
+
42
+ const win = getWindow();
43
+ if (win && !win.isDestroyed()) {
44
+ // Push notification to the renderer notification store
45
+ win.webContents.send(IPC.NOTIFICATION, { title, body, type });
46
+ }
47
+
48
+ // Show OS-level toast (only when window is not focused)
49
+ toastManager.show(title, body);
50
+
51
+ return Promise.resolve({ delivered: true, type });
52
+ });
53
+ }
@@ -0,0 +1,39 @@
1
+ import type { BrowserWindow } from 'electron';
2
+ import type { RpcRouter } from '../RpcRouter';
3
+ import { sendToRenderer } from './_bridge';
4
+
5
+ type GetWindow = () => BrowserWindow | null;
6
+
7
+ export function registerPaneRpc(router: RpcRouter, getWindow: GetWindow): void {
8
+ /**
9
+ * pane.list — returns all panes (leaf nodes) of the current workspace
10
+ */
11
+ router.register('pane.list', (_params) =>
12
+ sendToRenderer(getWindow, 'pane.list'),
13
+ );
14
+
15
+ /**
16
+ * pane.focus — focuses a specific pane
17
+ * params: { id: string }
18
+ */
19
+ router.register('pane.focus', (params) => {
20
+ if (typeof params['id'] !== 'string') {
21
+ return Promise.reject(new Error('pane.focus: missing required param "id"'));
22
+ }
23
+ return sendToRenderer(getWindow, 'pane.focus', { id: params['id'] });
24
+ });
25
+
26
+ /**
27
+ * pane.split — splits the active pane
28
+ * params: { direction: 'horizontal' | 'vertical' }
29
+ */
30
+ router.register('pane.split', (params) => {
31
+ const direction = params['direction'];
32
+ if (direction !== 'horizontal' && direction !== 'vertical') {
33
+ return Promise.reject(
34
+ new Error('pane.split: "direction" must be "horizontal" or "vertical"'),
35
+ );
36
+ }
37
+ return sendToRenderer(getWindow, 'pane.split', { direction });
38
+ });
39
+ }
@@ -0,0 +1,43 @@
1
+ import type { BrowserWindow } from 'electron';
2
+ import type { RpcRouter } from '../RpcRouter';
3
+ import { sendToRenderer } from './_bridge';
4
+
5
+ type GetWindow = () => BrowserWindow | null;
6
+
7
+ export function registerSurfaceRpc(router: RpcRouter, getWindow: GetWindow): void {
8
+ /**
9
+ * surface.list — returns surfaces of the current workspace's active pane
10
+ */
11
+ router.register('surface.list', (_params) =>
12
+ sendToRenderer(getWindow, 'surface.list'),
13
+ );
14
+
15
+ /**
16
+ * surface.new — creates a new surface in the active pane
17
+ */
18
+ router.register('surface.new', (_params) =>
19
+ sendToRenderer(getWindow, 'surface.new'),
20
+ );
21
+
22
+ /**
23
+ * surface.focus — focuses a specific surface
24
+ * params: { id: string }
25
+ */
26
+ router.register('surface.focus', (params) => {
27
+ if (typeof params['id'] !== 'string') {
28
+ return Promise.reject(new Error('surface.focus: missing required param "id"'));
29
+ }
30
+ return sendToRenderer(getWindow, 'surface.focus', { id: params['id'] });
31
+ });
32
+
33
+ /**
34
+ * surface.close — closes a specific surface
35
+ * params: { id: string }
36
+ */
37
+ router.register('surface.close', (params) => {
38
+ if (typeof params['id'] !== 'string') {
39
+ return Promise.reject(new Error('surface.close: missing required param "id"'));
40
+ }
41
+ return sendToRenderer(getWindow, 'surface.close', { id: params['id'] });
42
+ });
43
+ }
@@ -0,0 +1,36 @@
1
+ import { app } from 'electron';
2
+ import type { RpcRouter } from '../RpcRouter';
3
+ import { ALL_RPC_METHODS } from '../../../shared/rpc';
4
+
5
+ /**
6
+ * Shape returned by system.identify.
7
+ */
8
+ interface SystemIdentity {
9
+ app: string;
10
+ version: string;
11
+ platform: NodeJS.Platform;
12
+ electronVersion: string;
13
+ }
14
+
15
+ export function registerSystemRpc(router: RpcRouter): void {
16
+ /**
17
+ * system.identify — returns static information about the running WinMux instance.
18
+ * No renderer round-trip needed; answered entirely from Main.
19
+ */
20
+ router.register('system.identify', (_params): Promise<SystemIdentity> => {
21
+ return Promise.resolve({
22
+ app: 'wmux',
23
+ version: app.getVersion(),
24
+ platform: process.platform,
25
+ electronVersion: process.versions.electron ?? 'unknown',
26
+ });
27
+ });
28
+
29
+ /**
30
+ * system.capabilities — returns the full list of registered RPC method names.
31
+ * Sourced from the single-source-of-truth array in shared/rpc.ts.
32
+ */
33
+ router.register('system.capabilities', (_params) => {
34
+ return Promise.resolve({ methods: ALL_RPC_METHODS });
35
+ });
36
+ }
@@ -0,0 +1,52 @@
1
+ import type { BrowserWindow } from 'electron';
2
+ import type { RpcRouter } from '../RpcRouter';
3
+ import { sendToRenderer } from './_bridge';
4
+
5
+ type GetWindow = () => BrowserWindow | null;
6
+
7
+ export function registerWorkspaceRpc(router: RpcRouter, getWindow: GetWindow): void {
8
+ /**
9
+ * workspace.list — returns all workspaces as {id, name}[]
10
+ */
11
+ router.register('workspace.list', (_params) =>
12
+ sendToRenderer(getWindow, 'workspace.list'),
13
+ );
14
+
15
+ /**
16
+ * workspace.new — creates a new workspace
17
+ * params: { name?: string }
18
+ */
19
+ router.register('workspace.new', (params) => {
20
+ const name = typeof params['name'] === 'string' ? params['name'] : undefined;
21
+ return sendToRenderer(getWindow, 'workspace.new', name !== undefined ? { name } : {});
22
+ });
23
+
24
+ /**
25
+ * workspace.focus — sets the active workspace
26
+ * params: { id: string }
27
+ */
28
+ router.register('workspace.focus', (params) => {
29
+ if (typeof params['id'] !== 'string') {
30
+ return Promise.reject(new Error('workspace.focus: missing required param "id"'));
31
+ }
32
+ return sendToRenderer(getWindow, 'workspace.focus', { id: params['id'] });
33
+ });
34
+
35
+ /**
36
+ * workspace.close — removes a workspace
37
+ * params: { id: string }
38
+ */
39
+ router.register('workspace.close', (params) => {
40
+ if (typeof params['id'] !== 'string') {
41
+ return Promise.reject(new Error('workspace.close: missing required param "id"'));
42
+ }
43
+ return sendToRenderer(getWindow, 'workspace.close', { id: params['id'] });
44
+ });
45
+
46
+ /**
47
+ * workspace.current — returns the currently active workspace {id, name}
48
+ */
49
+ router.register('workspace.current', (_params) =>
50
+ sendToRenderer(getWindow, 'workspace.current'),
51
+ );
52
+ }
@@ -0,0 +1,247 @@
1
+ // Terminal agent status detection — monitors PTY output for known AI agent
2
+ // prompt patterns and status indicators. This is status display only;
3
+ // no content is captured, stored, or transmitted.
4
+
5
+ export interface AgentEvent {
6
+ agent: string;
7
+ status: 'completed' | 'waiting' | 'running' | 'error';
8
+ message: string;
9
+ }
10
+
11
+ export interface CriticalEvent {
12
+ action: string;
13
+ riskLevel: 'review' | 'critical';
14
+ }
15
+
16
+ type AgentEventCallback = (event: AgentEvent) => void;
17
+ type CriticalEventCallback = (event: CriticalEvent) => void;
18
+
19
+ interface AgentPattern {
20
+ agent: string;
21
+ patterns: { regex: RegExp; status: AgentEvent['status']; message: string }[];
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Common cross-agent terminal patterns
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Shared completion indicators (✓ ✔ Done Complete Finished Success) */
29
+ const COMMON_COMPLETE: AgentPattern['patterns'] = [
30
+ { regex: /[✓✔]\s+(.+)/, status: 'completed', message: 'Task completed' },
31
+ { regex: /\b(Done|Complete(?:d)?|Finished|Success(?:ful)?)\b/, status: 'completed', message: 'Task completed' },
32
+ ];
33
+
34
+ /** Shared error indicators (✗ ✘ Error Failed error:) */
35
+ const COMMON_ERROR: AgentPattern['patterns'] = [
36
+ { regex: /[✗✘]\s+(.+)/, status: 'error', message: 'Error occurred' },
37
+ { regex: /\bFailed\b/, status: 'error', message: 'Task failed' },
38
+ { regex: /\berror:\s+(.+)/i, status: 'error', message: 'Error occurred' },
39
+ ];
40
+
41
+ /** Shared waiting indicators (? Waiting for Press y/n [Y/n]) */
42
+ const COMMON_WAITING: AgentPattern['patterns'] = [
43
+ { regex: /\?\s+(.+)/, status: 'waiting', message: 'Waiting for input' },
44
+ { regex: /Waiting for\s+(.+)/i, status: 'waiting', message: 'Waiting for input' },
45
+ { regex: /Press\s+.+\s+to\s+/i, status: 'waiting', message: 'Waiting for key press' },
46
+ { regex: /\[Y\/n\]|\(y\/n\)/i, status: 'waiting', message: 'Waiting for confirmation' },
47
+ ];
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Per-agent patterns
51
+ // ---------------------------------------------------------------------------
52
+
53
+ const AGENT_PATTERNS: AgentPattern[] = [
54
+ // ── Claude Code ────────────────────────────────────────────────────────────
55
+ {
56
+ agent: 'Claude Code',
57
+ patterns: [
58
+ { regex: /⏳\s+(.+)/, status: 'running', message: 'Processing...' },
59
+ { regex: /❌\s+(.+)/, status: 'error', message: 'Error occurred' },
60
+ { regex: /Do you want to/, status: 'waiting', message: 'Waiting for confirmation' },
61
+ ...COMMON_COMPLETE,
62
+ ...COMMON_ERROR,
63
+ ...COMMON_WAITING,
64
+ ],
65
+ },
66
+
67
+ // ── Cursor Agent ──────────────────────────────────────────────────────────
68
+ {
69
+ agent: 'Cursor Agent',
70
+ patterns: [
71
+ { regex: /Applied \d+ changes?/, status: 'completed', message: 'Changes applied' },
72
+ { regex: /Thinking\.\.\./, status: 'running', message: 'Thinking...' },
73
+ ...COMMON_COMPLETE,
74
+ ...COMMON_ERROR,
75
+ ...COMMON_WAITING,
76
+ ],
77
+ },
78
+
79
+ // ── Aider ─────────────────────────────────────────────────────────────────
80
+ {
81
+ agent: 'Aider',
82
+ patterns: [
83
+ { regex: /Applied edit to/, status: 'completed', message: 'Edit applied' },
84
+ { regex: /aider>/, status: 'waiting', message: 'Waiting for input' },
85
+ ...COMMON_COMPLETE,
86
+ ...COMMON_ERROR,
87
+ ...COMMON_WAITING,
88
+ ],
89
+ },
90
+
91
+ // ── Codex CLI ─────────────────────────────────────────────────────────────
92
+ {
93
+ agent: 'Codex CLI',
94
+ patterns: [
95
+ { regex: /codex>/, status: 'waiting', message: 'Waiting for input' },
96
+ { regex: /Codex:\s+(.+)/, status: 'running', message: 'Processing...' },
97
+ ...COMMON_COMPLETE,
98
+ ...COMMON_ERROR,
99
+ ...COMMON_WAITING,
100
+ ],
101
+ },
102
+
103
+ // ── Gemini CLI ────────────────────────────────────────────────────────────
104
+ {
105
+ agent: 'Gemini CLI',
106
+ patterns: [
107
+ { regex: /gemini>/, status: 'waiting', message: 'Waiting for input' },
108
+ { regex: /Gemini:\s+(.+)/, status: 'running', message: 'Processing...' },
109
+ ...COMMON_COMPLETE,
110
+ ...COMMON_ERROR,
111
+ ...COMMON_WAITING,
112
+ ],
113
+ },
114
+
115
+ // ── OpenCode ──────────────────────────────────────────────────────────────
116
+ {
117
+ agent: 'OpenCode',
118
+ patterns: [
119
+ { regex: /opencode>/, status: 'waiting', message: 'Waiting for input' },
120
+ ...COMMON_COMPLETE,
121
+ ...COMMON_ERROR,
122
+ ...COMMON_WAITING,
123
+ ],
124
+ },
125
+
126
+ // ── GitHub Copilot CLI ────────────────────────────────────────────────────
127
+ {
128
+ agent: 'GitHub Copilot CLI',
129
+ patterns: [
130
+ { regex: /copilot>/, status: 'waiting', message: 'Waiting for input' },
131
+ { regex: /gh copilot\s+(.+)/, status: 'running', message: 'Processing...' },
132
+ ...COMMON_COMPLETE,
133
+ ...COMMON_ERROR,
134
+ ...COMMON_WAITING,
135
+ ],
136
+ },
137
+ ];
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Critical action patterns — require approval before execution
141
+ // ---------------------------------------------------------------------------
142
+
143
+ interface CriticalPattern {
144
+ regex: RegExp;
145
+ riskLevel: 'review' | 'critical';
146
+ label: string;
147
+ }
148
+
149
+ const CRITICAL_PATTERNS: CriticalPattern[] = [
150
+ // Destructive git operations
151
+ { regex: /git\s+push\s+(?:.*--force|-f)\b/i, riskLevel: 'critical', label: 'git push --force' },
152
+ { regex: /git\s+reset\s+--hard\b/i, riskLevel: 'critical', label: 'git reset --hard' },
153
+ { regex: /git\s+clean\s+.*-f\b/i, riskLevel: 'critical', label: 'git clean -f' },
154
+ // File system wipe
155
+ { regex: /\brm\s+(?:.*-r.*-f|-f.*-r|-rf|-fr)\s+/i, riskLevel: 'critical', label: 'rm -rf' },
156
+ { regex: /\brmdir\s+\/[sS]\s+/, riskLevel: 'critical', label: 'rmdir /S' },
157
+ // Database destructive
158
+ { regex: /\bDROP\s+(?:TABLE|DATABASE|SCHEMA)\b/i, riskLevel: 'critical', label: 'DROP TABLE/DATABASE' },
159
+ { regex: /\bDELETE\s+FROM\b/i, riskLevel: 'review', label: 'DELETE FROM' },
160
+ { regex: /\bTRUNCATE\s+TABLE\b/i, riskLevel: 'critical', label: 'TRUNCATE TABLE' },
161
+ // NPM publishing
162
+ { regex: /\bnpm\s+publish\b/i, riskLevel: 'critical', label: 'npm publish' },
163
+ { regex: /\bnpx\s+.*--publish\b/i, riskLevel: 'review', label: 'npx publish' },
164
+ // Cloud resource destruction
165
+ { regex: /\bterraform\s+destroy\b/i, riskLevel: 'critical', label: 'terraform destroy' },
166
+ { regex: /\bkubectl\s+delete\b/i, riskLevel: 'review', label: 'kubectl delete' },
167
+ { regex: /\baws\s+.*\s+delete\b/i, riskLevel: 'review', label: 'aws delete' },
168
+ // Disk formatting
169
+ { regex: /\bformat\s+[A-Za-z]:\\/i, riskLevel: 'critical', label: 'format disk' },
170
+ { regex: /\bmkfs\b/i, riskLevel: 'critical', label: 'mkfs' },
171
+ ];
172
+
173
+ const MAX_BUFFER = 16 * 1024; // 16 KB
174
+
175
+ export class AgentDetector {
176
+ private callbacks: AgentEventCallback[] = [];
177
+ private criticalCallbacks: CriticalEventCallback[] = [];
178
+ private lineBuffer = '';
179
+ private lastEmittedKey = '';
180
+
181
+ onEvent(callback: AgentEventCallback): void {
182
+ this.callbacks.push(callback);
183
+ }
184
+
185
+ onCritical(callback: CriticalEventCallback): void {
186
+ this.criticalCallbacks.push(callback);
187
+ }
188
+
189
+ feed(data: string): void {
190
+ // Accumulate lines
191
+ this.lineBuffer += data;
192
+ // Prevent unbounded buffer growth
193
+ if (this.lineBuffer.length > MAX_BUFFER) {
194
+ this.lineBuffer = this.lineBuffer.slice(-MAX_BUFFER);
195
+ }
196
+ const lines = this.lineBuffer.split(/\r?\n/);
197
+ // Keep the last incomplete line in buffer
198
+ this.lineBuffer = lines.pop() || '';
199
+
200
+ for (const line of lines) {
201
+ this.processLine(line);
202
+ }
203
+ }
204
+
205
+ private processLine(line: string): void {
206
+ // Strip ANSI escape codes for pattern matching
207
+ const clean = line.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').trim();
208
+ if (!clean) return;
209
+
210
+ // Check critical patterns first
211
+ for (const cp of CRITICAL_PATTERNS) {
212
+ if (cp.regex.test(clean)) {
213
+ const key = `critical:${cp.label}:${clean.slice(0, 80)}`;
214
+ if (key !== this.lastEmittedKey) {
215
+ this.lastEmittedKey = key;
216
+ const event: CriticalEvent = { action: cp.label, riskLevel: cp.riskLevel };
217
+ for (const cb of this.criticalCallbacks) {
218
+ cb(event);
219
+ }
220
+ }
221
+ return;
222
+ }
223
+ }
224
+
225
+ for (const ap of AGENT_PATTERNS) {
226
+ for (const p of ap.patterns) {
227
+ const match = clean.match(p.regex);
228
+ if (match) {
229
+ // Deduplicate: don't emit the same event twice in a row
230
+ const key = `${ap.agent}:${p.status}:${match[0]}`;
231
+ if (key === this.lastEmittedKey) return;
232
+ this.lastEmittedKey = key;
233
+
234
+ const event: AgentEvent = {
235
+ agent: ap.agent,
236
+ status: p.status,
237
+ message: match[1] || p.message,
238
+ };
239
+ for (const cb of this.callbacks) {
240
+ cb(event);
241
+ }
242
+ return;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,81 @@
1
+ export interface OscEvent {
2
+ code: number;
3
+ data: string;
4
+ }
5
+
6
+ export type OscCallback = (event: OscEvent) => void;
7
+
8
+ /**
9
+ * Parses OSC (Operating System Command) sequences from terminal data.
10
+ * Handles OSC 7 (CWD), OSC 9/99/777 (notifications).
11
+ */
12
+ const MAX_BUFFER = 64 * 1024; // 64 KB
13
+
14
+ export class OscParser {
15
+ private buffer = '';
16
+ private inOsc = false;
17
+ private callbacks: OscCallback[] = [];
18
+
19
+ onOsc(callback: OscCallback): void {
20
+ this.callbacks.push(callback);
21
+ }
22
+
23
+ /**
24
+ * Process terminal data, extract OSC sequences, return cleaned data.
25
+ */
26
+ process(data: string): string {
27
+ let result = '';
28
+ let i = 0;
29
+
30
+ while (i < data.length) {
31
+ if (this.inOsc) {
32
+ // Look for ST (String Terminator): BEL (\x07) or ESC \ (\x1b\x5c)
33
+ if (data[i] === '\x07') {
34
+ this.emitOsc(this.buffer);
35
+ this.buffer = '';
36
+ this.inOsc = false;
37
+ i++;
38
+ } else if (data[i] === '\x1b' && i + 1 < data.length && data[i + 1] === '\\') {
39
+ this.emitOsc(this.buffer);
40
+ this.buffer = '';
41
+ this.inOsc = false;
42
+ i += 2;
43
+ } else {
44
+ this.buffer += data[i];
45
+ // Prevent unbounded buffer growth
46
+ if (this.buffer.length > MAX_BUFFER) {
47
+ this.buffer = '';
48
+ this.inOsc = false;
49
+ }
50
+ i++;
51
+ }
52
+ } else if (data[i] === '\x1b' && i + 1 < data.length && data[i + 1] === ']') {
53
+ // OSC start: ESC ]
54
+ this.inOsc = true;
55
+ this.buffer = '';
56
+ i += 2;
57
+ } else {
58
+ result += data[i];
59
+ i++;
60
+ }
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ private emitOsc(raw: string): void {
67
+ // OSC format: code;data
68
+ const semicolonIdx = raw.indexOf(';');
69
+ if (semicolonIdx === -1) return;
70
+
71
+ const codeStr = raw.substring(0, semicolonIdx);
72
+ const code = parseInt(codeStr, 10);
73
+ if (isNaN(code)) return;
74
+
75
+ const data = raw.substring(semicolonIdx + 1);
76
+
77
+ for (const cb of this.callbacks) {
78
+ cb({ code, data });
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,88 @@
1
+ import { BrowserWindow } from 'electron';
2
+ import { PTYManager } from './PTYManager';
3
+ import { OscParser } from './OscParser';
4
+ import { AgentDetector } from './AgentDetector';
5
+ import { ToastManager } from '../notification/ToastManager';
6
+ import { IPC } from '../../shared/constants';
7
+
8
+ export class PTYBridge {
9
+ private oscParsers = new Map<string, OscParser>();
10
+ private agentDetectors = new Map<string, AgentDetector>();
11
+ private toastManager = new ToastManager();
12
+
13
+ constructor(
14
+ private ptyManager: PTYManager,
15
+ private getWindow: () => BrowserWindow | null,
16
+ ) {}
17
+
18
+ setupDataForwarding(ptyId: string): void {
19
+ const instance = this.ptyManager.get(ptyId);
20
+ if (!instance) return;
21
+
22
+ const oscParser = new OscParser();
23
+ this.oscParsers.set(ptyId, oscParser);
24
+
25
+ const agentDetector = new AgentDetector();
26
+ this.agentDetectors.set(ptyId, agentDetector);
27
+
28
+ // Handle OSC events
29
+ oscParser.onOsc((event) => {
30
+ const win = this.getWindow();
31
+ if (!win || win.isDestroyed()) return;
32
+
33
+ switch (event.code) {
34
+ case 7: {
35
+ // CWD changed — data is typically file://host/path
36
+ const cwd = event.data.replace(/^file:\/\/[^/]*/, '');
37
+ win.webContents.send(IPC.CWD_CHANGED, ptyId, cwd);
38
+ break;
39
+ }
40
+ case 9: // Windows Terminal notification
41
+ case 99: // iTerm2 notification
42
+ case 777: // rxvt-unicode notification
43
+ // Silently ignore — no notification, no sound
44
+ break;
45
+ }
46
+ });
47
+
48
+ // Handle agent detection events — status tracking only, no notification/sound
49
+ agentDetector.onEvent(() => {
50
+ // Agent status is tracked internally by AgentDetector.
51
+ // No notification or sound — these fire too frequently and flood the UI.
52
+ });
53
+
54
+ // Handle critical action events — send approval request to renderer
55
+ agentDetector.onCritical((criticalEvent) => {
56
+ const win = this.getWindow();
57
+ if (!win || win.isDestroyed()) return;
58
+
59
+ win.webContents.send(IPC.APPROVAL_REQUEST, ptyId, {
60
+ action: criticalEvent.action,
61
+ riskLevel: criticalEvent.riskLevel,
62
+ });
63
+ });
64
+
65
+ instance.process.onData((data: string) => {
66
+ const win = this.getWindow();
67
+ if (win && !win.isDestroyed()) {
68
+ // Process data through OscParser (strips OSC sequences)
69
+ oscParser.process(data);
70
+ // Feed data to AgentDetector
71
+ agentDetector.feed(data);
72
+ // Forward raw data to renderer (xterm handles OSC itself)
73
+ win.webContents.send(IPC.PTY_DATA, ptyId, data);
74
+ }
75
+ });
76
+
77
+ instance.process.onExit(({ exitCode }) => {
78
+ const win = this.getWindow();
79
+ if (win && !win.isDestroyed()) {
80
+ win.webContents.send(IPC.PTY_EXIT, ptyId, exitCode);
81
+ }
82
+ this.oscParsers.delete(ptyId);
83
+ this.agentDetectors.delete(ptyId);
84
+ // Process already exited — remove from map without calling kill()
85
+ this.ptyManager.remove(ptyId);
86
+ });
87
+ }
88
+ }