@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,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,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
|
+
});
|