@wong2kim/wmux 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -157
- package/dist/cli/cli/client.js +1 -1
- package/dist/cli/cli/commands/browser.js +19 -19
- package/dist/cli/cli/index.js +58 -58
- package/dist/cli/shared/constants.js +17 -4
- package/dist/mcp/shared/constants.js +17 -4
- package/package.json +96 -84
- package/assets/icon.ico +0 -0
- package/assets/icon.svg +0 -6
- package/forge.config.ts +0 -61
- package/index.html +0 -12
- package/postcss.config.js +0 -6
- package/src/cli/client.ts +0 -76
- package/src/cli/commands/browser.ts +0 -128
- package/src/cli/commands/input.ts +0 -72
- package/src/cli/commands/notify.ts +0 -29
- package/src/cli/commands/pane.ts +0 -90
- package/src/cli/commands/surface.ts +0 -102
- package/src/cli/commands/system.ts +0 -95
- package/src/cli/commands/workspace.ts +0 -116
- package/src/cli/index.ts +0 -145
- package/src/cli/utils.ts +0 -44
- package/src/main/index.ts +0 -86
- package/src/main/ipc/handlers/clipboard.handler.ts +0 -20
- package/src/main/ipc/handlers/metadata.handler.ts +0 -56
- package/src/main/ipc/handlers/pty.handler.ts +0 -69
- package/src/main/ipc/handlers/session.handler.ts +0 -17
- package/src/main/ipc/handlers/shell.handler.ts +0 -11
- package/src/main/ipc/registerHandlers.ts +0 -31
- package/src/main/mcp/McpRegistrar.ts +0 -156
- package/src/main/metadata/MetadataCollector.ts +0 -58
- package/src/main/notification/ToastManager.ts +0 -32
- package/src/main/pipe/PipeServer.ts +0 -190
- package/src/main/pipe/RpcRouter.ts +0 -46
- package/src/main/pipe/handlers/_bridge.ts +0 -40
- package/src/main/pipe/handlers/browser.rpc.ts +0 -132
- package/src/main/pipe/handlers/input.rpc.ts +0 -120
- package/src/main/pipe/handlers/meta.rpc.ts +0 -59
- package/src/main/pipe/handlers/notify.rpc.ts +0 -53
- package/src/main/pipe/handlers/pane.rpc.ts +0 -39
- package/src/main/pipe/handlers/surface.rpc.ts +0 -43
- package/src/main/pipe/handlers/system.rpc.ts +0 -36
- package/src/main/pipe/handlers/workspace.rpc.ts +0 -52
- package/src/main/pty/AgentDetector.ts +0 -247
- package/src/main/pty/OscParser.ts +0 -81
- package/src/main/pty/PTYBridge.ts +0 -88
- package/src/main/pty/PTYManager.ts +0 -104
- package/src/main/pty/ShellDetector.ts +0 -63
- package/src/main/session/SessionManager.ts +0 -53
- package/src/main/updater/AutoUpdater.ts +0 -132
- package/src/main/window/createWindow.ts +0 -71
- package/src/mcp/README.md +0 -56
- package/src/mcp/index.ts +0 -153
- package/src/mcp/wmux-client.ts +0 -127
- package/src/preload/index.ts +0 -111
- package/src/preload/preload.ts +0 -108
- package/src/renderer/App.tsx +0 -5
- package/src/renderer/components/Browser/BrowserPanel.tsx +0 -219
- package/src/renderer/components/Browser/BrowserToolbar.tsx +0 -253
- package/src/renderer/components/Company/ApprovalDialog.tsx +0 -3
- package/src/renderer/components/Company/CompanyView.tsx +0 -7
- package/src/renderer/components/Company/MessageFeedPanel.tsx +0 -3
- package/src/renderer/components/Layout/AppLayout.tsx +0 -234
- package/src/renderer/components/Notification/NotificationPanel.tsx +0 -129
- package/src/renderer/components/Palette/CommandPalette.tsx +0 -409
- package/src/renderer/components/Palette/PaletteItem.tsx +0 -55
- package/src/renderer/components/Pane/Pane.tsx +0 -122
- package/src/renderer/components/Pane/PaneContainer.tsx +0 -41
- package/src/renderer/components/Pane/SurfaceTabs.tsx +0 -46
- package/src/renderer/components/Settings/SettingsPanel.tsx +0 -886
- package/src/renderer/components/Sidebar/MiniSidebar.tsx +0 -67
- package/src/renderer/components/Sidebar/Sidebar.tsx +0 -84
- package/src/renderer/components/Sidebar/WorkspaceItem.tsx +0 -241
- package/src/renderer/components/StatusBar/StatusBar.tsx +0 -93
- package/src/renderer/components/Terminal/SearchBar.tsx +0 -126
- package/src/renderer/components/Terminal/Terminal.tsx +0 -102
- package/src/renderer/components/Terminal/ViCopyMode.tsx +0 -104
- package/src/renderer/hooks/useKeyboard.ts +0 -310
- package/src/renderer/hooks/useNotificationListener.ts +0 -80
- package/src/renderer/hooks/useNotificationSound.ts +0 -75
- package/src/renderer/hooks/useRpcBridge.ts +0 -451
- package/src/renderer/hooks/useT.ts +0 -11
- package/src/renderer/hooks/useTerminal.ts +0 -349
- package/src/renderer/hooks/useViCopyMode.ts +0 -320
- package/src/renderer/i18n/index.ts +0 -69
- package/src/renderer/i18n/locales/en.ts +0 -157
- package/src/renderer/i18n/locales/ja.ts +0 -155
- package/src/renderer/i18n/locales/ko.ts +0 -155
- package/src/renderer/i18n/locales/zh.ts +0 -155
- package/src/renderer/index.tsx +0 -6
- package/src/renderer/stores/index.ts +0 -19
- package/src/renderer/stores/slices/notificationSlice.ts +0 -56
- package/src/renderer/stores/slices/paneSlice.ts +0 -141
- package/src/renderer/stores/slices/surfaceSlice.ts +0 -122
- package/src/renderer/stores/slices/uiSlice.ts +0 -247
- package/src/renderer/stores/slices/workspaceSlice.ts +0 -120
- package/src/renderer/styles/globals.css +0 -150
- package/src/renderer/themes.ts +0 -99
- package/src/shared/constants.ts +0 -53
- package/src/shared/electron.d.ts +0 -11
- package/src/shared/rpc.ts +0 -71
- package/src/shared/types.ts +0 -176
- package/tailwind.config.js +0 -11
- package/tsconfig.cli.json +0 -24
- package/tsconfig.json +0 -21
- package/tsconfig.mcp.json +0 -25
- package/vite.main.config.ts +0 -14
- package/vite.preload.config.ts +0 -9
- package/vite.renderer.config.ts +0 -6
|
@@ -1,247 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import * as pty from 'node-pty';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import { getPipeName, ENV_KEYS } from '../../shared/constants';
|
|
4
|
-
|
|
5
|
-
export interface PTYInstance {
|
|
6
|
-
id: string;
|
|
7
|
-
process: pty.IPty;
|
|
8
|
-
shell: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class PTYManager {
|
|
12
|
-
private instances = new Map<string, PTYInstance>();
|
|
13
|
-
private nextId = 0;
|
|
14
|
-
private _authToken: string | undefined;
|
|
15
|
-
|
|
16
|
-
setAuthToken(token: string): void {
|
|
17
|
-
this._authToken = token;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
create(options?: {
|
|
21
|
-
shell?: string;
|
|
22
|
-
cwd?: string;
|
|
23
|
-
cols?: number;
|
|
24
|
-
rows?: number;
|
|
25
|
-
workspaceId?: string;
|
|
26
|
-
surfaceId?: string;
|
|
27
|
-
authToken?: string;
|
|
28
|
-
}): PTYInstance {
|
|
29
|
-
const id = `pty-${++this.nextId}`;
|
|
30
|
-
const shell = options?.shell || this.getDefaultShell();
|
|
31
|
-
const cwd = options?.cwd || os.homedir();
|
|
32
|
-
|
|
33
|
-
// Filter out ELECTRON_ variables to prevent leaking internal state to child processes
|
|
34
|
-
const env: Record<string, string> = {};
|
|
35
|
-
for (const [key, value] of Object.entries(globalThis.process.env)) {
|
|
36
|
-
if (value === undefined) continue;
|
|
37
|
-
if (key.startsWith('ELECTRON_')) continue;
|
|
38
|
-
env[key] = value;
|
|
39
|
-
}
|
|
40
|
-
env[ENV_KEYS.SOCKET_PATH] = getPipeName();
|
|
41
|
-
if (options?.workspaceId) env[ENV_KEYS.WORKSPACE_ID] = options.workspaceId;
|
|
42
|
-
if (options?.surfaceId) env[ENV_KEYS.SURFACE_ID] = options.surfaceId;
|
|
43
|
-
const authToken = options?.authToken || this._authToken;
|
|
44
|
-
if (authToken) env[ENV_KEYS.AUTH_TOKEN] = authToken;
|
|
45
|
-
|
|
46
|
-
const process = pty.spawn(shell, [], {
|
|
47
|
-
name: 'xterm-256color',
|
|
48
|
-
cols: options?.cols || 80,
|
|
49
|
-
rows: options?.rows || 24,
|
|
50
|
-
cwd,
|
|
51
|
-
env,
|
|
52
|
-
useConpty: true,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const instance: PTYInstance = { id, process, shell };
|
|
56
|
-
this.instances.set(id, instance);
|
|
57
|
-
return instance;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
write(id: string, data: string): void {
|
|
61
|
-
const instance = this.instances.get(id);
|
|
62
|
-
if (instance) {
|
|
63
|
-
instance.process.write(data);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
resize(id: string, cols: number, rows: number): void {
|
|
68
|
-
const instance = this.instances.get(id);
|
|
69
|
-
if (instance) {
|
|
70
|
-
instance.process.resize(cols, rows);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
dispose(id: string): void {
|
|
75
|
-
const instance = this.instances.get(id);
|
|
76
|
-
if (instance) {
|
|
77
|
-
try { instance.process.kill(); } catch { /* already dead */ }
|
|
78
|
-
this.instances.delete(id);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Remove an entry from the map without killing — use when the process has already exited. */
|
|
83
|
-
remove(id: string): void {
|
|
84
|
-
this.instances.delete(id);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
get(id: string): PTYInstance | undefined {
|
|
88
|
-
return this.instances.get(id);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
disposeAll(): void {
|
|
92
|
-
for (const [id] of this.instances) {
|
|
93
|
-
this.dispose(id);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private getDefaultShell(): string {
|
|
98
|
-
if (process.platform === 'win32') {
|
|
99
|
-
// PowerShell 우선 (cd로 드라이브 전환 자동 지원)
|
|
100
|
-
return 'powershell.exe';
|
|
101
|
-
}
|
|
102
|
-
return process.env.SHELL || '/bin/bash';
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export interface ShellInfo {
|
|
5
|
-
name: string;
|
|
6
|
-
path: string;
|
|
7
|
-
args?: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class ShellDetector {
|
|
11
|
-
detect(): ShellInfo[] {
|
|
12
|
-
const shells: ShellInfo[] = [];
|
|
13
|
-
|
|
14
|
-
// PowerShell 7+ (pwsh)
|
|
15
|
-
const pwshPaths = [
|
|
16
|
-
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
17
|
-
path.join(process.env.LOCALAPPDATA || '', 'Microsoft\\WindowsApps\\pwsh.exe'),
|
|
18
|
-
];
|
|
19
|
-
for (const p of pwshPaths) {
|
|
20
|
-
if (fs.existsSync(p)) {
|
|
21
|
-
shells.push({ name: 'PowerShell 7', path: p });
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Windows PowerShell 5.1
|
|
27
|
-
const ps5 = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\WindowsPowerShell\\v1.0\\powershell.exe');
|
|
28
|
-
if (fs.existsSync(ps5)) {
|
|
29
|
-
shells.push({ name: 'Windows PowerShell', path: ps5 });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Git Bash
|
|
33
|
-
const gitBashPaths = [
|
|
34
|
-
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
35
|
-
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
36
|
-
];
|
|
37
|
-
for (const p of gitBashPaths) {
|
|
38
|
-
if (fs.existsSync(p)) {
|
|
39
|
-
shells.push({ name: 'Git Bash', path: p, args: ['--login', '-i'] });
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// WSL
|
|
45
|
-
const wslPath = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\wsl.exe');
|
|
46
|
-
if (fs.existsSync(wslPath)) {
|
|
47
|
-
shells.push({ name: 'WSL', path: wslPath });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// cmd.exe
|
|
51
|
-
const cmd = process.env.COMSPEC || path.join(process.env.SystemRoot || 'C:\\Windows', 'System32\\cmd.exe');
|
|
52
|
-
if (fs.existsSync(cmd)) {
|
|
53
|
-
shells.push({ name: 'Command Prompt', path: cmd });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return shells;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
getDefault(): string {
|
|
60
|
-
const shells = this.detect();
|
|
61
|
-
return shells.length > 0 ? shells[0].path : 'powershell.exe';
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { app } from 'electron';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import type { SessionData } from '../../shared/types';
|
|
5
|
-
|
|
6
|
-
export class SessionManager {
|
|
7
|
-
private filePath: string;
|
|
8
|
-
|
|
9
|
-
constructor() {
|
|
10
|
-
this.filePath = path.join(app.getPath('userData'), 'session.json');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
save(data: SessionData): void {
|
|
14
|
-
try {
|
|
15
|
-
const dir = path.dirname(this.filePath);
|
|
16
|
-
if (!fs.existsSync(dir)) {
|
|
17
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error('Failed to save session:', err);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
load(): SessionData | null {
|
|
26
|
-
try {
|
|
27
|
-
if (!fs.existsSync(this.filePath)) return null;
|
|
28
|
-
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
29
|
-
|
|
30
|
-
// Guard against prototype pollution via JSON reviver
|
|
31
|
-
const parsed: unknown = JSON.parse(raw, (key, value) => {
|
|
32
|
-
if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
|
|
33
|
-
return value;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Basic schema validation
|
|
37
|
-
if (
|
|
38
|
-
typeof parsed !== 'object' ||
|
|
39
|
-
parsed === null ||
|
|
40
|
-
!Array.isArray((parsed as Record<string, unknown>)['workspaces']) ||
|
|
41
|
-
typeof (parsed as Record<string, unknown>)['activeWorkspaceId'] !== 'string'
|
|
42
|
-
) {
|
|
43
|
-
console.warn('[SessionManager] Session file failed schema validation — discarding.');
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return parsed as SessionData;
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error('Failed to load session:', err);
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|