@baixiangguo/dsh-session-folders 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +32 -2
- package/client.js +1202 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +5 -0
- package/index.js +189 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -6
package/client.js
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-session-folders',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
const NS = 'session-cwd-opener';
|
|
7
|
+
let primitives = null;
|
|
8
|
+
try {
|
|
9
|
+
primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
primitives = null;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
window.__sessionCwdOpener = { version: '0.1.0' };
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
|
|
17
|
+
const zh = {
|
|
18
|
+
'openDir': '打开工作目录',
|
|
19
|
+
'openInManager': '在文件管理器中打开',
|
|
20
|
+
'hint': '点击在文件管理器中打开',
|
|
21
|
+
'failed': '打开失败,请重试',
|
|
22
|
+
'workingDir': '工作目录',
|
|
23
|
+
'colorTitle': '文件夹颜色',
|
|
24
|
+
'noWorkspace': '该会话暂无所属文件夹,无法标色',
|
|
25
|
+
'filterTitle': '按颜色筛选',
|
|
26
|
+
'showAll': '全部显示',
|
|
27
|
+
'refresh': '刷新列表',
|
|
28
|
+
'settingsTitle': '文件夹颜色',
|
|
29
|
+
'settingsDesc': '给工作区文件夹标色,并给每种颜色起别名。设色入口在每条会话的 ... 菜单里;筛选按钮在侧边栏底部。',
|
|
30
|
+
'alias': '别名',
|
|
31
|
+
'addColor': '添加颜色',
|
|
32
|
+
'delete': '删除',
|
|
33
|
+
'reset': '恢复默认 8 色',
|
|
34
|
+
'inUse': '个文件夹在用',
|
|
35
|
+
'needRestart': '工作区列表不可用,请重启 DSH 后再试',
|
|
36
|
+
};
|
|
37
|
+
const en = {
|
|
38
|
+
'openDir': 'Open working directory',
|
|
39
|
+
'openInManager': 'Open in file manager',
|
|
40
|
+
'hint': 'Click to open in file manager',
|
|
41
|
+
'failed': 'Could not open. Try again.',
|
|
42
|
+
'workingDir': 'Working directory',
|
|
43
|
+
'colorTitle': 'Folder color',
|
|
44
|
+
'noWorkspace': 'This session has no workspace folder to color',
|
|
45
|
+
'filterTitle': 'Filter by color',
|
|
46
|
+
'showAll': 'Show all',
|
|
47
|
+
'refresh': 'Refresh',
|
|
48
|
+
'settingsTitle': 'Folder colors',
|
|
49
|
+
'settingsDesc': 'Color workspace folders and rename color aliases. Set a color from any session ... menu; filter from the palette button at the sidebar foot.',
|
|
50
|
+
'alias': 'Alias',
|
|
51
|
+
'addColor': 'Add color',
|
|
52
|
+
'delete': 'Delete',
|
|
53
|
+
'reset': 'Reset to 8 defaults',
|
|
54
|
+
'inUse': 'folders use it',
|
|
55
|
+
'needRestart': 'Workspace list unavailable, restart DSH and try again',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const DEFAULT_PALETTE = [
|
|
59
|
+
{ id: 'red', name: '红色', hex: '#F2555A' },
|
|
60
|
+
{ id: 'orange', name: '橙色', hex: '#F79009' },
|
|
61
|
+
{ id: 'yellow', name: '黄色', hex: '#FAC905' },
|
|
62
|
+
{ id: 'green', name: '绿色', hex: '#3FB950' },
|
|
63
|
+
{ id: 'blue', name: '蓝色', hex: '#4493F8' },
|
|
64
|
+
{ id: 'purple', name: '紫色', hex: '#A371F7' },
|
|
65
|
+
{ id: 'pink', name: '粉色', hex: '#F778BA' },
|
|
66
|
+
{ id: 'gray', name: '灰色', hex: '#8B949E' },
|
|
67
|
+
];
|
|
68
|
+
const STORE_KEY = 'session-cwd-opener:v1';
|
|
69
|
+
|
|
70
|
+
function defaultStore() {
|
|
71
|
+
return {
|
|
72
|
+
palette: DEFAULT_PALETTE.map((c) => ({ ...c })),
|
|
73
|
+
colors: {},
|
|
74
|
+
filter: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function loadStore() {
|
|
78
|
+
try {
|
|
79
|
+
const raw = window.localStorage.getItem(STORE_KEY);
|
|
80
|
+
if (!raw) return defaultStore();
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
const palette = Array.isArray(parsed.palette) && parsed.palette.length > 0
|
|
83
|
+
? parsed.palette.filter((c) => c && typeof c.id === 'string' && typeof c.hex === 'string')
|
|
84
|
+
.map((c) => ({ id: c.id, name: typeof c.name === 'string' ? c.name : c.id, hex: c.hex }))
|
|
85
|
+
: defaultStore().palette;
|
|
86
|
+
const colors = parsed.colors && typeof parsed.colors === 'object' ? parsed.colors : {};
|
|
87
|
+
const filter = Array.isArray(parsed.filter) ? parsed.filter.filter((v) => typeof v === 'string') : [];
|
|
88
|
+
return { palette, colors, filter };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return defaultStore();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let storeState = loadStore();
|
|
94
|
+
const storeListeners = new Set();
|
|
95
|
+
function getStore() {
|
|
96
|
+
return storeState;
|
|
97
|
+
}
|
|
98
|
+
function setStore(patch) {
|
|
99
|
+
storeState = { ...storeState, ...patch };
|
|
100
|
+
try {
|
|
101
|
+
window.localStorage.setItem(STORE_KEY, JSON.stringify(storeState));
|
|
102
|
+
} catch (e) {}
|
|
103
|
+
storeListeners.forEach((fn) => {
|
|
104
|
+
try {
|
|
105
|
+
fn();
|
|
106
|
+
} catch (e) {}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function subscribeStore(fn) {
|
|
110
|
+
storeListeners.add(fn);
|
|
111
|
+
return () => {
|
|
112
|
+
storeListeners.delete(fn);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function useStoreSel(sel) {
|
|
116
|
+
const get = React.useCallback(() => sel(getStore()), [sel]);
|
|
117
|
+
let value;
|
|
118
|
+
try {
|
|
119
|
+
value = React.useSyncExternalStore(subscribeStore, get, get);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
value = get();
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
// Colors are keyed by workspace PATH (stable for the user) with a
|
|
126
|
+
// fallback to legacy workspace-id keys, so old settings keep working.
|
|
127
|
+
function colorIdForWs(w) {
|
|
128
|
+
if (!w) return undefined;
|
|
129
|
+
const colors = getStore().colors || {};
|
|
130
|
+
if (w.path && colors[w.path]) return colors[w.path];
|
|
131
|
+
if (w.id && colors[w.id]) return colors[w.id];
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function setWorkspaceColor(ws, colorId) {
|
|
135
|
+
if (!ws) return;
|
|
136
|
+
const colors = { ...getStore().colors };
|
|
137
|
+
const prev = (ws.path && colors[ws.path]) || (ws.id && colors[ws.id]);
|
|
138
|
+
delete colors[ws.id];
|
|
139
|
+
if (ws.path) delete colors[ws.path];
|
|
140
|
+
if (colorId && prev !== colorId && ws.path) colors[ws.path] = colorId;
|
|
141
|
+
else if (colorId && prev !== colorId && ws.id) colors[ws.id] = colorId;
|
|
142
|
+
setStore({ colors });
|
|
143
|
+
}
|
|
144
|
+
function toggleFilter(colorId) {
|
|
145
|
+
const filter = getStore().filter.slice();
|
|
146
|
+
const i = filter.indexOf(colorId);
|
|
147
|
+
if (i >= 0) filter.splice(i, 1);
|
|
148
|
+
else filter.push(colorId);
|
|
149
|
+
setStore({ filter });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sessionIdOf(props) {
|
|
153
|
+
if (!props || typeof props !== 'object') return undefined;
|
|
154
|
+
if (typeof props.sessionId === 'string' && props.sessionId) return props.sessionId;
|
|
155
|
+
if (typeof props.sessionID === 'string' && props.sessionID) return props.sessionID;
|
|
156
|
+
if (props.node && typeof props.node.id === 'string' && props.node.id) return props.node.id;
|
|
157
|
+
for (const key of Object.keys(props)) {
|
|
158
|
+
if (/session.*id/i.test(key) && typeof props[key] === 'string' && props[key]) return props[key];
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function FolderIcon(props) {
|
|
164
|
+
const s = props && typeof props.size === 'number' ? props.size : 13;
|
|
165
|
+
return h(
|
|
166
|
+
'svg',
|
|
167
|
+
{
|
|
168
|
+
width: s,
|
|
169
|
+
height: s,
|
|
170
|
+
viewBox: '0 0 16 16',
|
|
171
|
+
fill: 'none',
|
|
172
|
+
'aria-hidden': true,
|
|
173
|
+
style: { flex: 'none', display: 'block' },
|
|
174
|
+
},
|
|
175
|
+
h('path', {
|
|
176
|
+
d: 'M1.5 3.6c0-.9.7-1.6 1.6-1.6h2.5l1.4 1.7h5.4c.9 0 1.6.7 1.6 1.6v4.9c0 .9-.7 1.6-1.6 1.6H3.1c-.9 0-1.6-.7-1.6-1.6V3.6z',
|
|
177
|
+
stroke: 'currentColor',
|
|
178
|
+
strokeWidth: 1.2,
|
|
179
|
+
strokeLinejoin: 'round',
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ColorDot(props) {
|
|
185
|
+
const size = props && typeof props.size === 'number' ? props.size : 10;
|
|
186
|
+
return h('span', {
|
|
187
|
+
'aria-hidden': true,
|
|
188
|
+
style: {
|
|
189
|
+
width: size,
|
|
190
|
+
height: size,
|
|
191
|
+
borderRadius: '50%',
|
|
192
|
+
background: props.hex,
|
|
193
|
+
flex: 'none',
|
|
194
|
+
display: 'inline-block',
|
|
195
|
+
boxSizing: 'border-box',
|
|
196
|
+
border: '1px solid rgba(128,128,128,.45)',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function PaletteGlyph(props) {
|
|
202
|
+
const s = props && typeof props.size === 'number' ? props.size : 15;
|
|
203
|
+
return h(
|
|
204
|
+
'svg',
|
|
205
|
+
{ width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, style: { display: 'block' } },
|
|
206
|
+
h('circle', { cx: 5, cy: 6, r: 2.1, fill: '#F2555A' }),
|
|
207
|
+
h('circle', { cx: 10.5, cy: 5, r: 2.1, fill: '#FAC905' }),
|
|
208
|
+
h('circle', { cx: 4.5, cy: 11, r: 2.1, fill: '#3FB950' }),
|
|
209
|
+
h('circle', { cx: 10, cy: 10.5, r: 2.1, fill: '#4493F8' }),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
inject: ['sessions', 'slots', 'locale'],
|
|
215
|
+
apply(ctx) {
|
|
216
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'session-cwd-opener: dictionaries');
|
|
217
|
+
const t = ctx.locale.bind(NS);
|
|
218
|
+
|
|
219
|
+
// ---------- session helpers ----------
|
|
220
|
+
function readCwd(sessionId) {
|
|
221
|
+
if (!sessionId) return undefined;
|
|
222
|
+
try {
|
|
223
|
+
const snap = ctx.sessions && ctx.sessions.list && ctx.sessions.list.getSnapshot
|
|
224
|
+
? ctx.sessions.list.getSnapshot()
|
|
225
|
+
: undefined;
|
|
226
|
+
const row = snap && snap.byId ? snap.byId[sessionId] : undefined;
|
|
227
|
+
const cwd = row ? row.cwd : undefined;
|
|
228
|
+
if (typeof cwd === 'string' && cwd) return cwd;
|
|
229
|
+
} catch (e) {}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
function readSessionsSnap() {
|
|
233
|
+
try {
|
|
234
|
+
const snap = ctx.sessions && ctx.sessions.list && ctx.sessions.list.getSnapshot
|
|
235
|
+
? ctx.sessions.list.getSnapshot()
|
|
236
|
+
: undefined;
|
|
237
|
+
return snap && snap.byId ? snap : { byId: {}, ids: [] };
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return { byId: {}, ids: [] };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function currentSessionId() {
|
|
243
|
+
try {
|
|
244
|
+
const snap = readSessionsSnap();
|
|
245
|
+
const rows = Object.values(snap.byId || {});
|
|
246
|
+
const cur = rows.find((r) => r && ((r.retainedBy && r.retainedBy.mainView) || 0) > 0);
|
|
247
|
+
return cur && cur.id ? cur.id : undefined;
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function subscribeSessions(listener) {
|
|
253
|
+
try {
|
|
254
|
+
const list = ctx.sessions && ctx.sessions.list;
|
|
255
|
+
if (list && typeof list.subscribe === 'function') return list.subscribe(listener);
|
|
256
|
+
} catch (e) {}
|
|
257
|
+
return () => {};
|
|
258
|
+
}
|
|
259
|
+
function useCwd(sessionId) {
|
|
260
|
+
const get = React.useCallback(() => readCwd(sessionId), [sessionId]);
|
|
261
|
+
const sub = React.useCallback((fn) => subscribeSessions(fn), []);
|
|
262
|
+
let value;
|
|
263
|
+
try {
|
|
264
|
+
value = React.useSyncExternalStore(sub, get, get);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
value = get();
|
|
267
|
+
}
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
function useSessionsTick() {
|
|
271
|
+
const get = React.useCallback(() => {
|
|
272
|
+
try {
|
|
273
|
+
const snap = readSessionsSnap();
|
|
274
|
+
return (snap.ids || []).join(',') + '|' + Object.keys(snap.byId || {}).length;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
}, []);
|
|
279
|
+
const sub = React.useCallback((fn) => subscribeSessions(fn), []);
|
|
280
|
+
let value;
|
|
281
|
+
try {
|
|
282
|
+
value = React.useSyncExternalStore(sub, get, get);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
value = get();
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function openCwd(path) {
|
|
290
|
+
try {
|
|
291
|
+
const r = await fetch('open-in-app/apps', { headers: { accept: 'application/json' } });
|
|
292
|
+
if (r.ok) {
|
|
293
|
+
const payload = await r.json();
|
|
294
|
+
const apps = Array.isArray(payload.apps) ? payload.apps.filter((v) => typeof v === 'string') : [];
|
|
295
|
+
const pick =
|
|
296
|
+
['explorer', 'finder', 'filemanager'].find((a) => apps.indexOf(a) >= 0) || apps[0];
|
|
297
|
+
if (pick) {
|
|
298
|
+
const ro = await fetch('open-in-app/open', {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'content-type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ app: pick, path }),
|
|
302
|
+
});
|
|
303
|
+
if (ro.ok) return { ok: true };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {}
|
|
307
|
+
try {
|
|
308
|
+
const r2 = await fetch('session-cwd-opener/open', {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: { 'content-type': 'application/json' },
|
|
311
|
+
body: JSON.stringify({ path }),
|
|
312
|
+
});
|
|
313
|
+
if (r2.ok) return { ok: true };
|
|
314
|
+
} catch (e) {}
|
|
315
|
+
return { ok: false };
|
|
316
|
+
}
|
|
317
|
+
function useOpener(cwd) {
|
|
318
|
+
const [busy, setBusy] = React.useState(false);
|
|
319
|
+
const [failed, setFailed] = React.useState(false);
|
|
320
|
+
const run = React.useCallback(
|
|
321
|
+
(e) => {
|
|
322
|
+
if (e) {
|
|
323
|
+
e.stopPropagation();
|
|
324
|
+
if (e.preventDefault) e.preventDefault();
|
|
325
|
+
}
|
|
326
|
+
if (!cwd || busy) return;
|
|
327
|
+
setBusy(true);
|
|
328
|
+
setFailed(false);
|
|
329
|
+
openCwd(cwd).then((r) => {
|
|
330
|
+
setBusy(false);
|
|
331
|
+
setFailed(!r.ok);
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
[cwd, busy],
|
|
335
|
+
);
|
|
336
|
+
return { busy, failed, run };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------- workspaces (via Host route) ----------
|
|
340
|
+
const wsCache = { status: 'idle', data: [] };
|
|
341
|
+
const wsListeners = new Set();
|
|
342
|
+
function emitWs() {
|
|
343
|
+
wsListeners.forEach((fn) => {
|
|
344
|
+
try {
|
|
345
|
+
fn();
|
|
346
|
+
} catch (e) {}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
let wsInflight = null;
|
|
350
|
+
async function refreshWorkspaces() {
|
|
351
|
+
if (wsInflight) return wsInflight;
|
|
352
|
+
wsInflight = (async () => {
|
|
353
|
+
try {
|
|
354
|
+
const r = await fetch('session-cwd-opener/workspaces', { headers: { accept: 'application/json' } });
|
|
355
|
+
if (r.ok) {
|
|
356
|
+
const payload = await r.json();
|
|
357
|
+
if (Array.isArray(payload.workspaces)) {
|
|
358
|
+
wsCache.status = 'ready';
|
|
359
|
+
wsCache.data = payload.workspaces;
|
|
360
|
+
emitWs();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
wsCache.status = 'error';
|
|
365
|
+
emitWs();
|
|
366
|
+
} catch (e) {
|
|
367
|
+
wsCache.status = 'error';
|
|
368
|
+
emitWs();
|
|
369
|
+
} finally {
|
|
370
|
+
wsInflight = null;
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
return wsInflight;
|
|
374
|
+
}
|
|
375
|
+
function subscribeWs(fn) {
|
|
376
|
+
wsListeners.add(fn);
|
|
377
|
+
return () => {
|
|
378
|
+
wsListeners.delete(fn);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function useWorkspaces() {
|
|
382
|
+
const get = React.useCallback(() => wsCache.status + ':' + wsCache.data.length, []);
|
|
383
|
+
try {
|
|
384
|
+
React.useSyncExternalStore(subscribeWs, get, get);
|
|
385
|
+
} catch (e) {}
|
|
386
|
+
return wsCache;
|
|
387
|
+
}
|
|
388
|
+
function workspaceOfSession(sessionId, cwd) {
|
|
389
|
+
const data = wsCache.data || [];
|
|
390
|
+
for (const w of data) {
|
|
391
|
+
if (w.sessionIds && w.sessionIds.indexOf(sessionId) >= 0) return w;
|
|
392
|
+
}
|
|
393
|
+
if (cwd) {
|
|
394
|
+
for (const w of data) {
|
|
395
|
+
if (w.path === cwd) return w;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- generated CSS: recolor + selected-text migration + filter ----------
|
|
402
|
+
function escAttr(v) {
|
|
403
|
+
try {
|
|
404
|
+
if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(v);
|
|
405
|
+
} catch (e) {}
|
|
406
|
+
return String(v).replace(/["\\]/g, '\\$&');
|
|
407
|
+
}
|
|
408
|
+
const ACCENT = 'var(--dsw-alias-state-business-primary, var(--dsw-alias-label-primary))';
|
|
409
|
+
function buildCss() {
|
|
410
|
+
const st = getStore();
|
|
411
|
+
const byId = {};
|
|
412
|
+
st.palette.forEach((c) => {
|
|
413
|
+
byId[c.id] = c;
|
|
414
|
+
});
|
|
415
|
+
const rules = [];
|
|
416
|
+
const wsData = wsCache.data || [];
|
|
417
|
+
// Observe the REAL sidebar structure: walk rows in document order and
|
|
418
|
+
// assign each session row to its nearest preceding workspace header.
|
|
419
|
+
// Key-agnostic: works no matter how the host names group keys.
|
|
420
|
+
const headerMap = (function observeHeaderMapping() {
|
|
421
|
+
const map = {};
|
|
422
|
+
let els = [];
|
|
423
|
+
try {
|
|
424
|
+
els = document.querySelectorAll('[data-row-key^="workspace:"],[data-row-key^="session:"]');
|
|
425
|
+
} catch (e) {
|
|
426
|
+
return map;
|
|
427
|
+
}
|
|
428
|
+
let curHeader = null;
|
|
429
|
+
for (let i = 0; i < els.length; i++) {
|
|
430
|
+
let k = null;
|
|
431
|
+
try {
|
|
432
|
+
k = els[i].getAttribute('data-row-key');
|
|
433
|
+
} catch (err) {
|
|
434
|
+
k = null;
|
|
435
|
+
}
|
|
436
|
+
if (!k) continue;
|
|
437
|
+
if (k.indexOf('workspace:') === 0) {
|
|
438
|
+
curHeader = k;
|
|
439
|
+
if (!map[curHeader]) map[curHeader] = { sessions: [] };
|
|
440
|
+
} else if (curHeader && k.indexOf('session:') === 0) {
|
|
441
|
+
map[curHeader].sessions.push(k.slice(8));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return map;
|
|
445
|
+
})();
|
|
446
|
+
// Identify each observed header by majority vote of its member
|
|
447
|
+
// sessions against the API membership; fall back to matching the
|
|
448
|
+
// header key itself against a workspace id or path.
|
|
449
|
+
const apiBySession = {};
|
|
450
|
+
wsData.forEach((w) => {
|
|
451
|
+
(w.sessionIds || []).forEach((sid) => {
|
|
452
|
+
if (!apiBySession[sid]) apiBySession[sid] = [];
|
|
453
|
+
apiBySession[sid].push(w);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
const headerWs = {};
|
|
457
|
+
Object.keys(headerMap).forEach((hk) => {
|
|
458
|
+
const votes = {};
|
|
459
|
+
const order = [];
|
|
460
|
+
headerMap[hk].sessions.forEach((sid) => {
|
|
461
|
+
(apiBySession[sid] || []).forEach((w) => {
|
|
462
|
+
const k = w.path || w.id;
|
|
463
|
+
if (!votes[k]) {
|
|
464
|
+
votes[k] = { n: 0, w };
|
|
465
|
+
order.push(k);
|
|
466
|
+
}
|
|
467
|
+
votes[k].n += 1;
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
let best = null;
|
|
471
|
+
order.forEach((k) => {
|
|
472
|
+
if (!best || votes[k].n > votes[best].n) best = k;
|
|
473
|
+
});
|
|
474
|
+
if (best) {
|
|
475
|
+
headerWs[hk] = votes[best].w;
|
|
476
|
+
} else {
|
|
477
|
+
const tail = hk.slice(10);
|
|
478
|
+
const w = wsData.find((x) => x.id === tail || x.path === tail);
|
|
479
|
+
if (w) headerWs[hk] = w;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
const hexForWs = (w) => {
|
|
483
|
+
if (!w) return null;
|
|
484
|
+
const colors = getStore().colors || {};
|
|
485
|
+
const cid = (w.path && colors[w.path]) || (w.id && colors[w.id]);
|
|
486
|
+
const c = cid && byId[cid];
|
|
487
|
+
return c ? { hex: c.hex, id: c.id } : null;
|
|
488
|
+
};
|
|
489
|
+
// 1) folder recolor on OBSERVED header rows (never by assumed keys)
|
|
490
|
+
Object.keys(headerWs).forEach((hk) => {
|
|
491
|
+
const hit = hexForWs(headerWs[hk]);
|
|
492
|
+
if (!hit) return;
|
|
493
|
+
const sel = '[data-row-key="' + escAttr(hk) + '"]';
|
|
494
|
+
rules.push(
|
|
495
|
+
// folder icon itself (works when the icon inherits currentColor)
|
|
496
|
+
sel + ' > span:first-child{color:' + hit.hex + ' !important;}',
|
|
497
|
+
// whole folder row gets a colored edge + tint so the color is
|
|
498
|
+
// always visible even if the host icon ignores `color`
|
|
499
|
+
sel + '{box-shadow: inset 4px 0 0 ' + hit.hex + ' !important;background: color-mix(in srgb, ' + hit.hex + ' 24%, transparent) !important;}',
|
|
500
|
+
// folder label text always wears the custom color (the later
|
|
501
|
+
// selected-state rule upgrades it to the theme accent)
|
|
502
|
+
sel + ' > span:nth-child(3), ' + sel + ' > span:nth-child(3) span{color:' + hit.hex + ' !important;}',
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
const snap = readSessionsSnap();
|
|
506
|
+
const cur = currentSessionId();
|
|
507
|
+
let curHeader = null;
|
|
508
|
+
if (cur) {
|
|
509
|
+
Object.keys(headerMap).forEach((hk) => {
|
|
510
|
+
if (headerMap[hk].sessions.indexOf(cur) >= 0) curHeader = hk;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// 2) selected-state text highlight migration (folder keeps custom color)
|
|
514
|
+
if (curHeader) {
|
|
515
|
+
rules.push(
|
|
516
|
+
'[data-row-key="workspace:' + escAttr(curHeader) + '"] > span:nth-child(3), ' +
|
|
517
|
+
'[data-row-key="workspace:' + escAttr(curHeader) + '"] > span:nth-child(3) span{color:' + ACCENT + ' !important;}',
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (cur && snap.byId && snap.byId[cur] && !snap.byId[cur].blank) {
|
|
521
|
+
rules.push(
|
|
522
|
+
'[data-row-key="session:' + escAttr(cur) + '"] > span:nth-child(2){color:' + ACCENT + ' !important;}',
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
// 3) color filter (only once the workspace map is loaded;
|
|
526
|
+
// a missing map must never hide rows)
|
|
527
|
+
const filter = (st.filter || []).filter((id) => byId[id]);
|
|
528
|
+
if (filter.length > 0 && wsCache.status === 'ready') {
|
|
529
|
+
const showHeaders = new Set();
|
|
530
|
+
const showSessions = new Set();
|
|
531
|
+
Object.keys(headerWs).forEach((hk) => {
|
|
532
|
+
const hitH = hexForWs(headerWs[hk]);
|
|
533
|
+
if (hitH && filter.indexOf(hitH.id) >= 0) {
|
|
534
|
+
showHeaders.add(hk);
|
|
535
|
+
(headerMap[hk].sessions || []).forEach((id) => showSessions.add(id));
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
wsData.forEach((w) => {
|
|
539
|
+
const hitW = hexForWs(w);
|
|
540
|
+
if (hitW && filter.indexOf(hitW.id) >= 0) {
|
|
541
|
+
(w.sessionIds || []).forEach((id) => showSessions.add(id));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
if (cur) showSessions.add(cur);
|
|
545
|
+
if (curHeader) showHeaders.add(curHeader);
|
|
546
|
+
// keep blank (new-session placeholder) rows visible
|
|
547
|
+
Object.keys(snap.byId || {}).forEach((id) => {
|
|
548
|
+
if (snap.byId[id] && snap.byId[id].blank) showSessions.add(id);
|
|
549
|
+
});
|
|
550
|
+
const hideHeaders = Object.keys(headerMap).filter((hk) => !showHeaders.has(hk));
|
|
551
|
+
if (hideHeaders.length > 0) {
|
|
552
|
+
rules.push(
|
|
553
|
+
hideHeaders.map((hk) => '[data-row-key="' + escAttr(hk) + '"]').join(',') +
|
|
554
|
+
'{display:none !important;}'
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const hideSessions = Object.keys(snap.byId || {}).filter((id) => !showSessions.has(id));
|
|
558
|
+
if (hideSessions.length > 0) {
|
|
559
|
+
const chunks = [];
|
|
560
|
+
for (let i = 0; i < hideSessions.length; i += 60) {
|
|
561
|
+
chunks.push(
|
|
562
|
+
hideSessions.slice(i, i + 60).map((id) => '[data-row-key="session:' + escAttr(id) + '"]').join(',') +
|
|
563
|
+
'{display:none !important;}',
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
rules.push(...chunks);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return rules.join('\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const hoverStyle = {
|
|
573
|
+
display: 'flex',
|
|
574
|
+
alignItems: 'center',
|
|
575
|
+
gap: 6,
|
|
576
|
+
minWidth: 0,
|
|
577
|
+
width: '100%',
|
|
578
|
+
marginTop: 6,
|
|
579
|
+
padding: '5px 7px',
|
|
580
|
+
borderRadius: 6,
|
|
581
|
+
border: '1px solid var(--dsw-alias-border-l4)',
|
|
582
|
+
background: 'var(--dsw-alias-interactive-bg-hover)',
|
|
583
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
584
|
+
fontSize: 11,
|
|
585
|
+
lineHeight: '15px',
|
|
586
|
+
cursor: 'pointer',
|
|
587
|
+
textAlign: 'left',
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// ---- 1. Hover card ----
|
|
591
|
+
function SidebarHoverCwd(props) {
|
|
592
|
+
const sessionId = sessionIdOf(props);
|
|
593
|
+
const cwd = useCwd(sessionId);
|
|
594
|
+
const { busy, failed, run } = useOpener(cwd);
|
|
595
|
+
const wsColorHex = useStoreSel((st) => {
|
|
596
|
+
const ws = workspaceOfSession(sessionId, cwd);
|
|
597
|
+
if (!ws) return null;
|
|
598
|
+
const c = (st.palette || []).find((x) => x.id === (st.colors || {})[ws.id]);
|
|
599
|
+
return c ? c.hex : null;
|
|
600
|
+
});
|
|
601
|
+
if (!sessionId || !cwd) return null;
|
|
602
|
+
return h(
|
|
603
|
+
'div',
|
|
604
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 } },
|
|
605
|
+
h(
|
|
606
|
+
'button',
|
|
607
|
+
{
|
|
608
|
+
type: 'button',
|
|
609
|
+
title: cwd + '\n' + t('hint'),
|
|
610
|
+
'aria-label': t('openInManager') + ': ' + cwd,
|
|
611
|
+
onClick: run,
|
|
612
|
+
style: Object.assign({}, hoverStyle, { opacity: busy ? 0.6 : 1 }),
|
|
613
|
+
},
|
|
614
|
+
wsColorHex ? h(ColorDot, { hex: wsColorHex, size: 9 }) : h(FolderIcon, { size: 13 }),
|
|
615
|
+
h(
|
|
616
|
+
'span',
|
|
617
|
+
{
|
|
618
|
+
style: {
|
|
619
|
+
minWidth: 0,
|
|
620
|
+
flex: 1,
|
|
621
|
+
overflow: 'hidden',
|
|
622
|
+
textOverflow: 'ellipsis',
|
|
623
|
+
whiteSpace: 'nowrap',
|
|
624
|
+
direction: 'ltr',
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
cwd,
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
failed &&
|
|
631
|
+
h(
|
|
632
|
+
'div',
|
|
633
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-state-error-primary)' } },
|
|
634
|
+
t('failed'),
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ---- 2. Session "..." menu: path + open + folder color ----
|
|
640
|
+
function OpenMenuButtonInner({ cwd, closeMenu }) {
|
|
641
|
+
const { busy, failed, run } = useOpener(cwd);
|
|
642
|
+
const doSelect = () => {
|
|
643
|
+
try {
|
|
644
|
+
closeMenu();
|
|
645
|
+
} catch (e) {}
|
|
646
|
+
run();
|
|
647
|
+
};
|
|
648
|
+
const MenuItemButton = primitives && primitives.MenuItemButton;
|
|
649
|
+
const HostIcon =
|
|
650
|
+
(primitives && (primitives.IconFolderOpenOutlineRegular || primitives.IconFolderOpenRegular)) ||
|
|
651
|
+
null;
|
|
652
|
+
if (MenuItemButton) {
|
|
653
|
+
return h(
|
|
654
|
+
React.Fragment,
|
|
655
|
+
null,
|
|
656
|
+
h(MenuItemButton, {
|
|
657
|
+
icon: HostIcon ? h(HostIcon, {}) : h(FolderIcon, { size: 14 }),
|
|
658
|
+
onSelect: doSelect,
|
|
659
|
+
children: busy ? '…' : t('openDir'),
|
|
660
|
+
}),
|
|
661
|
+
failed &&
|
|
662
|
+
h(
|
|
663
|
+
'div',
|
|
664
|
+
{
|
|
665
|
+
style: {
|
|
666
|
+
padding: '2px 10px 4px 10px',
|
|
667
|
+
fontSize: 11,
|
|
668
|
+
color: 'var(--dsw-alias-state-error-primary)',
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
t('failed'),
|
|
672
|
+
),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
return h(
|
|
676
|
+
'button',
|
|
677
|
+
{
|
|
678
|
+
type: 'button',
|
|
679
|
+
onClick: run,
|
|
680
|
+
style: {
|
|
681
|
+
display: 'flex',
|
|
682
|
+
alignItems: 'center',
|
|
683
|
+
gap: 8,
|
|
684
|
+
width: '100%',
|
|
685
|
+
boxSizing: 'border-box',
|
|
686
|
+
padding: '6px 10px',
|
|
687
|
+
fontSize: 12,
|
|
688
|
+
lineHeight: '18px',
|
|
689
|
+
border: 0,
|
|
690
|
+
background: 'transparent',
|
|
691
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
692
|
+
cursor: busy ? 'default' : 'pointer',
|
|
693
|
+
textAlign: 'left',
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
h(FolderIcon, { size: 14 }),
|
|
697
|
+
h('span', { style: { opacity: busy ? 0.55 : 1 } }, busy ? '…' : t('openDir')),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
function OpenMenuButtonWithHook({ cwd, useMenuOpenState }) {
|
|
701
|
+
const [, setMenuOpen] = useMenuOpenState();
|
|
702
|
+
return h(OpenMenuButtonInner, { cwd, closeMenu: () => setMenuOpen(false) });
|
|
703
|
+
}
|
|
704
|
+
function ColorRow({ sessionId, cwd }) {
|
|
705
|
+
const palette = useStoreSel((st) => st.palette);
|
|
706
|
+
const currentColorId = useStoreSel((st) => {
|
|
707
|
+
const ws = workspaceOfSession(sessionId, cwd);
|
|
708
|
+
if (!ws) return undefined;
|
|
709
|
+
const cc = st.colors || {};
|
|
710
|
+
return (ws.path && cc[ws.path]) || (ws.id && cc[ws.id]) || undefined;
|
|
711
|
+
});
|
|
712
|
+
const wsReady = useWorkspaces().status;
|
|
713
|
+
const ws = workspaceOfSession(sessionId, cwd);
|
|
714
|
+
if (!ws) {
|
|
715
|
+
return h(
|
|
716
|
+
'div',
|
|
717
|
+
{
|
|
718
|
+
style: {
|
|
719
|
+
padding: '2px 10px 4px 10px',
|
|
720
|
+
fontSize: 11,
|
|
721
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
wsReady === 'ready' ? t('noWorkspace') : t('needRestart'),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return h(
|
|
728
|
+
'div',
|
|
729
|
+
{
|
|
730
|
+
style: {
|
|
731
|
+
display: 'flex',
|
|
732
|
+
alignItems: 'center',
|
|
733
|
+
flexWrap: 'wrap',
|
|
734
|
+
gap: 7,
|
|
735
|
+
padding: '4px 10px 6px 10px',
|
|
736
|
+
},
|
|
737
|
+
onClick: (e) => e.stopPropagation(),
|
|
738
|
+
},
|
|
739
|
+
h(
|
|
740
|
+
'span',
|
|
741
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginRight: 2 } },
|
|
742
|
+
t('colorTitle'),
|
|
743
|
+
),
|
|
744
|
+
palette.map((c) =>
|
|
745
|
+
h(
|
|
746
|
+
'button',
|
|
747
|
+
{
|
|
748
|
+
key: c.id,
|
|
749
|
+
type: 'button',
|
|
750
|
+
title: c.name,
|
|
751
|
+
'aria-label': c.name,
|
|
752
|
+
onClick: (e) => {
|
|
753
|
+
e.stopPropagation();
|
|
754
|
+
setWorkspaceColor(ws, c.id);
|
|
755
|
+
// if a filter is active, include the newly painted color
|
|
756
|
+
// so the row doesn't vanish from under the open menu
|
|
757
|
+
const after = getStore();
|
|
758
|
+
const cc2 = after.colors || {};
|
|
759
|
+
const painted = (ws.path && cc2[ws.path]) || (ws.id && cc2[ws.id]);
|
|
760
|
+
if (
|
|
761
|
+
painted &&
|
|
762
|
+
after.filter.length > 0 &&
|
|
763
|
+
after.filter.indexOf(painted) < 0
|
|
764
|
+
) {
|
|
765
|
+
setStore({ filter: after.filter.concat([painted]) });
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
style: {
|
|
769
|
+
width: 18,
|
|
770
|
+
height: 18,
|
|
771
|
+
borderRadius: '50%',
|
|
772
|
+
background: c.hex,
|
|
773
|
+
border: '1px solid rgba(128,128,128,.5)',
|
|
774
|
+
outline: currentColorId === c.id ? '2px solid ' + ACCENT : 'none',
|
|
775
|
+
outlineOffset: 1,
|
|
776
|
+
padding: 0,
|
|
777
|
+
cursor: 'pointer',
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
),
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
function SessionMenuCwd(props) {
|
|
785
|
+
const sessionId = sessionIdOf(props);
|
|
786
|
+
const cwd = useCwd(sessionId);
|
|
787
|
+
useWorkspaces();
|
|
788
|
+
if (!sessionId || !cwd) return null;
|
|
789
|
+
const useMenuOpenState = props && typeof props.useMenuOpenState === 'function' ? props.useMenuOpenState : null;
|
|
790
|
+
const rowBase = {
|
|
791
|
+
display: 'flex',
|
|
792
|
+
alignItems: 'center',
|
|
793
|
+
gap: 8,
|
|
794
|
+
width: '100%',
|
|
795
|
+
boxSizing: 'border-box',
|
|
796
|
+
padding: '6px 10px',
|
|
797
|
+
fontSize: 12,
|
|
798
|
+
lineHeight: '18px',
|
|
799
|
+
};
|
|
800
|
+
return h(
|
|
801
|
+
'div',
|
|
802
|
+
{ style: { minWidth: 0, padding: '2px 0' }, onClick: (e) => e.stopPropagation() },
|
|
803
|
+
h(
|
|
804
|
+
'div',
|
|
805
|
+
{
|
|
806
|
+
title: cwd,
|
|
807
|
+
style: Object.assign({}, rowBase, {
|
|
808
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
809
|
+
cursor: 'default',
|
|
810
|
+
alignItems: 'flex-start',
|
|
811
|
+
}),
|
|
812
|
+
},
|
|
813
|
+
h(
|
|
814
|
+
'span',
|
|
815
|
+
{
|
|
816
|
+
style: {
|
|
817
|
+
minWidth: 0,
|
|
818
|
+
flex: 1,
|
|
819
|
+
overflowWrap: 'anywhere',
|
|
820
|
+
wordBreak: 'break-all',
|
|
821
|
+
whiteSpace: 'normal',
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
cwd,
|
|
825
|
+
),
|
|
826
|
+
),
|
|
827
|
+
useMenuOpenState
|
|
828
|
+
? h(OpenMenuButtonWithHook, { cwd, useMenuOpenState })
|
|
829
|
+
: h(OpenMenuButtonInner, { cwd, closeMenu: () => {} }),
|
|
830
|
+
h(ColorRow, { sessionId, cwd }),
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ---- 3. Sidebar foot: color filter ----
|
|
835
|
+
function FooterFilter() {
|
|
836
|
+
const palette = useStoreSel((st) => st.palette);
|
|
837
|
+
const filter = useStoreSel((st) => st.filter);
|
|
838
|
+
const [open, setOpen] = React.useState(false);
|
|
839
|
+
const [pos, setPos] = React.useState(null);
|
|
840
|
+
const [, setTick] = React.useState(0);
|
|
841
|
+
const btnRef = React.useRef(null);
|
|
842
|
+
const panelRef = React.useRef(null);
|
|
843
|
+
useSessionsTick();
|
|
844
|
+
useWorkspaces();
|
|
845
|
+
useStoreSel((st) => (st.colors ? Object.keys(st.colors).length : 0));
|
|
846
|
+
const css = buildCss();
|
|
847
|
+
React.useEffect(() => {
|
|
848
|
+
const id = setInterval(() => {
|
|
849
|
+
refreshWorkspaces();
|
|
850
|
+
setTick((x) => x + 1);
|
|
851
|
+
}, 4000);
|
|
852
|
+
return () => clearInterval(id);
|
|
853
|
+
}, []);
|
|
854
|
+
React.useEffect(() => {
|
|
855
|
+
if (!open) return undefined;
|
|
856
|
+
const onDown = (e) => {
|
|
857
|
+
try {
|
|
858
|
+
if (panelRef.current && panelRef.current.contains(e.target)) return;
|
|
859
|
+
if (btnRef.current && btnRef.current.contains(e.target)) return;
|
|
860
|
+
} catch (err) {}
|
|
861
|
+
setOpen(false);
|
|
862
|
+
};
|
|
863
|
+
const onKey = (e) => {
|
|
864
|
+
if (e && e.key === 'Escape') setOpen(false);
|
|
865
|
+
};
|
|
866
|
+
document.addEventListener('mousedown', onDown);
|
|
867
|
+
document.addEventListener('keydown', onKey);
|
|
868
|
+
return () => {
|
|
869
|
+
document.removeEventListener('mousedown', onDown);
|
|
870
|
+
document.removeEventListener('keydown', onKey);
|
|
871
|
+
};
|
|
872
|
+
}, [open ]);
|
|
873
|
+
const toggleOne = (id) => {
|
|
874
|
+
toggleFilter(id);
|
|
875
|
+
};
|
|
876
|
+
return h(
|
|
877
|
+
React.Fragment,
|
|
878
|
+
null,
|
|
879
|
+
h('style', null, css),
|
|
880
|
+
h(
|
|
881
|
+
'button',
|
|
882
|
+
{
|
|
883
|
+
type: 'button',
|
|
884
|
+
title: t('filterTitle'),
|
|
885
|
+
'aria-label': t('filterTitle'),
|
|
886
|
+
ref: btnRef,
|
|
887
|
+
onClick: (e) => {
|
|
888
|
+
e.stopPropagation();
|
|
889
|
+
try {
|
|
890
|
+
const r = btnRef.current && btnRef.current.getBoundingClientRect
|
|
891
|
+
? btnRef.current.getBoundingClientRect()
|
|
892
|
+
: null;
|
|
893
|
+
if (r) {
|
|
894
|
+
setPos({
|
|
895
|
+
left: Math.max(8, Math.min(r.left, window.innerWidth - 260)),
|
|
896
|
+
bottom: Math.max(8, window.innerHeight - r.top + 8),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
} catch (err) {}
|
|
900
|
+
setOpen((v) => !v);
|
|
901
|
+
},
|
|
902
|
+
style: {
|
|
903
|
+
position: 'relative',
|
|
904
|
+
display: 'inline-flex',
|
|
905
|
+
alignItems: 'center',
|
|
906
|
+
justifyContent: 'center',
|
|
907
|
+
width: 28,
|
|
908
|
+
height: 28,
|
|
909
|
+
padding: 0,
|
|
910
|
+
border: 0,
|
|
911
|
+
borderRadius: 6,
|
|
912
|
+
background: 'transparent',
|
|
913
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
914
|
+
cursor: 'pointer',
|
|
915
|
+
},
|
|
916
|
+
onMouseEnter: (e) => {
|
|
917
|
+
e.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover)';
|
|
918
|
+
},
|
|
919
|
+
onMouseLeave: (e) => {
|
|
920
|
+
e.currentTarget.style.background = 'transparent';
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
h(PaletteGlyph, { size: 15 }),
|
|
924
|
+
filter.length > 0 &&
|
|
925
|
+
h('span', {
|
|
926
|
+
'aria-hidden': true,
|
|
927
|
+
style: {
|
|
928
|
+
position: 'absolute',
|
|
929
|
+
top: 2,
|
|
930
|
+
right: 2,
|
|
931
|
+
minWidth: 14,
|
|
932
|
+
height: 14,
|
|
933
|
+
borderRadius: 7,
|
|
934
|
+
background: 'var(--dsw-alias-state-business-primary, #3E82F7)',
|
|
935
|
+
color: '#fff',
|
|
936
|
+
fontSize: 10,
|
|
937
|
+
lineHeight: '14px',
|
|
938
|
+
textAlign: 'center',
|
|
939
|
+
padding: '0 3px',
|
|
940
|
+
boxSizing: 'border-box',
|
|
941
|
+
},
|
|
942
|
+
}, String(filter.length)),
|
|
943
|
+
),
|
|
944
|
+
open &&
|
|
945
|
+
h(
|
|
946
|
+
'div',
|
|
947
|
+
{
|
|
948
|
+
ref: panelRef,
|
|
949
|
+
role: 'menu',
|
|
950
|
+
'aria-label': t('filterTitle'),
|
|
951
|
+
onClick: (e) => e.stopPropagation(),
|
|
952
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
953
|
+
style: {
|
|
954
|
+
position: 'fixed',
|
|
955
|
+
zIndex: 9999,
|
|
956
|
+
left: pos && typeof pos.left === 'number' ? pos.left : 8,
|
|
957
|
+
bottom: pos && typeof pos.bottom === 'number' ? pos.bottom : 104,
|
|
958
|
+
minWidth: 150,
|
|
959
|
+
maxWidth: 210,
|
|
960
|
+
maxHeight: 'min(46vh, 340px)',
|
|
961
|
+
overflowY: 'auto',
|
|
962
|
+
overflowX: 'hidden',
|
|
963
|
+
background: 'color-mix(in srgb, var(--dsw-alias-interactive-bg-hover) 55%, transparent)',
|
|
964
|
+
backdropFilter: 'blur(20px) saturate(1.5)',
|
|
965
|
+
WebkitBackdropFilter: 'blur(20px) saturate(1.5)',
|
|
966
|
+
border: '1px solid var(--dsw-alias-border-l4)',
|
|
967
|
+
borderRadius: 12,
|
|
968
|
+
boxShadow: '0 8px 28px rgba(0,0,0,.45)',
|
|
969
|
+
padding: 6,
|
|
970
|
+
display: 'flex',
|
|
971
|
+
flexDirection: 'column',
|
|
972
|
+
gap: 2,
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
(palette || []).map((c) => {
|
|
976
|
+
const active = filter.indexOf(c.id) >= 0;
|
|
977
|
+
return h(
|
|
978
|
+
'button',
|
|
979
|
+
{
|
|
980
|
+
key: c.id,
|
|
981
|
+
type: 'button',
|
|
982
|
+
role: 'menuitemcheckbox',
|
|
983
|
+
'aria-checked': active,
|
|
984
|
+
title: c.name,
|
|
985
|
+
onClick: (e) => {
|
|
986
|
+
e.stopPropagation();
|
|
987
|
+
toggleOne(c.id);
|
|
988
|
+
},
|
|
989
|
+
style: {
|
|
990
|
+
display: 'flex',
|
|
991
|
+
alignItems: 'center',
|
|
992
|
+
gap: 8,
|
|
993
|
+
width: '100%',
|
|
994
|
+
boxSizing: 'border-box',
|
|
995
|
+
padding: '6px 10px',
|
|
996
|
+
background: active ? 'var(--dsw-alias-border-l4)' : 'transparent',
|
|
997
|
+
border: 0,
|
|
998
|
+
borderRadius: 6,
|
|
999
|
+
cursor: 'pointer',
|
|
1000
|
+
fontSize: 12,
|
|
1001
|
+
lineHeight: '18px',
|
|
1002
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1003
|
+
textAlign: 'left',
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
h(ColorDot, { hex: c.hex, size: 10 }),
|
|
1007
|
+
h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.name),
|
|
1008
|
+
active && h('span', { 'aria-hidden': true, style: { color: ACCENT, fontWeight: 700 } }, '✓'),
|
|
1009
|
+
);
|
|
1010
|
+
}),
|
|
1011
|
+
h('div', { style: { height: 1, background: 'var(--dsw-alias-border-l4)', margin: '4px 2px' } }),
|
|
1012
|
+
h(
|
|
1013
|
+
'button',
|
|
1014
|
+
{
|
|
1015
|
+
type: 'button',
|
|
1016
|
+
onClick: (e) => {
|
|
1017
|
+
e.stopPropagation();
|
|
1018
|
+
setStore({ filter: [] });
|
|
1019
|
+
setOpen(false);
|
|
1020
|
+
},
|
|
1021
|
+
style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: 'transparent', border: 0, borderRadius: 6, cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-primary)', textAlign: 'left' },
|
|
1022
|
+
},
|
|
1023
|
+
t('showAll'),
|
|
1024
|
+
),
|
|
1025
|
+
h(
|
|
1026
|
+
'button',
|
|
1027
|
+
{
|
|
1028
|
+
type: 'button',
|
|
1029
|
+
onClick: (e) => {
|
|
1030
|
+
e.stopPropagation();
|
|
1031
|
+
refreshWorkspaces();
|
|
1032
|
+
},
|
|
1033
|
+
style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: 'transparent', border: 0, borderRadius: 6, cursor: 'pointer', fontSize: 12, color: 'var(--dsw-alias-label-secondary)', textAlign: 'left' },
|
|
1034
|
+
},
|
|
1035
|
+
t('refresh'),
|
|
1036
|
+
),
|
|
1037
|
+
),
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ---- 4. Settings page: palette + aliases ----
|
|
1042
|
+
function SettingsPage() {
|
|
1043
|
+
const palette = useStoreSel((st) => st.palette);
|
|
1044
|
+
const colors = useStoreSel((st) => st.colors);
|
|
1045
|
+
const [newName, setNewName] = React.useState('');
|
|
1046
|
+
const [newHex, setNewHex] = React.useState('#56B6C2');
|
|
1047
|
+
useWorkspaces();
|
|
1048
|
+
const usage = {};
|
|
1049
|
+
Object.keys(colors || {}).forEach((wsId) => {
|
|
1050
|
+
const cid = colors[wsId];
|
|
1051
|
+
usage[cid] = (usage[cid] || 0) + 1;
|
|
1052
|
+
});
|
|
1053
|
+
const Btn = (primitives && primitives.Button) || 'button';
|
|
1054
|
+
return h(
|
|
1055
|
+
'div',
|
|
1056
|
+
{ style: { maxWidth: 560, padding: '4px 0', display: 'flex', flexDirection: 'column', gap: 12 } },
|
|
1057
|
+
h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', lineHeight: '18px' } }, t('settingsDesc')),
|
|
1058
|
+
(palette || []).map((c) =>
|
|
1059
|
+
h(
|
|
1060
|
+
'div',
|
|
1061
|
+
{ key: c.id, style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
1062
|
+
h('input', {
|
|
1063
|
+
type: 'color',
|
|
1064
|
+
value: c.hex,
|
|
1065
|
+
'aria-label': c.name,
|
|
1066
|
+
onChange: (e) => {
|
|
1067
|
+
const next = getStore().palette.map((x) => (x.id === c.id ? { ...x, hex: e.target.value } : x));
|
|
1068
|
+
setStore({ palette: next });
|
|
1069
|
+
},
|
|
1070
|
+
style: { width: 30, height: 24, padding: 0, border: '1px solid var(--dsw-alias-border-l4)', borderRadius: 4, background: 'transparent', cursor: 'pointer' },
|
|
1071
|
+
}),
|
|
1072
|
+
h('input', {
|
|
1073
|
+
type: 'text',
|
|
1074
|
+
value: c.name,
|
|
1075
|
+
'aria-label': t('alias'),
|
|
1076
|
+
onChange: (e) => {
|
|
1077
|
+
const next = getStore().palette.map((x) => (x.id === c.id ? { ...x, name: e.target.value } : x));
|
|
1078
|
+
setStore({ palette: next });
|
|
1079
|
+
},
|
|
1080
|
+
style: { flex: 1, minWidth: 0, height: 28, padding: '2px 8px', fontSize: 12, borderRadius: 6, border: '1px solid var(--dsw-alias-border-l4)', background: 'transparent', color: 'var(--dsw-alias-label-primary)' },
|
|
1081
|
+
}),
|
|
1082
|
+
h(
|
|
1083
|
+
'span',
|
|
1084
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', whiteSpace: 'nowrap' } },
|
|
1085
|
+
(usage[c.id] || 0) + ' ' + t('inUse'),
|
|
1086
|
+
),
|
|
1087
|
+
h(
|
|
1088
|
+
Btn,
|
|
1089
|
+
{
|
|
1090
|
+
onClick: () => {
|
|
1091
|
+
const st = getStore();
|
|
1092
|
+
const nextPalette = st.palette.filter((x) => x.id !== c.id);
|
|
1093
|
+
if (nextPalette.length === 0) return;
|
|
1094
|
+
const nextColors = { ...st.colors };
|
|
1095
|
+
Object.keys(nextColors).forEach((k) => {
|
|
1096
|
+
if (nextColors[k] === c.id) delete nextColors[k];
|
|
1097
|
+
});
|
|
1098
|
+
setStore({ palette: nextPalette, colors: nextColors, filter: st.filter.filter((id) => id !== c.id) });
|
|
1099
|
+
},
|
|
1100
|
+
disabled: (palette || []).length <= 1,
|
|
1101
|
+
children: t('delete'),
|
|
1102
|
+
},
|
|
1103
|
+
),
|
|
1104
|
+
),
|
|
1105
|
+
),
|
|
1106
|
+
h(
|
|
1107
|
+
'div',
|
|
1108
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
1109
|
+
h('input', {
|
|
1110
|
+
type: 'color',
|
|
1111
|
+
value: newHex,
|
|
1112
|
+
onChange: (e) => setNewHex(e.target.value),
|
|
1113
|
+
style: { width: 30, height: 24, padding: 0, border: '1px solid var(--dsw-alias-border-l4)', borderRadius: 4, background: 'transparent', cursor: 'pointer' },
|
|
1114
|
+
}),
|
|
1115
|
+
h('input', {
|
|
1116
|
+
type: 'text',
|
|
1117
|
+
value: newName,
|
|
1118
|
+
placeholder: t('alias'),
|
|
1119
|
+
onChange: (e) => setNewName(e.target.value),
|
|
1120
|
+
style: { flex: 1, minWidth: 0, height: 28, padding: '2px 8px', fontSize: 12, borderRadius: 6, border: '1px solid var(--dsw-alias-border-l4)', background: 'transparent', color: 'var(--dsw-alias-label-primary)' },
|
|
1121
|
+
}),
|
|
1122
|
+
h(
|
|
1123
|
+
Btn,
|
|
1124
|
+
{
|
|
1125
|
+
onClick: () => {
|
|
1126
|
+
const name = newName.trim();
|
|
1127
|
+
if (!name) return;
|
|
1128
|
+
const st = getStore();
|
|
1129
|
+
setStore({ palette: st.palette.concat([{ id: 'c' + Date.now().toString(36), name, hex: newHex }]) });
|
|
1130
|
+
setNewName('');
|
|
1131
|
+
},
|
|
1132
|
+
children: t('addColor'),
|
|
1133
|
+
},
|
|
1134
|
+
),
|
|
1135
|
+
),
|
|
1136
|
+
h(
|
|
1137
|
+
Btn,
|
|
1138
|
+
{
|
|
1139
|
+
onClick: () => {
|
|
1140
|
+
const st = getStore();
|
|
1141
|
+
const valid = new Set(defaultStore().palette.map((c) => c.id));
|
|
1142
|
+
const nextColors = {};
|
|
1143
|
+
Object.keys(st.colors || {}).forEach((k) => {
|
|
1144
|
+
if (valid.has(st.colors[k])) nextColors[k] = st.colors[k];
|
|
1145
|
+
});
|
|
1146
|
+
setStore({ palette: defaultStore().palette, colors: nextColors, filter: st.filter.filter((id) => valid.has(id)) });
|
|
1147
|
+
},
|
|
1148
|
+
children: t('reset'),
|
|
1149
|
+
},
|
|
1150
|
+
),
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
ctx.slots.inject('sidebar.session.row.hover', () =>
|
|
1155
|
+
ctx.slots.register(
|
|
1156
|
+
{ name: 'sidebar.session.row.hover', id: 'session-cwd-opener-hover', order: -10 },
|
|
1157
|
+
SidebarHoverCwd,
|
|
1158
|
+
),
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
ctx.slots.inject('sidebar.workspaces.session.menu.item', () =>
|
|
1162
|
+
ctx.slots.register(
|
|
1163
|
+
{
|
|
1164
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
1165
|
+
id: 'session-cwd-opener-menu',
|
|
1166
|
+
order: -10,
|
|
1167
|
+
label: () => t('openDir'),
|
|
1168
|
+
},
|
|
1169
|
+
SessionMenuCwd,
|
|
1170
|
+
),
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
ctx.slots.inject('sidebar.footer.action', () =>
|
|
1174
|
+
ctx.slots.register(
|
|
1175
|
+
{ name: 'sidebar.footer.action', id: 'session-cwd-opener-filter', order: 10, label: () => t('filterTitle') },
|
|
1176
|
+
FooterFilter,
|
|
1177
|
+
),
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
ctx.slots.inject('settings.section', () =>
|
|
1181
|
+
ctx.slots.register(
|
|
1182
|
+
{ name: 'settings.section', id: 'session-cwd-opener-colors', order: 30, label: () => t('settingsTitle') },
|
|
1183
|
+
SettingsPage,
|
|
1184
|
+
),
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
// keep workspace map fresh: initial + on session-list change (debounced)
|
|
1188
|
+
let wsTimer = 0;
|
|
1189
|
+
refreshWorkspaces();
|
|
1190
|
+
try {
|
|
1191
|
+
subscribeSessions(() => {
|
|
1192
|
+
if (wsTimer) return;
|
|
1193
|
+
wsTimer = setTimeout(() => {
|
|
1194
|
+
wsTimer = 0;
|
|
1195
|
+
refreshWorkspaces();
|
|
1196
|
+
}, 600);
|
|
1197
|
+
});
|
|
1198
|
+
} catch (e) {}
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
},
|
|
1202
|
+
});
|