@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.
- package/README.md +157 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.svg +6 -0
- package/dist/cli/cli/client.js +102 -0
- package/dist/cli/cli/commands/browser.js +137 -0
- package/dist/cli/cli/commands/input.js +80 -0
- package/dist/cli/cli/commands/notify.js +28 -0
- package/dist/cli/cli/commands/pane.js +88 -0
- package/dist/cli/cli/commands/surface.js +98 -0
- package/dist/cli/cli/commands/system.js +98 -0
- package/dist/cli/cli/commands/workspace.js +117 -0
- package/dist/cli/cli/index.js +140 -0
- package/dist/cli/cli/utils.js +47 -0
- package/dist/cli/shared/constants.js +54 -0
- package/dist/cli/shared/rpc.js +33 -0
- package/dist/cli/shared/types.js +79 -0
- package/dist/mcp/mcp/index.js +60 -0
- package/dist/mcp/mcp/wmux-client.js +146 -0
- package/dist/mcp/shared/constants.js +54 -0
- package/dist/mcp/shared/rpc.js +33 -0
- package/dist/mcp/shared/types.js +79 -0
- package/forge.config.ts +61 -0
- package/index.html +12 -0
- package/package.json +84 -0
- package/postcss.config.js +6 -0
- package/src/cli/client.ts +76 -0
- package/src/cli/commands/browser.ts +128 -0
- package/src/cli/commands/input.ts +72 -0
- package/src/cli/commands/notify.ts +29 -0
- package/src/cli/commands/pane.ts +90 -0
- package/src/cli/commands/surface.ts +102 -0
- package/src/cli/commands/system.ts +95 -0
- package/src/cli/commands/workspace.ts +116 -0
- package/src/cli/index.ts +145 -0
- package/src/cli/utils.ts +44 -0
- package/src/main/index.ts +86 -0
- package/src/main/ipc/handlers/clipboard.handler.ts +20 -0
- package/src/main/ipc/handlers/metadata.handler.ts +56 -0
- package/src/main/ipc/handlers/pty.handler.ts +69 -0
- package/src/main/ipc/handlers/session.handler.ts +17 -0
- package/src/main/ipc/handlers/shell.handler.ts +11 -0
- package/src/main/ipc/registerHandlers.ts +31 -0
- package/src/main/mcp/McpRegistrar.ts +156 -0
- package/src/main/metadata/MetadataCollector.ts +58 -0
- package/src/main/notification/ToastManager.ts +32 -0
- package/src/main/pipe/PipeServer.ts +190 -0
- package/src/main/pipe/RpcRouter.ts +46 -0
- package/src/main/pipe/handlers/_bridge.ts +40 -0
- package/src/main/pipe/handlers/browser.rpc.ts +132 -0
- package/src/main/pipe/handlers/input.rpc.ts +120 -0
- package/src/main/pipe/handlers/meta.rpc.ts +59 -0
- package/src/main/pipe/handlers/notify.rpc.ts +53 -0
- package/src/main/pipe/handlers/pane.rpc.ts +39 -0
- package/src/main/pipe/handlers/surface.rpc.ts +43 -0
- package/src/main/pipe/handlers/system.rpc.ts +36 -0
- package/src/main/pipe/handlers/workspace.rpc.ts +52 -0
- package/src/main/pty/AgentDetector.ts +247 -0
- package/src/main/pty/OscParser.ts +81 -0
- package/src/main/pty/PTYBridge.ts +88 -0
- package/src/main/pty/PTYManager.ts +104 -0
- package/src/main/pty/ShellDetector.ts +63 -0
- package/src/main/session/SessionManager.ts +53 -0
- package/src/main/updater/AutoUpdater.ts +132 -0
- package/src/main/window/createWindow.ts +71 -0
- package/src/mcp/README.md +56 -0
- package/src/mcp/index.ts +153 -0
- package/src/mcp/wmux-client.ts +127 -0
- package/src/preload/index.ts +111 -0
- package/src/preload/preload.ts +108 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/components/Browser/BrowserPanel.tsx +219 -0
- package/src/renderer/components/Browser/BrowserToolbar.tsx +253 -0
- package/src/renderer/components/Company/ApprovalDialog.tsx +3 -0
- package/src/renderer/components/Company/CompanyView.tsx +7 -0
- package/src/renderer/components/Company/MessageFeedPanel.tsx +3 -0
- package/src/renderer/components/Layout/AppLayout.tsx +234 -0
- package/src/renderer/components/Notification/NotificationPanel.tsx +129 -0
- package/src/renderer/components/Palette/CommandPalette.tsx +409 -0
- package/src/renderer/components/Palette/PaletteItem.tsx +55 -0
- package/src/renderer/components/Pane/Pane.tsx +122 -0
- package/src/renderer/components/Pane/PaneContainer.tsx +41 -0
- package/src/renderer/components/Pane/SurfaceTabs.tsx +46 -0
- package/src/renderer/components/Settings/SettingsPanel.tsx +886 -0
- package/src/renderer/components/Sidebar/MiniSidebar.tsx +67 -0
- package/src/renderer/components/Sidebar/Sidebar.tsx +84 -0
- package/src/renderer/components/Sidebar/WorkspaceItem.tsx +241 -0
- package/src/renderer/components/StatusBar/StatusBar.tsx +93 -0
- package/src/renderer/components/Terminal/SearchBar.tsx +126 -0
- package/src/renderer/components/Terminal/Terminal.tsx +102 -0
- package/src/renderer/components/Terminal/ViCopyMode.tsx +104 -0
- package/src/renderer/hooks/useKeyboard.ts +310 -0
- package/src/renderer/hooks/useNotificationListener.ts +80 -0
- package/src/renderer/hooks/useNotificationSound.ts +75 -0
- package/src/renderer/hooks/useRpcBridge.ts +451 -0
- package/src/renderer/hooks/useT.ts +11 -0
- package/src/renderer/hooks/useTerminal.ts +349 -0
- package/src/renderer/hooks/useViCopyMode.ts +320 -0
- package/src/renderer/i18n/index.ts +69 -0
- package/src/renderer/i18n/locales/en.ts +157 -0
- package/src/renderer/i18n/locales/ja.ts +155 -0
- package/src/renderer/i18n/locales/ko.ts +155 -0
- package/src/renderer/i18n/locales/zh.ts +155 -0
- package/src/renderer/index.tsx +6 -0
- package/src/renderer/stores/index.ts +19 -0
- package/src/renderer/stores/slices/notificationSlice.ts +56 -0
- package/src/renderer/stores/slices/paneSlice.ts +141 -0
- package/src/renderer/stores/slices/surfaceSlice.ts +122 -0
- package/src/renderer/stores/slices/uiSlice.ts +247 -0
- package/src/renderer/stores/slices/workspaceSlice.ts +120 -0
- package/src/renderer/styles/globals.css +150 -0
- package/src/renderer/themes.ts +99 -0
- package/src/shared/constants.ts +53 -0
- package/src/shared/electron.d.ts +11 -0
- package/src/shared/rpc.ts +71 -0
- package/src/shared/types.ts +176 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.cli.json +24 -0
- package/tsconfig.json +21 -0
- package/tsconfig.mcp.json +25 -0
- package/vite.main.config.ts +14 -0
- package/vite.preload.config.ts +9 -0
- 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
|
+
}
|