@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,155 @@
1
+ export const zh = {
2
+ // Sidebar
3
+ 'sidebar.workspaces': '工作区',
4
+ 'sidebar.newWorkspace': '新建工作区',
5
+ 'sidebar.newWorkspaceTooltip': '新建工作区 (Ctrl+N)',
6
+ 'sidebar.hideTooltip': '隐藏侧边栏 (Ctrl+B)',
7
+ 'sidebar.expandTooltip': '展开侧边栏 (Ctrl+B)',
8
+ 'sidebar.unreadCount': '{count} 条未读',
9
+
10
+ // Workspace
11
+ 'workspace.agentRunning': '智能体运行中',
12
+ 'workspace.agentComplete': '智能体已完成',
13
+ 'workspace.agentError': '智能体错误',
14
+ 'workspace.agentWaiting': '智能体等待中',
15
+ 'workspace.agentIdle': '智能体空闲',
16
+ 'workspace.close': '关闭工作区',
17
+
18
+ // Pane
19
+ 'pane.empty': '空面板',
20
+ 'pane.splitRight': '向右分割',
21
+ 'pane.splitDown': '向下分割',
22
+
23
+ // Surface
24
+ 'surface.terminal': '终端',
25
+ 'surface.closeTab': '关闭标签',
26
+
27
+ // Search
28
+ 'search.placeholder': '搜索...',
29
+ 'search.prevTooltip': '上一个结果 (Shift+Enter)',
30
+ 'search.nextTooltip': '下一个结果 (Enter)',
31
+ 'search.closeTooltip': '关闭 (ESC)',
32
+
33
+ // Notification
34
+ 'notification.title': '通知',
35
+ 'notification.markAllRead': '全部已读',
36
+ 'notification.clear': '清除',
37
+ 'notification.empty': '暂无通知',
38
+ 'notification.toggle': 'Ctrl+I 切换',
39
+
40
+ // Command palette
41
+ 'palette.placeholder': '输入命令...',
42
+ 'palette.noResults': '未找到结果:',
43
+ 'palette.navigate': '导航',
44
+ 'palette.select': '选择',
45
+ 'palette.close': '关闭',
46
+ 'palette.cmd.toggleSidebar': '切换侧边栏',
47
+ 'palette.cmd.newWorkspace': '新建工作区',
48
+ 'palette.cmd.newSurface': '新建界面',
49
+ 'palette.cmd.splitRight': '向右分割',
50
+ 'palette.cmd.splitDown': '向下分割',
51
+ 'palette.cmd.showNotifications': '显示通知',
52
+ 'palette.cmd.openSettings': '打开设置',
53
+ 'palette.cmd.openBrowser': '打开浏览器',
54
+ 'palette.catWorkspace': '工作区',
55
+ 'palette.catSurface': '界面',
56
+ 'palette.catCommand': '命令',
57
+
58
+ // Terminal
59
+ 'terminal.exited': '进程已退出,代码 {code}',
60
+ 'terminal.exitedBracket': '[进程已退出,代码 {code}]',
61
+ 'terminal.copied': '已复制!',
62
+
63
+ // Browser
64
+ 'browser.urlPlaceholder': '输入URL...',
65
+ 'browser.back': '后退',
66
+ 'browser.forward': '前进',
67
+ 'browser.reload': '刷新',
68
+ 'browser.close': '关闭',
69
+ 'browser.devToolsTooltip': '打开开发者工具 (F12)',
70
+ 'browser.title': '浏览器',
71
+
72
+ // VI copy mode
73
+ 'viCopy.mode': '-- 复制模式 --',
74
+ 'viCopy.visual': '-- 可视 --',
75
+
76
+ // StatusBar
77
+ 'statusBar.company': '公司',
78
+ 'statusBar.session': '会话: {min}分钟',
79
+ 'statusBar.settingsTooltip': '设置 (Ctrl+,)',
80
+
81
+ // Settings
82
+ 'settings.title': '设置',
83
+ 'settings.language': '语言',
84
+ 'settings.sound': '通知声音',
85
+ 'settings.soundOn': '开启',
86
+ 'settings.soundOff': '关闭',
87
+ 'settings.checkUpdate': '检查更新',
88
+ 'settings.checking': '检查中...',
89
+ 'settings.upToDate': '已是最新版本',
90
+ 'settings.updateAvailable': '有可用更新',
91
+ 'settings.close': '关闭',
92
+ 'settings.shortcuts': '键盘快捷键',
93
+ 'settings.tabGeneral': '常规',
94
+ 'settings.tabAppearance': '外观',
95
+ 'settings.tabNotifications': '通知',
96
+ 'settings.tabShortcuts': '快捷键',
97
+ 'settings.tabAbout': '关于',
98
+ 'settings.terminal': '终端',
99
+ 'settings.defaultShell': '默认 Shell',
100
+ 'settings.scrollbackLines': '回滚行数',
101
+ 'settings.scrollbackDesc': '终端缓冲区保留行数',
102
+ 'settings.updates': '更新',
103
+ 'settings.wmuxUpdates': 'wmux 更新',
104
+ 'settings.updateFailed': '检查更新失败',
105
+ 'settings.lastCheckedNever': '上次检查:从未',
106
+ 'settings.installUpdate': '安装更新',
107
+ 'settings.retryCheck': '重新检查',
108
+ 'settings.fontSize': '字体大小',
109
+ 'settings.fontSizeRange': '范围 12~24',
110
+ 'settings.fontFamily': '字体族',
111
+ 'settings.fontFamilyDesc': '终端等宽字体',
112
+ 'settings.layout': '布局',
113
+ 'settings.sidebarPosition': '侧边栏位置',
114
+ 'settings.sidebarPositionDesc': '终端区域的左侧或右侧',
115
+ 'settings.sidebarLeft': '左',
116
+ 'settings.sidebarRight': '右',
117
+ 'settings.updateReady': '更新已准备就绪',
118
+ 'settings.checkFailed': '检查失败',
119
+ 'settings.unknownError': '未知错误',
120
+ 'settings.notificationBehavior': '通知行为',
121
+ 'settings.soundDesc': 'Web Audio API — 无需外部文件',
122
+ 'settings.toast': '弹出通知',
123
+ 'settings.toastDesc': '代理完成时显示弹出通知',
124
+ 'settings.ring': '光环动画',
125
+ 'settings.ringDesc': '未读通知窗格的脉冲边框',
126
+ 'settings.sc.toggleSidebar': '切换侧边栏',
127
+ 'settings.sc.splitHorizontal': '水平分割面板',
128
+ 'settings.sc.splitVertical': '垂直分割面板',
129
+ 'settings.sc.newWorkspace': '新建工作区',
130
+ 'settings.sc.closePane': '关闭面板/工作区',
131
+ 'settings.sc.searchTerminal': '在终端中搜索',
132
+ 'settings.sc.commandPalette': '命令面板',
133
+ 'settings.sc.toggleNotifications': '切换通知面板',
134
+ 'settings.sc.viCopyMode': 'Vi 复制模式',
135
+ 'settings.sc.renameWorkspace': '重命名工作区',
136
+ 'settings.sc.highlightPane': '高亮活动面板',
137
+ 'settings.shortcutsNotAvailable': '快捷键自定义暂不可用。',
138
+ 'settings.aboutTagline': 'Windows 原生 AI 代理终端',
139
+ 'settings.builtWith': '构建工具',
140
+ 'settings.links': '链接',
141
+ 'settings.githubRepo': 'GitHub 仓库',
142
+ 'settings.toggleHint': 'Ctrl+, 切换',
143
+
144
+ // Custom keybindings
145
+ 'settings.customKeybindings': '自定义快捷键',
146
+ 'settings.kb.add': '添加快捷键',
147
+ 'settings.kb.key': '按键',
148
+ 'settings.kb.label': '名称',
149
+ 'settings.kb.command': '命令',
150
+ 'settings.kb.sendEnter': '发送回车',
151
+ 'settings.kb.pressKey': '请按下按键...',
152
+ 'settings.kb.conflict': '与内置快捷键冲突',
153
+ 'settings.kb.delete': '删除',
154
+ 'settings.kb.noBindings': '暂无自定义快捷键',
155
+ } as const;
@@ -0,0 +1,6 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import App from './App';
3
+ import './styles/globals.css';
4
+
5
+ const root = createRoot(document.getElementById('root')!);
6
+ root.render(<App />);
@@ -0,0 +1,19 @@
1
+ import { create } from 'zustand';
2
+ import { immer } from 'zustand/middleware/immer';
3
+ import { createWorkspaceSlice, type WorkspaceSlice } from './slices/workspaceSlice';
4
+ import { createPaneSlice, type PaneSlice } from './slices/paneSlice';
5
+ import { createSurfaceSlice, type SurfaceSlice } from './slices/surfaceSlice';
6
+ import { createUISlice, type UISlice } from './slices/uiSlice';
7
+ import { createNotificationSlice, type NotificationSlice } from './slices/notificationSlice';
8
+
9
+ export type StoreState = WorkspaceSlice & PaneSlice & SurfaceSlice & UISlice & NotificationSlice;
10
+
11
+ export const useStore = create<StoreState>()(
12
+ immer((...args) => ({
13
+ ...createWorkspaceSlice(...args),
14
+ ...createPaneSlice(...args),
15
+ ...createSurfaceSlice(...args),
16
+ ...createUISlice(...args),
17
+ ...createNotificationSlice(...args),
18
+ }))
19
+ );
@@ -0,0 +1,56 @@
1
+ import type { StateCreator } from 'zustand';
2
+ import type { StoreState } from '../index';
3
+ import type { Notification } from '../../../shared/types';
4
+ import { generateId } from '../../../shared/types';
5
+
6
+ export interface NotificationSlice {
7
+ notifications: Notification[];
8
+ addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void;
9
+ markRead: (id: string) => void;
10
+ markAllReadForWorkspace: (workspaceId: string) => void;
11
+ clearNotifications: () => void;
12
+ }
13
+
14
+ export const createNotificationSlice: StateCreator<StoreState, [['zustand/immer', never]], [], NotificationSlice> = (set) => ({
15
+ notifications: [],
16
+
17
+ addNotification: (notification) => set((state: StoreState) => {
18
+ state.notifications.push({
19
+ ...notification,
20
+ id: generateId('notif'),
21
+ timestamp: Date.now(),
22
+ read: false,
23
+ });
24
+ // 500개 초과 시 읽은 오래된 알림 제거
25
+ if (state.notifications.length > 500) {
26
+ const readOld = state.notifications.findIndex((n) => n.read);
27
+ if (readOld !== -1) {
28
+ state.notifications.splice(readOld, 1);
29
+ } else {
30
+ // 모두 unread면 가장 오래된 것 제거
31
+ state.notifications.shift();
32
+ }
33
+ }
34
+ // Update workspace metadata lastNotification
35
+ const ws = state.workspaces.find((w) => w.id === notification.workspaceId);
36
+ if (ws) {
37
+ if (!ws.metadata) ws.metadata = {};
38
+ ws.metadata.lastNotification = Date.now();
39
+ }
40
+ }),
41
+
42
+ markRead: (id) => set((state: StoreState) => {
43
+ const notif = state.notifications.find((n) => n.id === id);
44
+ if (notif) notif.read = true;
45
+ }),
46
+
47
+ markAllReadForWorkspace: (workspaceId) => set((state: StoreState) => {
48
+ for (const n of state.notifications) {
49
+ if (n.workspaceId === workspaceId) n.read = true;
50
+ }
51
+ }),
52
+
53
+ clearNotifications: () => set((state: StoreState) => {
54
+ state.notifications = [];
55
+ }),
56
+ });
@@ -0,0 +1,141 @@
1
+ import type { StateCreator } from 'zustand';
2
+ import type { StoreState } from '../index';
3
+ import type { Pane, PaneLeaf, PaneBranch, Workspace } from '../../../shared/types';
4
+ import { createLeafPane, generateId } from '../../../shared/types';
5
+
6
+ export interface PaneSlice {
7
+ splitPane: (paneId: string, direction: 'horizontal' | 'vertical') => void;
8
+ closePane: (paneId: string) => void;
9
+ setActivePane: (paneId: string) => void;
10
+ focusPaneDirection: (direction: 'up' | 'down' | 'left' | 'right') => void;
11
+ }
12
+
13
+ function findPane(root: Pane, id: string): Pane | null {
14
+ if (root.id === id) return root;
15
+ if (root.type === 'branch') {
16
+ for (const child of root.children) {
17
+ const found = findPane(child, id);
18
+ if (found) return found;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ function findParent(root: Pane, id: string): PaneBranch | null {
25
+ if (root.type === 'branch') {
26
+ for (const child of root.children) {
27
+ if (child.id === id) return root;
28
+ const found = findParent(child, id);
29
+ if (found) return found;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function collectLeafIds(pane: Pane): string[] {
36
+ if (pane.type === 'leaf') return [pane.id];
37
+ return pane.children.flatMap(collectLeafIds);
38
+ }
39
+
40
+ function getLeafPanes(root: Pane): PaneLeaf[] {
41
+ if (root.type === 'leaf') return [root];
42
+ return root.children.flatMap(getLeafPanes);
43
+ }
44
+
45
+ export const createPaneSlice: StateCreator<StoreState, [['zustand/immer', never]], [], PaneSlice> = (set, get) => ({
46
+ splitPane: (paneId, direction) => set((state: StoreState) => {
47
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
48
+ if (!ws) return;
49
+
50
+ const targetPane = findPane(ws.rootPane, paneId);
51
+ if (!targetPane || targetPane.type !== 'leaf') return;
52
+
53
+ const newPane = createLeafPane();
54
+ const branch: PaneBranch = {
55
+ id: generateId('pane'),
56
+ type: 'branch',
57
+ direction,
58
+ children: [{ ...targetPane }, newPane],
59
+ sizes: [50, 50],
60
+ };
61
+
62
+ // Replace target with branch
63
+ const parent = findParent(ws.rootPane, paneId);
64
+ if (parent) {
65
+ const idx = parent.children.findIndex((c) => c.id === paneId);
66
+ if (idx !== -1) {
67
+ parent.children[idx] = branch;
68
+ }
69
+ } else {
70
+ // Target is the root
71
+ ws.rootPane = branch;
72
+ }
73
+
74
+ ws.activePaneId = newPane.id;
75
+ }),
76
+
77
+ closePane: (paneId) => set((state: StoreState) => {
78
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
79
+ if (!ws) return;
80
+
81
+ const parent = findParent(ws.rootPane, paneId);
82
+ if (!parent) {
83
+ // Can't close root pane, but can clear its surfaces
84
+ return;
85
+ }
86
+
87
+ const idx = parent.children.findIndex((c) => c.id === paneId);
88
+ if (idx === -1) return;
89
+
90
+ parent.children.splice(idx, 1);
91
+
92
+ if (parent.children.length === 1) {
93
+ // Collapse: replace parent with the remaining child
94
+ const remaining = parent.children[0];
95
+ const grandParent = findParent(ws.rootPane, parent.id);
96
+ if (grandParent) {
97
+ const parentIdx = grandParent.children.findIndex((c) => c.id === parent.id);
98
+ if (parentIdx !== -1) {
99
+ grandParent.children[parentIdx] = remaining;
100
+ }
101
+ } else {
102
+ // Parent was root
103
+ ws.rootPane = remaining;
104
+ }
105
+ }
106
+
107
+ // Update active pane
108
+ const leaves = getLeafPanes(ws.rootPane);
109
+ if (leaves.length > 0 && !leaves.some((l) => l.id === ws.activePaneId)) {
110
+ ws.activePaneId = leaves[0].id;
111
+ }
112
+ }),
113
+
114
+ setActivePane: (paneId) => set((state: StoreState) => {
115
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
116
+ if (!ws) return;
117
+ if (findPane(ws.rootPane, paneId)) {
118
+ ws.activePaneId = paneId;
119
+ }
120
+ }),
121
+
122
+ focusPaneDirection: (_direction) => set((state: StoreState) => {
123
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
124
+ if (!ws) return;
125
+
126
+ const leaves = getLeafPanes(ws.rootPane);
127
+ if (leaves.length <= 1) return;
128
+
129
+ const currentIdx = leaves.findIndex((l) => l.id === ws.activePaneId);
130
+ if (currentIdx === -1) return;
131
+
132
+ // Simple round-robin navigation for now
133
+ let nextIdx: number;
134
+ if (_direction === 'right' || _direction === 'down') {
135
+ nextIdx = (currentIdx + 1) % leaves.length;
136
+ } else {
137
+ nextIdx = (currentIdx - 1 + leaves.length) % leaves.length;
138
+ }
139
+ ws.activePaneId = leaves[nextIdx].id;
140
+ }),
141
+ });
@@ -0,0 +1,122 @@
1
+ import type { StateCreator } from 'zustand';
2
+ import type { StoreState } from '../index';
3
+ import type { Pane, PaneLeaf, Surface, Workspace } from '../../../shared/types';
4
+ import { createSurface, generateId } from '../../../shared/types';
5
+
6
+ export interface SurfaceSlice {
7
+ addSurface: (paneId: string, ptyId: string, shell: string, cwd: string) => void;
8
+ addBrowserSurface: (paneId: string, url?: string) => void;
9
+ closeSurface: (paneId: string, surfaceId: string) => void;
10
+ setActiveSurface: (paneId: string, surfaceId: string) => void;
11
+ nextSurface: (paneId: string) => void;
12
+ prevSurface: (paneId: string) => void;
13
+ updateSurfacePtyId: (paneId: string, surfaceId: string, ptyId: string) => void;
14
+ updateSurfaceTitle: (surfaceId: string, title: string) => void;
15
+ }
16
+
17
+ function findLeafPane(root: Pane, id: string): PaneLeaf | null {
18
+ if (root.id === id && root.type === 'leaf') return root;
19
+ if (root.type === 'branch') {
20
+ for (const child of root.children) {
21
+ const found = findLeafPane(child, id);
22
+ if (found) return found;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export const createSurfaceSlice: StateCreator<StoreState, [['zustand/immer', never]], [], SurfaceSlice> = (set) => ({
29
+ addSurface: (paneId, ptyId, shell, cwd) => set((state: StoreState) => {
30
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
31
+ if (!ws) return;
32
+ const pane = findLeafPane(ws.rootPane, paneId);
33
+ if (!pane) return;
34
+ const surface = createSurface(ptyId, shell, cwd);
35
+ pane.surfaces.push(surface);
36
+ pane.activeSurfaceId = surface.id;
37
+ }),
38
+
39
+ addBrowserSurface: (paneId, url) => set((state: StoreState) => {
40
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
41
+ if (!ws) return;
42
+ const pane = findLeafPane(ws.rootPane, paneId);
43
+ if (!pane) return;
44
+ const surface: Surface = {
45
+ id: generateId('surface'),
46
+ ptyId: '',
47
+ title: 'Browser',
48
+ shell: '',
49
+ cwd: '',
50
+ surfaceType: 'browser',
51
+ browserUrl: url || 'https://google.com',
52
+ };
53
+ pane.surfaces.push(surface);
54
+ pane.activeSurfaceId = surface.id;
55
+ }),
56
+
57
+ closeSurface: (paneId, surfaceId) => set((state: StoreState) => {
58
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
59
+ if (!ws) return;
60
+ const pane = findLeafPane(ws.rootPane, paneId);
61
+ if (!pane) return;
62
+
63
+ const idx = pane.surfaces.findIndex((s) => s.id === surfaceId);
64
+ if (idx === -1) return;
65
+
66
+ pane.surfaces.splice(idx, 1);
67
+ if (pane.activeSurfaceId === surfaceId) {
68
+ pane.activeSurfaceId = pane.surfaces[Math.min(idx, pane.surfaces.length - 1)]?.id || '';
69
+ }
70
+ }),
71
+
72
+ setActiveSurface: (paneId, surfaceId) => set((state: StoreState) => {
73
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
74
+ if (!ws) return;
75
+ const pane = findLeafPane(ws.rootPane, paneId);
76
+ if (!pane) return;
77
+ if (pane.surfaces.some((s) => s.id === surfaceId)) {
78
+ pane.activeSurfaceId = surfaceId;
79
+ }
80
+ }),
81
+
82
+ nextSurface: (paneId) => set((state: StoreState) => {
83
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
84
+ if (!ws) return;
85
+ const pane = findLeafPane(ws.rootPane, paneId);
86
+ if (!pane || pane.surfaces.length <= 1) return;
87
+ const idx = pane.surfaces.findIndex((s) => s.id === pane.activeSurfaceId);
88
+ pane.activeSurfaceId = pane.surfaces[(idx + 1) % pane.surfaces.length].id;
89
+ }),
90
+
91
+ prevSurface: (paneId) => set((state: StoreState) => {
92
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
93
+ if (!ws) return;
94
+ const pane = findLeafPane(ws.rootPane, paneId);
95
+ if (!pane || pane.surfaces.length <= 1) return;
96
+ const idx = pane.surfaces.findIndex((s) => s.id === pane.activeSurfaceId);
97
+ pane.activeSurfaceId = pane.surfaces[(idx - 1 + pane.surfaces.length) % pane.surfaces.length].id;
98
+ }),
99
+
100
+ updateSurfacePtyId: (paneId, surfaceId, ptyId) => set((state: StoreState) => {
101
+ const ws = state.workspaces.find((w: Workspace) => w.id === state.activeWorkspaceId);
102
+ if (!ws) return;
103
+ const pane = findLeafPane(ws.rootPane, paneId);
104
+ if (!pane) return;
105
+ const surface = pane.surfaces.find((s) => s.id === surfaceId);
106
+ if (surface) surface.ptyId = ptyId;
107
+ }),
108
+
109
+ updateSurfaceTitle: (surfaceId, title) => set((state: StoreState) => {
110
+ for (const ws of state.workspaces) {
111
+ const updateInPane = (pane: Pane): boolean => {
112
+ if (pane.type === 'leaf') {
113
+ const surface = pane.surfaces.find((s) => s.id === surfaceId);
114
+ if (surface) { surface.title = title; return true; }
115
+ return false;
116
+ }
117
+ return pane.children.some(updateInPane);
118
+ };
119
+ if (updateInPane(ws.rootPane)) return;
120
+ }
121
+ }),
122
+ });