@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,253 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useT } from '../../hooks/useT';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// SVG Icon components
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function IconBack() {
|
|
9
|
+
return (
|
|
10
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
11
|
+
<polyline points="9,2 4,7 9,12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function IconForward() {
|
|
17
|
+
return (
|
|
18
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
19
|
+
<polyline points="5,2 10,7 5,12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function IconRefresh() {
|
|
25
|
+
return (
|
|
26
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<path d="M12 7A5 5 0 1 1 7 2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
|
28
|
+
<polyline points="7,0.5 9.5,2.5 7,4.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function IconDevTools() {
|
|
34
|
+
return (
|
|
35
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
36
|
+
<rect x="1" y="1" width="12" height="12" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
|
|
37
|
+
<line x1="1" y1="4.5" x2="13" y2="4.5" stroke="currentColor" strokeWidth="1.2" />
|
|
38
|
+
<polyline points="3.5,7 5.5,9 3.5,11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
39
|
+
<line x1="7" y1="11" x2="10.5" y2="11" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function IconClose() {
|
|
45
|
+
return (
|
|
46
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
47
|
+
<line x1="2" y1="2" x2="10" y2="10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
48
|
+
<line x1="10" y1="2" x2="2" y2="10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function IconLock() {
|
|
54
|
+
return (
|
|
55
|
+
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
56
|
+
<rect x="1.5" y="4.5" width="8" height="5.5" rx="1" stroke="currentColor" strokeWidth="1.2" />
|
|
57
|
+
<path d="M3.5 4.5V3a2 2 0 0 1 4 0v1.5" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// BrowserToolbar props
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
interface BrowserToolbarProps {
|
|
67
|
+
currentUrl: string;
|
|
68
|
+
isLoading: boolean;
|
|
69
|
+
canGoBack: boolean;
|
|
70
|
+
canGoForward: boolean;
|
|
71
|
+
isActive: boolean;
|
|
72
|
+
onNavigate: (url: string) => void;
|
|
73
|
+
onBack: () => void;
|
|
74
|
+
onForward: () => void;
|
|
75
|
+
onRefresh: () => void;
|
|
76
|
+
onOpenDevTools: () => void;
|
|
77
|
+
onClose: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Component
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export default function BrowserToolbar({
|
|
85
|
+
currentUrl,
|
|
86
|
+
isLoading,
|
|
87
|
+
canGoBack,
|
|
88
|
+
canGoForward,
|
|
89
|
+
isActive,
|
|
90
|
+
onNavigate,
|
|
91
|
+
onBack,
|
|
92
|
+
onForward,
|
|
93
|
+
onRefresh,
|
|
94
|
+
onOpenDevTools,
|
|
95
|
+
onClose,
|
|
96
|
+
}: BrowserToolbarProps) {
|
|
97
|
+
const t = useT();
|
|
98
|
+
const [inputValue, setInputValue] = useState(currentUrl);
|
|
99
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
100
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
101
|
+
|
|
102
|
+
// Sync display URL when not focused
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!isFocused) {
|
|
105
|
+
setInputValue(currentUrl);
|
|
106
|
+
}
|
|
107
|
+
}, [currentUrl, isFocused]);
|
|
108
|
+
|
|
109
|
+
// Ctrl+L focuses the URL bar â only register when this browser panel is active
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!isActive) return;
|
|
112
|
+
const handler = (e: KeyboardEvent) => {
|
|
113
|
+
if (e.ctrlKey && !e.shiftKey && !e.altKey && e.key === 'l') {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
inputRef.current?.focus();
|
|
116
|
+
inputRef.current?.select();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('keydown', handler);
|
|
120
|
+
return () => window.removeEventListener('keydown', handler);
|
|
121
|
+
}, [isActive]);
|
|
122
|
+
|
|
123
|
+
const handleSubmit = useCallback((e: React.FormEvent) => {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
const raw = inputValue.trim();
|
|
126
|
+
if (!raw) return;
|
|
127
|
+
// Normalize: add protocol if missing
|
|
128
|
+
let url = raw;
|
|
129
|
+
if (!/^https?:\/\//i.test(url) && !/^about:/i.test(url)) {
|
|
130
|
+
// If it looks like a domain, add https://; otherwise treat as search
|
|
131
|
+
if (/^[\w-]+(\.[\w-]+)+([\/?#].*)?$/.test(url)) {
|
|
132
|
+
url = `https://${url}`;
|
|
133
|
+
} else {
|
|
134
|
+
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
setInputValue(url);
|
|
138
|
+
onNavigate(url);
|
|
139
|
+
inputRef.current?.blur();
|
|
140
|
+
}, [inputValue, onNavigate]);
|
|
141
|
+
|
|
142
|
+
const isSecure = currentUrl.startsWith('https://');
|
|
143
|
+
|
|
144
|
+
const btnBase = 'flex items-center justify-center w-6 h-6 rounded transition-colors duration-100';
|
|
145
|
+
const btnEnabled = `${btnBase} text-[var(--text-sub2)] hover:text-[var(--text-main)] hover:bg-[var(--bg-surface)] cursor-pointer`;
|
|
146
|
+
const btnDisabled = `${btnBase} text-[var(--bg-overlay)] cursor-default`;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
className="flex items-center gap-1.5 px-2 py-1.5 shrink-0"
|
|
151
|
+
style={{ backgroundColor: 'var(--bg-mantle)', borderBottom: '1px solid var(--bg-surface)' }}
|
|
152
|
+
>
|
|
153
|
+
{/* Back */}
|
|
154
|
+
<button
|
|
155
|
+
className={canGoBack ? btnEnabled : btnDisabled}
|
|
156
|
+
onClick={canGoBack ? onBack : undefined}
|
|
157
|
+
title={t('browser.back')}
|
|
158
|
+
tabIndex={-1}
|
|
159
|
+
>
|
|
160
|
+
<IconBack />
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
{/* Forward */}
|
|
164
|
+
<button
|
|
165
|
+
className={canGoForward ? btnEnabled : btnDisabled}
|
|
166
|
+
onClick={canGoForward ? onForward : undefined}
|
|
167
|
+
title={t('browser.forward')}
|
|
168
|
+
tabIndex={-1}
|
|
169
|
+
>
|
|
170
|
+
<IconForward />
|
|
171
|
+
</button>
|
|
172
|
+
|
|
173
|
+
{/* Refresh */}
|
|
174
|
+
<button
|
|
175
|
+
className={btnEnabled}
|
|
176
|
+
onClick={onRefresh}
|
|
177
|
+
title={t('browser.reload')}
|
|
178
|
+
tabIndex={-1}
|
|
179
|
+
>
|
|
180
|
+
<span className={isLoading ? 'animate-spin' : ''}>
|
|
181
|
+
<IconRefresh />
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
|
|
185
|
+
{/* URL bar */}
|
|
186
|
+
<form className="flex-1 min-w-0" onSubmit={handleSubmit}>
|
|
187
|
+
<div
|
|
188
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-md"
|
|
189
|
+
style={{
|
|
190
|
+
backgroundColor: isFocused ? 'var(--bg-base)' : '#11111b',
|
|
191
|
+
border: `1px solid ${isFocused ? 'var(--accent-blue)' : 'var(--bg-surface)'}`,
|
|
192
|
+
transition: 'border-color 0.15s',
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
{/* Lock icon */}
|
|
196
|
+
<span className={isSecure ? 'text-[var(--accent-green)]' : 'text-[var(--text-muted)]'} style={{ flexShrink: 0 }}>
|
|
197
|
+
<IconLock />
|
|
198
|
+
</span>
|
|
199
|
+
|
|
200
|
+
{/* Loading indicator */}
|
|
201
|
+
{isLoading && (
|
|
202
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--accent-blue)] animate-pulse shrink-0" />
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<input
|
|
206
|
+
ref={inputRef}
|
|
207
|
+
type="text"
|
|
208
|
+
value={inputValue}
|
|
209
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
210
|
+
onFocus={() => {
|
|
211
|
+
setIsFocused(true);
|
|
212
|
+
inputRef.current?.select();
|
|
213
|
+
}}
|
|
214
|
+
onBlur={() => {
|
|
215
|
+
setIsFocused(false);
|
|
216
|
+
setInputValue(currentUrl);
|
|
217
|
+
}}
|
|
218
|
+
onKeyDown={(e) => {
|
|
219
|
+
if (e.key === 'Escape') {
|
|
220
|
+
setInputValue(currentUrl);
|
|
221
|
+
inputRef.current?.blur();
|
|
222
|
+
}
|
|
223
|
+
}}
|
|
224
|
+
className="flex-1 min-w-0 bg-transparent text-[var(--text-main)] text-xs outline-none"
|
|
225
|
+
style={{ fontFamily: 'ui-monospace, monospace' }}
|
|
226
|
+
spellCheck={false}
|
|
227
|
+
autoComplete="off"
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
</form>
|
|
231
|
+
|
|
232
|
+
{/* DevTools */}
|
|
233
|
+
<button
|
|
234
|
+
className={btnEnabled}
|
|
235
|
+
onClick={onOpenDevTools}
|
|
236
|
+
title={t('browser.devToolsTooltip')}
|
|
237
|
+
tabIndex={-1}
|
|
238
|
+
>
|
|
239
|
+
<IconDevTools />
|
|
240
|
+
</button>
|
|
241
|
+
|
|
242
|
+
{/* Close */}
|
|
243
|
+
<button
|
|
244
|
+
className={`${btnBase} text-[var(--text-sub2)] hover:text-[var(--accent-red)] hover:bg-[#3b1e1e] cursor-pointer`}
|
|
245
|
+
onClick={onClose}
|
|
246
|
+
title={t('browser.close')}
|
|
247
|
+
tabIndex={-1}
|
|
248
|
+
>
|
|
249
|
+
<IconClose />
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { useStore } from '../../stores';
|
|
3
|
+
import Sidebar from '../Sidebar/Sidebar';
|
|
4
|
+
import MiniSidebar from '../Sidebar/MiniSidebar';
|
|
5
|
+
import PaneContainer from '../Pane/PaneContainer';
|
|
6
|
+
import StatusBar from '../StatusBar/StatusBar';
|
|
7
|
+
import NotificationPanel from '../Notification/NotificationPanel';
|
|
8
|
+
import CommandPalette from '../Palette/CommandPalette';
|
|
9
|
+
import SettingsPanel from '../Settings/SettingsPanel';
|
|
10
|
+
import ApprovalDialog from '../Company/ApprovalDialog';
|
|
11
|
+
import CompanyView from '../Company/CompanyView';
|
|
12
|
+
import MessageFeedPanel from '../Company/MessageFeedPanel';
|
|
13
|
+
import { useKeyboard } from '../../hooks/useKeyboard';
|
|
14
|
+
import { useNotificationListener } from '../../hooks/useNotificationListener';
|
|
15
|
+
import { useRpcBridge } from '../../hooks/useRpcBridge';
|
|
16
|
+
import type { SessionData, PaneLeaf } from '../../../shared/types';
|
|
17
|
+
|
|
18
|
+
export default function AppLayout() {
|
|
19
|
+
const sidebarVisible = useStore((s) => s.sidebarVisible);
|
|
20
|
+
const sidebarPosition = useStore((s) => s.sidebarPosition);
|
|
21
|
+
const companyViewVisible = useStore((s) => s.companyViewVisible);
|
|
22
|
+
const setCompanyViewVisible = useStore((s) => s.setCompanyViewVisible);
|
|
23
|
+
const activeWorkspaceId = useStore((s) => s.activeWorkspaceId);
|
|
24
|
+
const workspaces = useStore((s) => s.workspaces);
|
|
25
|
+
const addSurface = useStore((s) => s.addSurface);
|
|
26
|
+
|
|
27
|
+
const activeWorkspace = workspaces.find((w) => w.id === activeWorkspaceId);
|
|
28
|
+
|
|
29
|
+
useKeyboard();
|
|
30
|
+
useNotificationListener();
|
|
31
|
+
useRpcBridge();
|
|
32
|
+
|
|
33
|
+
// âââ Drop overlay (VS Code-style) âââââââââââââââââââââââââââââââââââââ
|
|
34
|
+
// A transparent full-window overlay appears during external file drags.
|
|
35
|
+
// This guarantees the OS-level cursor shows "copy" regardless of WebGL canvas.
|
|
36
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
37
|
+
const dragCounterRef = useRef(0);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
// dragenter/dragleave fire for every child boundary crossing,
|
|
41
|
+
// so we use a counter to track when the drag truly leaves the window.
|
|
42
|
+
const onEnter = (e: DragEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
45
|
+
dragCounterRef.current++;
|
|
46
|
+
if (dragCounterRef.current === 1) setIsDragging(true);
|
|
47
|
+
};
|
|
48
|
+
const onLeave = () => {
|
|
49
|
+
dragCounterRef.current--;
|
|
50
|
+
if (dragCounterRef.current <= 0) {
|
|
51
|
+
dragCounterRef.current = 0;
|
|
52
|
+
setIsDragging(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const onDrop = (e: DragEvent) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
dragCounterRef.current = 0;
|
|
58
|
+
setIsDragging(false);
|
|
59
|
+
|
|
60
|
+
const files = e.dataTransfer?.files;
|
|
61
|
+
if (!files || files.length === 0) return;
|
|
62
|
+
|
|
63
|
+
// Get active terminal's PTY ID
|
|
64
|
+
const state = useStore.getState();
|
|
65
|
+
const ws = state.workspaces.find((w) => w.id === state.activeWorkspaceId);
|
|
66
|
+
if (!ws) return;
|
|
67
|
+
|
|
68
|
+
const findLeaf = (pane: typeof ws.rootPane): PaneLeaf | null => {
|
|
69
|
+
if (pane.type === 'leaf') return pane.id === ws.activePaneId ? pane : null;
|
|
70
|
+
for (const child of pane.children) {
|
|
71
|
+
const found = findLeaf(child);
|
|
72
|
+
if (found) return found;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
const leaf = findLeaf(ws.rootPane);
|
|
77
|
+
if (!leaf) return;
|
|
78
|
+
|
|
79
|
+
const activeSurface = leaf.surfaces.find((s) => s.id === leaf.activeSurfaceId);
|
|
80
|
+
if (!activeSurface || activeSurface.surfaceType === 'browser') return;
|
|
81
|
+
|
|
82
|
+
const ptyId = activeSurface.ptyId;
|
|
83
|
+
const paths: string[] = [];
|
|
84
|
+
for (let i = 0; i < files.length; i++) {
|
|
85
|
+
paths.push((files[i] as File & { path: string }).path);
|
|
86
|
+
}
|
|
87
|
+
const text = paths.map((p) => (p.includes(' ') ? `"${p}"` : p)).join(' ');
|
|
88
|
+
window.electronAPI.pty.write(ptyId, text);
|
|
89
|
+
};
|
|
90
|
+
// Prevent default on dragover at window level to allow drop everywhere
|
|
91
|
+
const onOver = (e: DragEvent) => {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
94
|
+
};
|
|
95
|
+
// Use capture phase so these fire before any child element (e.g. xterm
|
|
96
|
+
// WebGL canvas) can consume the event and cause a "forbidden" cursor.
|
|
97
|
+
window.addEventListener('dragenter', onEnter, true);
|
|
98
|
+
window.addEventListener('dragleave', onLeave, true);
|
|
99
|
+
window.addEventListener('dragover', onOver, true);
|
|
100
|
+
window.addEventListener('drop', onDrop, true);
|
|
101
|
+
return () => {
|
|
102
|
+
window.removeEventListener('dragenter', onEnter, true);
|
|
103
|
+
window.removeEventListener('dragleave', onLeave, true);
|
|
104
|
+
window.removeEventListener('dragover', onOver, true);
|
|
105
|
+
window.removeEventListener('drop', onDrop, true);
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// ėą ėė ė ė¸ė
ëŗĩė
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
window.electronAPI.session.load().then((saved: SessionData | null) => {
|
|
112
|
+
if (!saved) return;
|
|
113
|
+
useStore.getState().loadSession(saved);
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Save session on beforeunload
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const saveSession = () => {
|
|
120
|
+
const state = useStore.getState();
|
|
121
|
+
// Strip dangerous flags from session persistence
|
|
122
|
+
const companySafe = state.company ? { ...state.company, skipPermissions: undefined } : null;
|
|
123
|
+
const data: SessionData = {
|
|
124
|
+
workspaces: state.workspaces,
|
|
125
|
+
activeWorkspaceId: state.activeWorkspaceId,
|
|
126
|
+
sidebarVisible: state.sidebarVisible,
|
|
127
|
+
sidebarMode: state.sidebarMode,
|
|
128
|
+
company: companySafe,
|
|
129
|
+
memberCosts: state.memberCosts,
|
|
130
|
+
sessionStartTime: state.sessionStartTime,
|
|
131
|
+
// User preferences
|
|
132
|
+
theme: state.theme,
|
|
133
|
+
locale: state.locale,
|
|
134
|
+
terminalFontSize: state.terminalFontSize,
|
|
135
|
+
terminalFontFamily: state.terminalFontFamily,
|
|
136
|
+
defaultShell: state.defaultShell,
|
|
137
|
+
scrollbackLines: state.scrollbackLines,
|
|
138
|
+
sidebarPosition: state.sidebarPosition,
|
|
139
|
+
notificationSoundEnabled: state.notificationSoundEnabled,
|
|
140
|
+
toastEnabled: state.toastEnabled,
|
|
141
|
+
notificationRingEnabled: state.notificationRingEnabled,
|
|
142
|
+
customKeybindings: state.customKeybindings,
|
|
143
|
+
};
|
|
144
|
+
window.electronAPI.session.save(data);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
window.addEventListener('beforeunload', saveSession);
|
|
148
|
+
return () => window.removeEventListener('beforeunload', saveSession);
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
// Auto-create initial surface for empty leaf panes
|
|
152
|
+
// ė¸ė
ëŗĩėë ę˛Ŋė°: surfacesę° ė´ë¯¸ ėėŧë¯ëĄ ė´ effectë ė¤íëė§ ėė
|
|
153
|
+
// ë¸ëŧė°ė surfaceë§ ėë pane: surfaceTypeė´ 'browser'ė´ëŠ´ PTY ėėą ė¤íĩ
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!activeWorkspace) return;
|
|
156
|
+
const root = activeWorkspace.rootPane;
|
|
157
|
+
if (root.type !== 'leaf') return;
|
|
158
|
+
|
|
159
|
+
// surfacesę° ëšė´ėė ëë§ ė PTY ėėą
|
|
160
|
+
if (root.surfaces.length === 0) {
|
|
161
|
+
let cancelled = false;
|
|
162
|
+
const paneId = root.id;
|
|
163
|
+
window.electronAPI.pty.create().then((result: { id: string }) => {
|
|
164
|
+
if (cancelled) {
|
|
165
|
+
window.electronAPI.pty.dispose(result.id);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
addSurface(paneId, result.id, 'Terminal', '');
|
|
169
|
+
});
|
|
170
|
+
return () => { cancelled = true; };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// surfacesę° ėė§ë§ ëǍë browser íė
ė¸ ę˛Ŋė° PTY ėėą ė¤íĩ
|
|
174
|
+
const hasTerminalSurface = root.surfaces.some(
|
|
175
|
+
(s) => !s.surfaceType || s.surfaceType === 'terminal'
|
|
176
|
+
);
|
|
177
|
+
if (!hasTerminalSurface) {
|
|
178
|
+
// ë¸ëŧė°ė ë§ ėë pane â PTY ëļíė, ėëŦ´ę˛ë íė§ ėė
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}, [activeWorkspace?.id]);
|
|
182
|
+
|
|
183
|
+
if (!activeWorkspace) return null;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={`flex h-screen w-screen bg-[var(--bg-base)] overflow-hidden ${sidebarPosition === 'right' ? 'flex-row-reverse' : ''}`}>
|
|
187
|
+
{sidebarVisible ? <Sidebar /> : <MiniSidebar />}
|
|
188
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
189
|
+
<StatusBar />
|
|
190
|
+
{/* Render ALL workspaces but only show the active one.
|
|
191
|
+
This preserves xterm Terminal instances (and their scroll state)
|
|
192
|
+
across workspace switches â same pattern as surface tab switching. */}
|
|
193
|
+
<div className="flex-1 min-h-0 relative">
|
|
194
|
+
{workspaces.map((ws) => (
|
|
195
|
+
<div
|
|
196
|
+
key={ws.id}
|
|
197
|
+
style={{
|
|
198
|
+
position: 'absolute',
|
|
199
|
+
inset: 0,
|
|
200
|
+
display: ws.id === activeWorkspaceId ? 'flex' : 'none',
|
|
201
|
+
flexDirection: 'column',
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
<PaneContainer pane={ws.rootPane} isWorkspaceVisible={ws.id === activeWorkspaceId} />
|
|
205
|
+
</div>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<NotificationPanel />
|
|
210
|
+
<MessageFeedPanel />
|
|
211
|
+
<CommandPalette />
|
|
212
|
+
<SettingsPanel />
|
|
213
|
+
<ApprovalDialog />
|
|
214
|
+
{companyViewVisible && (
|
|
215
|
+
<CompanyView onClose={() => setCompanyViewVisible(false)} />
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Visual drag indicator â pointer-events always 'none' so it never
|
|
219
|
+
blocks clicks, scrolling, or keyboard. Drop handling is done entirely
|
|
220
|
+
via the window-level listeners registered in the useEffect above. */}
|
|
221
|
+
{isDragging && (
|
|
222
|
+
<div
|
|
223
|
+
style={{
|
|
224
|
+
position: 'fixed',
|
|
225
|
+
inset: 0,
|
|
226
|
+
zIndex: 99999,
|
|
227
|
+
pointerEvents: 'none',
|
|
228
|
+
backgroundColor: 'rgba(137, 180, 250, 0.08)',
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useStore } from '../../stores';
|
|
3
|
+
import { useT } from '../../hooks/useT';
|
|
4
|
+
|
|
5
|
+
export default function NotificationPanel() {
|
|
6
|
+
const t = useT();
|
|
7
|
+
const notifications = useStore((s) => s.notifications);
|
|
8
|
+
const notificationPanelVisible = useStore((s) => s.notificationPanelVisible);
|
|
9
|
+
const toggleNotificationPanel = useStore((s) => s.toggleNotificationPanel);
|
|
10
|
+
const markRead = useStore((s) => s.markRead);
|
|
11
|
+
const markAllReadForWorkspace = useStore((s) => s.markAllReadForWorkspace);
|
|
12
|
+
const clearNotifications = useStore((s) => s.clearNotifications);
|
|
13
|
+
const setActiveWorkspace = useStore((s) => s.setActiveWorkspace);
|
|
14
|
+
const activeWorkspaceId = useStore((s) => s.activeWorkspaceId);
|
|
15
|
+
|
|
16
|
+
const sorted = useMemo(
|
|
17
|
+
() => [...notifications].sort((a, b) => b.timestamp - a.timestamp),
|
|
18
|
+
[notifications],
|
|
19
|
+
);
|
|
20
|
+
const unreadCount = useMemo(
|
|
21
|
+
() => notifications.filter((n) => !n.read).length,
|
|
22
|
+
[notifications],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (!notificationPanelVisible) return null;
|
|
26
|
+
|
|
27
|
+
const handleNotifClick = (notif: typeof sorted[0]) => {
|
|
28
|
+
markRead(notif.id);
|
|
29
|
+
if (notif.workspaceId !== activeWorkspaceId) {
|
|
30
|
+
setActiveWorkspace(notif.workspaceId);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const formatTime = (ts: number) => {
|
|
35
|
+
const d = new Date(ts);
|
|
36
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const typeIcon = (type: string) => {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case 'agent': return 'đ¤';
|
|
42
|
+
case 'error': return 'â';
|
|
43
|
+
case 'warning': return 'â ī¸';
|
|
44
|
+
default: return 'âšī¸';
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="fixed right-0 top-0 h-full w-80 bg-[var(--bg-mantle)] border-l border-[var(--bg-surface)] z-50 flex flex-col shadow-2xl notification-panel-enter">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--bg-surface)]">
|
|
52
|
+
<div className="flex items-center gap-2">
|
|
53
|
+
<span className="text-sm font-bold text-[var(--text-main)]">{t('notification.title')}</span>
|
|
54
|
+
{unreadCount > 0 && (
|
|
55
|
+
<span className="bg-[var(--accent-blue)] text-[var(--bg-base)] text-[10px] font-bold px-1.5 py-0.5 rounded-full">
|
|
56
|
+
{unreadCount}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex items-center gap-2">
|
|
61
|
+
{notifications.length > 0 && (
|
|
62
|
+
<>
|
|
63
|
+
<button
|
|
64
|
+
className="text-[10px] text-[var(--text-subtle)] hover:text-[var(--accent-blue)] transition-colors"
|
|
65
|
+
onClick={() => markAllReadForWorkspace(activeWorkspaceId)}
|
|
66
|
+
>
|
|
67
|
+
{t('notification.markAllRead')}
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
className="text-[10px] text-[var(--text-subtle)] hover:text-[var(--accent-red)] transition-colors"
|
|
71
|
+
onClick={clearNotifications}
|
|
72
|
+
>
|
|
73
|
+
{t('notification.clear')}
|
|
74
|
+
</button>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
<button
|
|
78
|
+
className="text-[var(--text-subtle)] hover:text-[var(--text-main)] text-sm transition-colors"
|
|
79
|
+
onClick={toggleNotificationPanel}
|
|
80
|
+
>
|
|
81
|
+
â
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* List */}
|
|
87
|
+
<div className="flex-1 overflow-y-auto">
|
|
88
|
+
{sorted.length === 0 ? (
|
|
89
|
+
<div className="flex items-center justify-center h-full text-[var(--text-muted)] text-sm">
|
|
90
|
+
{t('notification.empty')}
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
sorted.map((notif) => (
|
|
94
|
+
<div
|
|
95
|
+
key={notif.id}
|
|
96
|
+
className={`px-4 py-3 border-b border-[rgba(var(--bg-surface-rgb),0.5)] cursor-pointer hover:bg-[rgba(var(--bg-surface-rgb),0.3)] transition-colors ${
|
|
97
|
+
notif.read ? 'opacity-60' : ''
|
|
98
|
+
}`}
|
|
99
|
+
onClick={() => handleNotifClick(notif)}
|
|
100
|
+
>
|
|
101
|
+
<div className="flex items-start gap-2">
|
|
102
|
+
<span className="text-xs mt-0.5">{typeIcon(notif.type)}</span>
|
|
103
|
+
<div className="flex-1 min-w-0">
|
|
104
|
+
<div className="flex items-center justify-between">
|
|
105
|
+
<span className={`text-xs font-medium truncate ${notif.read ? 'text-[var(--text-subtle)]' : 'text-[var(--text-main)]'}`}>
|
|
106
|
+
{notif.title}
|
|
107
|
+
</span>
|
|
108
|
+
<span className="text-[10px] text-[var(--text-muted)] flex-shrink-0 ml-2">
|
|
109
|
+
{formatTime(notif.timestamp)}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<p className="text-[11px] text-[var(--text-sub2)] mt-0.5 truncate">{notif.body}</p>
|
|
113
|
+
</div>
|
|
114
|
+
{!notif.read && (
|
|
115
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[var(--accent-blue)] mt-1.5 flex-shrink-0" />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
))
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Footer */}
|
|
124
|
+
<div className="px-4 py-2 border-t border-[var(--bg-surface)] text-[10px] text-[var(--text-muted)]">
|
|
125
|
+
{t('notification.toggle')}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|