@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,60 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const zod_1 = require("zod");
7
+ const wmux_client_1 = require("./wmux-client");
8
+ const server = new mcp_js_1.McpServer({
9
+ name: 'wmux',
10
+ version: '1.0.0',
11
+ });
12
+ // Helper: wrap an RPC call as an MCP tool result
13
+ async function callRpc(method, params = {}) {
14
+ const result = await (0, wmux_client_1.sendRpc)(method, params);
15
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
16
+ return { content: [{ type: 'text', text }] };
17
+ }
18
+ // Optional surfaceId schema used by browser and terminal tools
19
+ const optionalSurfaceId = zod_1.z.string().optional().describe('Target a specific surface by ID. Omit to use the active surface.');
20
+ // === Browser tools ===
21
+ server.tool('browser_open', 'Open a new browser panel in the active pane. Use this when no browser surface exists yet.', {
22
+ url: zod_1.z.string().optional().describe('Initial URL to load (defaults to google.com)'),
23
+ }, async ({ url }) => callRpc('browser.open', url ? { url } : {}));
24
+ server.tool('browser_navigate', 'Navigate the wmux browser panel to a URL', {
25
+ url: zod_1.z.string().describe('The URL to navigate to'),
26
+ surfaceId: optionalSurfaceId,
27
+ }, async ({ url, surfaceId }) => callRpc('browser.navigate', { url, ...(surfaceId && { surfaceId }) }));
28
+ server.tool('browser_snapshot', 'Get the full HTML content of the current page in the wmux browser panel', { surfaceId: optionalSurfaceId }, async ({ surfaceId }) => callRpc('browser.snapshot', surfaceId ? { surfaceId } : {}));
29
+ server.tool('browser_click', 'Click an element in the wmux browser panel by CSS selector', {
30
+ selector: zod_1.z.string().describe('CSS selector of the element to click'),
31
+ surfaceId: optionalSurfaceId,
32
+ }, async ({ selector, surfaceId }) => callRpc('browser.click', { selector, ...(surfaceId && { surfaceId }) }));
33
+ server.tool('browser_fill', 'Fill an input field in the wmux browser panel by CSS selector', {
34
+ selector: zod_1.z.string().describe('CSS selector of the input element'),
35
+ text: zod_1.z.string().describe('Text to fill into the input'),
36
+ surfaceId: optionalSurfaceId,
37
+ }, async ({ selector, text, surfaceId }) => callRpc('browser.fill', { selector, text, ...(surfaceId && { surfaceId }) }));
38
+ server.tool('browser_eval', 'Execute JavaScript in the wmux browser panel and return the result', {
39
+ code: zod_1.z.string().describe('JavaScript code to execute in the browser context'),
40
+ surfaceId: optionalSurfaceId,
41
+ }, async ({ code, surfaceId }) => callRpc('browser.eval', { code, ...(surfaceId && { surfaceId }) }));
42
+ // === Terminal tools ===
43
+ server.tool('terminal_read', 'Read the current visible text from the active terminal in wmux', {}, async () => callRpc('input.readScreen'));
44
+ server.tool('terminal_send', 'Send text to the active terminal in wmux', { text: zod_1.z.string().describe('Text to send to the terminal') }, async ({ text }) => callRpc('input.send', { text }));
45
+ server.tool('terminal_send_key', 'Send a named key to the active terminal (enter, tab, ctrl+c, ctrl+d, ctrl+z, ctrl+l, escape, up, down, right, left)', {
46
+ key: zod_1.z.string().describe('Key name: enter, tab, ctrl+c, ctrl+d, ctrl+z, ctrl+l, escape, up, down, right, left'),
47
+ }, async ({ key }) => callRpc('input.sendKey', { key }));
48
+ // === Workspace tools ===
49
+ server.tool('workspace_list', 'List all workspaces in wmux', {}, async () => callRpc('workspace.list'));
50
+ server.tool('surface_list', 'List all surfaces (terminals and browsers) in the active workspace', {}, async () => callRpc('surface.list'));
51
+ server.tool('pane_list', 'List all panes in the current workspace', {}, async () => callRpc('pane.list'));
52
+ // === Start server ===
53
+ async function main() {
54
+ const transport = new stdio_js_1.StdioServerTransport();
55
+ await server.connect(transport);
56
+ }
57
+ main().catch((err) => {
58
+ console.error('wmux MCP server failed to start:', err);
59
+ process.exit(1);
60
+ });
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sendRpc = sendRpc;
37
+ const net = __importStar(require("net"));
38
+ const fs = __importStar(require("fs"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const constants_1 = require("../shared/constants");
41
+ const TIMEOUT_MS = 10000;
42
+ const RETRY_COUNT = 3;
43
+ const RETRY_DELAY_MS = 1000;
44
+ function readAuthToken() {
45
+ // Env var takes priority (when running inside wmux terminal)
46
+ if (process.env.WMUX_AUTH_TOKEN)
47
+ return process.env.WMUX_AUTH_TOKEN;
48
+ // File fallback (when spawned by Claude Code as MCP server)
49
+ try {
50
+ return fs.readFileSync((0, constants_1.getAuthTokenPath)(), 'utf8').trim();
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function attemptRpc(pipePath, token, method, params) {
57
+ return new Promise((resolve, reject) => {
58
+ const id = crypto.randomUUID();
59
+ const request = JSON.stringify({ id, method, params, token }) + '\n';
60
+ const socket = net.connect(pipePath);
61
+ let buffer = '';
62
+ let settled = false;
63
+ const timer = setTimeout(() => {
64
+ if (!settled) {
65
+ settled = true;
66
+ socket.destroy();
67
+ reject(new Error(`RPC timeout: ${method} (${TIMEOUT_MS}ms)`));
68
+ }
69
+ }, TIMEOUT_MS);
70
+ socket.on('connect', () => {
71
+ socket.write(request);
72
+ });
73
+ socket.on('data', (chunk) => {
74
+ buffer += chunk.toString('utf8');
75
+ const lines = buffer.split('\n');
76
+ buffer = lines.pop() ?? '';
77
+ for (const line of lines) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed)
80
+ continue;
81
+ try {
82
+ const response = JSON.parse(trimmed);
83
+ if (response.id === id && !settled) {
84
+ settled = true;
85
+ clearTimeout(timer);
86
+ socket.destroy();
87
+ if (response.ok) {
88
+ resolve(response.result);
89
+ }
90
+ else {
91
+ reject(new Error(response.error));
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // ignore malformed lines
97
+ }
98
+ }
99
+ });
100
+ socket.on('error', (err) => {
101
+ if (!settled) {
102
+ settled = true;
103
+ clearTimeout(timer);
104
+ if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
105
+ reject(new Error('wmux is not running. Start the app first.'));
106
+ }
107
+ else {
108
+ reject(new Error(`Connection error: ${err.message}`));
109
+ }
110
+ }
111
+ });
112
+ socket.on('close', () => {
113
+ if (!settled) {
114
+ settled = true;
115
+ clearTimeout(timer);
116
+ reject(new Error('Connection closed before response was received.'));
117
+ }
118
+ });
119
+ });
120
+ }
121
+ function sleep(ms) {
122
+ return new Promise((r) => setTimeout(r, ms));
123
+ }
124
+ async function sendRpc(method, params = {}) {
125
+ const pipePath = process.env.WMUX_SOCKET_PATH || (0, constants_1.getPipeName)();
126
+ for (let attempt = 0; attempt < RETRY_COUNT; attempt++) {
127
+ // Re-read token on every attempt (wmux may have restarted with new token)
128
+ const token = readAuthToken();
129
+ if (!token) {
130
+ throw new Error('wmux auth token not found. Is wmux running?');
131
+ }
132
+ try {
133
+ return await attemptRpc(pipePath, token, method, params);
134
+ }
135
+ catch (err) {
136
+ const msg = err.message;
137
+ const isRetryable = msg.includes('not running') || msg.includes('unauthorized');
138
+ if (isRetryable && attempt < RETRY_COUNT - 1) {
139
+ await sleep(RETRY_DELAY_MS);
140
+ continue;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ throw new Error('wmux is not running. Start the app first.');
146
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENV_KEYS = exports.PIPE_NAME = exports.IPC = void 0;
4
+ exports.getPipeName = getPipeName;
5
+ exports.getAuthTokenPath = getAuthTokenPath;
6
+ // IPC Channel names
7
+ exports.IPC = {
8
+ PTY_CREATE: 'pty:create',
9
+ PTY_WRITE: 'pty:write',
10
+ PTY_RESIZE: 'pty:resize',
11
+ PTY_DISPOSE: 'pty:dispose',
12
+ PTY_DATA: 'pty:data',
13
+ PTY_EXIT: 'pty:exit',
14
+ SHELL_LIST: 'shell:list',
15
+ SESSION_SAVE: 'session:save',
16
+ SESSION_LOAD: 'session:load',
17
+ NOTIFICATION: 'notification:new',
18
+ CWD_CHANGED: 'notification:cwd-changed',
19
+ METADATA_UPDATE: 'metadata:update',
20
+ METADATA_REQUEST: 'metadata:request',
21
+ // Phase 3: RPC bridge (Main ↔ Renderer)
22
+ RPC_COMMAND: 'rpc:command',
23
+ RPC_RESPONSE: 'rpc:response',
24
+ // Clipboard (main process bridge)
25
+ CLIPBOARD_WRITE: 'clipboard:write',
26
+ CLIPBOARD_READ: 'clipboard:read',
27
+ // Phase 4: Auto updater
28
+ UPDATE_CHECK: 'update:check',
29
+ UPDATE_AVAILABLE: 'update:available',
30
+ UPDATE_NOT_AVAILABLE: 'update:not-available',
31
+ UPDATE_ERROR: 'update:error',
32
+ UPDATE_DOWNLOAD: 'update:download',
33
+ UPDATE_INSTALL: 'update:install',
34
+ // Settings sync (renderer → main)
35
+ TOAST_ENABLED: 'settings:toast-enabled',
36
+ };
37
+ // Named Pipe path for wmux API
38
+ // Fixed name so MCP clients (e.g. Claude Code) can reconnect across wmux restarts
39
+ exports.PIPE_NAME = '\\\\.\\pipe\\wmux';
40
+ function getPipeName() {
41
+ return exports.PIPE_NAME;
42
+ }
43
+ // Environment variable names injected into PTY sessions
44
+ exports.ENV_KEYS = {
45
+ WORKSPACE_ID: 'WMUX_WORKSPACE_ID',
46
+ SURFACE_ID: 'WMUX_SURFACE_ID',
47
+ SOCKET_PATH: 'WMUX_SOCKET_PATH',
48
+ AUTH_TOKEN: 'WMUX_AUTH_TOKEN',
49
+ };
50
+ // Auth token file path — written by wmux main process, read by MCP server
51
+ function getAuthTokenPath() {
52
+ const home = process.env.USERPROFILE || process.env.HOME || '';
53
+ return `${home}/.wmux-auth-token`;
54
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // === JSON-RPC Protocol Types ===
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ALL_RPC_METHODS = void 0;
5
+ // All available methods as array (for system.capabilities)
6
+ exports.ALL_RPC_METHODS = [
7
+ 'workspace.list',
8
+ 'workspace.new',
9
+ 'workspace.focus',
10
+ 'workspace.close',
11
+ 'workspace.current',
12
+ 'surface.list',
13
+ 'surface.new',
14
+ 'surface.focus',
15
+ 'surface.close',
16
+ 'pane.list',
17
+ 'pane.focus',
18
+ 'pane.split',
19
+ 'input.send',
20
+ 'input.sendKey',
21
+ 'input.readScreen',
22
+ 'notify',
23
+ 'meta.setStatus',
24
+ 'meta.setProgress',
25
+ 'system.identify',
26
+ 'system.capabilities',
27
+ 'browser.open',
28
+ 'browser.snapshot',
29
+ 'browser.click',
30
+ 'browser.fill',
31
+ 'browser.eval',
32
+ 'browser.navigate',
33
+ ];
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateId = generateId;
4
+ exports.sanitizePtyText = sanitizePtyText;
5
+ exports.validateName = validateName;
6
+ exports.validateMessage = validateMessage;
7
+ exports.createSurface = createSurface;
8
+ exports.createLeafPane = createLeafPane;
9
+ exports.createWorkspace = createWorkspace;
10
+ // === Utility: generate unique IDs ===
11
+ function generateId(prefix) {
12
+ return `${prefix}-${crypto.randomUUID()}`;
13
+ }
14
+ // === Security: sanitize text before PTY write ===
15
+ /**
16
+ * Strips control characters (\r, \n, \x00-\x1f except \t) from text
17
+ * that will be written to a PTY, preventing embedded command injection.
18
+ */
19
+ function sanitizePtyText(text) {
20
+ // Remove all control chars except tab (\x09)
21
+ // eslint-disable-next-line no-control-regex
22
+ return text.replace(/[\x00-\x08\x0a-\x1f\x7f\u0080-\u009f]/g, '');
23
+ }
24
+ /**
25
+ * Validates and clamps a user-supplied name string.
26
+ * Returns the trimmed string if valid, or throws if invalid.
27
+ */
28
+ function validateName(value, label, maxLength = 100) {
29
+ const trimmed = value.trim();
30
+ if (trimmed.length === 0) {
31
+ throw new Error(`${label} must not be empty`);
32
+ }
33
+ if (trimmed.length > maxLength) {
34
+ throw new Error(`${label} must be ${maxLength} characters or fewer`);
35
+ }
36
+ return trimmed;
37
+ }
38
+ /**
39
+ * Validates a message body string.
40
+ * Returns the trimmed string if valid, or throws if invalid.
41
+ */
42
+ function validateMessage(value, maxLength = 10000) {
43
+ const trimmed = value.trim();
44
+ if (trimmed.length === 0) {
45
+ throw new Error('Message must not be empty');
46
+ }
47
+ if (trimmed.length > maxLength) {
48
+ throw new Error(`Message must be ${maxLength} characters or fewer`);
49
+ }
50
+ return trimmed;
51
+ }
52
+ // === Factory functions ===
53
+ function createSurface(ptyId, shell, cwd) {
54
+ return {
55
+ id: generateId('surface'),
56
+ ptyId,
57
+ title: shell,
58
+ shell,
59
+ cwd,
60
+ };
61
+ }
62
+ function createLeafPane(surface) {
63
+ const surfaces = surface ? [surface] : [];
64
+ return {
65
+ id: generateId('pane'),
66
+ type: 'leaf',
67
+ surfaces,
68
+ activeSurfaceId: surfaces[0]?.id || '',
69
+ };
70
+ }
71
+ function createWorkspace(name) {
72
+ const rootPane = createLeafPane();
73
+ return {
74
+ id: generateId('ws'),
75
+ name,
76
+ rootPane,
77
+ activePaneId: rootPane.id,
78
+ };
79
+ }
@@ -0,0 +1,61 @@
1
+ import type { ForgeConfig } from '@electron-forge/shared-types';
2
+ import { MakerSquirrel } from '@electron-forge/maker-squirrel';
3
+ import { MakerZIP } from '@electron-forge/maker-zip';
4
+ import { VitePlugin } from '@electron-forge/plugin-vite';
5
+ import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
6
+ import { FusesPlugin } from '@electron-forge/plugin-fuses';
7
+ import { FuseV1Options, FuseVersion } from '@electron/fuses';
8
+
9
+ const config: ForgeConfig = {
10
+ packagerConfig: {
11
+ asar: true,
12
+ icon: './assets/icon',
13
+ extraResource: ['./dist/mcp'],
14
+ },
15
+ rebuildConfig: { disablePreGypRecuild: true },
16
+ hooks: {
17
+ // Skip native rebuild — pre-built binaries already in node_modules
18
+ readPackageJson: async (_config, packageJson) => {
19
+ packageJson.dependencies = packageJson.dependencies || {};
20
+ return packageJson;
21
+ },
22
+ },
23
+ makers: [
24
+ new MakerSquirrel({}),
25
+ new MakerZIP({}, ['darwin']),
26
+ ],
27
+ plugins: [
28
+ new AutoUnpackNativesPlugin({}),
29
+ new VitePlugin({
30
+ build: [
31
+ {
32
+ entry: 'src/main/index.ts',
33
+ config: 'vite.main.config.ts',
34
+ target: 'main',
35
+ },
36
+ {
37
+ entry: 'src/preload/preload.ts',
38
+ config: 'vite.preload.config.ts',
39
+ target: 'preload',
40
+ },
41
+ ],
42
+ renderer: [
43
+ {
44
+ name: 'main_window',
45
+ config: 'vite.renderer.config.ts',
46
+ },
47
+ ],
48
+ }),
49
+ new FusesPlugin({
50
+ version: FuseVersion.V1,
51
+ [FuseV1Options.RunAsNode]: false,
52
+ [FuseV1Options.EnableCookieEncryption]: true,
53
+ [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
54
+ [FuseV1Options.EnableNodeCliInspectArguments]: false,
55
+ [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
56
+ [FuseV1Options.OnlyLoadAppFromAsar]: true,
57
+ }),
58
+ ],
59
+ };
60
+
61
+ export default config;
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>WinMux</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/renderer/index.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@wong2kim/wmux",
3
+ "productName": "wmux",
4
+ "version": "1.0.0",
5
+ "description": "AI Agent Terminal for Windows - Run Claude Code, Codex, Gemini CLI in parallel",
6
+ "main": ".vite/build/index.js",
7
+ "scripts": {
8
+ "start": "npm run build:mcp && electron-forge start",
9
+ "package": "npm run build:mcp && electron-forge package",
10
+ "make": "npm run build:mcp && electron-forge make",
11
+ "publish": "electron-forge publish",
12
+ "lint": "eslint --ext .ts,.tsx .",
13
+ "build:cli": "tsc -p tsconfig.cli.json",
14
+ "build:mcp": "tsc -p tsconfig.mcp.json",
15
+ "cli": "node dist/cli/cli/index.js",
16
+ "mcp": "node dist/mcp/mcp/index.js"
17
+ },
18
+ "bin": {
19
+ "wmux": "dist/cli/cli/index.js",
20
+ "wmux-mcp": "dist/mcp/mcp/index.js"
21
+ },
22
+ "keywords": ["terminal", "multiplexer", "electron", "ai", "claude", "agent", "windows", "pty", "mcp"],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/openwong2kim/wmux.git"
26
+ },
27
+ "author": {
28
+ "name": "openwong2kim",
29
+ "email": "100856670+openwong2kim@users.noreply.github.com"
30
+ },
31
+ "license": "MIT",
32
+ "files": [
33
+ "dist/cli",
34
+ "dist/mcp",
35
+ "src",
36
+ "assets",
37
+ "index.html",
38
+ "forge.config.ts",
39
+ "vite.*.config.ts",
40
+ "tsconfig*.json",
41
+ "tailwind.config.js",
42
+ "postcss.config.js"
43
+ ],
44
+ "devDependencies": {
45
+ "@electron-forge/cli": "^7.11.1",
46
+ "@electron-forge/maker-deb": "^7.11.1",
47
+ "@electron-forge/maker-rpm": "^7.11.1",
48
+ "@electron-forge/maker-squirrel": "^7.11.1",
49
+ "@electron-forge/maker-zip": "^7.11.1",
50
+ "@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
51
+ "@electron-forge/plugin-fuses": "^7.11.1",
52
+ "@electron-forge/plugin-vite": "^7.11.1",
53
+ "@electron/fuses": "^1.8.0",
54
+ "@types/electron-squirrel-startup": "^1.0.2",
55
+ "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
57
+ "@typescript-eslint/eslint-plugin": "^5.62.0",
58
+ "@typescript-eslint/parser": "^5.62.0",
59
+ "@vitejs/plugin-react": "^4.7.0",
60
+ "autoprefixer": "^10.4.27",
61
+ "electron": "41.0.3",
62
+ "eslint": "^8.57.1",
63
+ "eslint-plugin-import": "^2.32.0",
64
+ "postcss": "^8.5.8",
65
+ "tailwindcss": "^3.4.19",
66
+ "typescript": "^5.9.3",
67
+ "vite": "^5.4.21"
68
+ },
69
+ "dependencies": {
70
+ "@modelcontextprotocol/sdk": "^1.27.1",
71
+ "@xterm/addon-fit": "^0.11.0",
72
+ "@xterm/addon-search": "^0.16.0",
73
+ "@xterm/addon-webgl": "^0.19.0",
74
+ "@xterm/xterm": "^6.0.0",
75
+ "electron-squirrel-startup": "^1.0.1",
76
+ "immer": "^11.1.4",
77
+ "node-pty": "^1.1.0",
78
+ "react": "^19.2.4",
79
+ "react-dom": "^19.2.4",
80
+ "react-resizable-panels": "^4.7.3",
81
+ "zod": "^4.3.6",
82
+ "zustand": "^5.0.12"
83
+ }
84
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import * as net from 'net';
3
+ import * as crypto from 'crypto';
4
+ import type { RpcRequest, RpcResponse, RpcMethod } from '../shared/rpc';
5
+
6
+ const PIPE_NAME = process.env.WMUX_SOCKET_PATH || '\\\\.\\pipe\\wmux';
7
+ const TIMEOUT_MS = 5000;
8
+
9
+ export function sendRequest(
10
+ method: RpcMethod,
11
+ params: Record<string, unknown> = {}
12
+ ): Promise<RpcResponse> {
13
+ return new Promise((resolve, reject) => {
14
+ const id = crypto.randomUUID();
15
+ const token = process.env.WMUX_AUTH_TOKEN;
16
+ const request: RpcRequest = { id, method, params, token };
17
+
18
+ const socket = net.connect(PIPE_NAME);
19
+ let buffer = '';
20
+ let settled = false;
21
+
22
+ const timer = setTimeout(() => {
23
+ if (!settled) {
24
+ settled = true;
25
+ socket.destroy();
26
+ reject(new Error('Request timed out after 5 seconds.'));
27
+ }
28
+ }, TIMEOUT_MS);
29
+
30
+ socket.on('connect', () => {
31
+ socket.write(JSON.stringify(request) + '\n');
32
+ });
33
+
34
+ socket.on('data', (chunk: Buffer) => {
35
+ buffer += chunk.toString('utf8');
36
+ const lines = buffer.split('\n');
37
+ buffer = lines.pop() ?? '';
38
+
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed) continue;
42
+ try {
43
+ const response = JSON.parse(trimmed) as RpcResponse;
44
+ if (response.id === id && !settled) {
45
+ settled = true;
46
+ clearTimeout(timer);
47
+ socket.destroy();
48
+ resolve(response);
49
+ }
50
+ } catch {
51
+ // ignore malformed lines
52
+ }
53
+ }
54
+ });
55
+
56
+ socket.on('error', (err: NodeJS.ErrnoException) => {
57
+ if (!settled) {
58
+ settled = true;
59
+ clearTimeout(timer);
60
+ if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
61
+ reject(new Error('wmux is not running. Start the app first.'));
62
+ } else {
63
+ reject(new Error(`Connection error: ${err.message}`));
64
+ }
65
+ }
66
+ });
67
+
68
+ socket.on('close', () => {
69
+ if (!settled) {
70
+ settled = true;
71
+ clearTimeout(timer);
72
+ reject(new Error('Connection closed before response was received.'));
73
+ }
74
+ });
75
+ });
76
+ }