@qzsy/dsh-command-palette 1.2.0 → 1.3.1
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 +36 -3
- package/dist/client.js +255 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,10 @@ initials, or any mixed form.
|
|
|
30
30
|
The section label counts active Sessions, e.g. `近期会话 · 2 个进行中`; the
|
|
31
31
|
count updates live while the palette stays open. When the Git badge bundle
|
|
32
32
|
is installed, the Workspace name under each thread title also carries that
|
|
33
|
-
Workspace's `+N −M` / `干净` / `无 Git` badge.
|
|
33
|
+
Workspace's `+N −M` / `干净` / `无 Git` badge. Choosing a Session also
|
|
34
|
+
reveals it in the left Workspace sidebar: a folded Workspace (and every
|
|
35
|
+
folded Workspace Tree ancestor) expands, the Session row watermark rises,
|
|
36
|
+
the group smooth-scrolls into view, and the Workspace header flashes.
|
|
34
37
|
- **Sub-agent threads are hidden by default.** Sessions whose
|
|
35
38
|
`origin === "subagent"` are excluded from rows, search results, workspace
|
|
36
39
|
activity badges and counts. When any exist, the footer shows
|
|
@@ -48,9 +51,12 @@ initials, or any mixed form.
|
|
|
48
51
|
or a fresh blank Session when it has none.
|
|
49
52
|
- *Files*: live, debounced Host file-reference lookup scoped to the Session in
|
|
50
53
|
the main panel; choosing a row opens the file in the right-hand preview.
|
|
54
|
+
Every file row carries a `项目 · <Workspace>` badge below its path so the
|
|
55
|
+
search scope stays visible, and the Files placeholder names the same project.
|
|
51
56
|
Opened files are remembered locally (origin-scoped `localStorage`) and listed
|
|
52
|
-
as *Recent files* when the search box is empty
|
|
53
|
-
Host-ranked path
|
|
57
|
+
as *Recent files* when the search box is empty; recent files resolve their
|
|
58
|
+
project from the Session's Workspace. File matching stays Host-ranked path
|
|
59
|
+
matching (see limitations).
|
|
54
60
|
- *Actions*: every registered shortcut command (labels and keys come from the
|
|
55
61
|
live shortcut catalog, so plugin commands appear automatically). Enter
|
|
56
62
|
invokes the command through the shortcut registry; Settings and the keyboard
|
|
@@ -105,6 +111,33 @@ The palette reads the root `useSessionStatus` hook of the `shell.overlay` slot
|
|
|
105
111
|
badges and counts change live as agents start, wait for input, and finish.
|
|
106
112
|
Pending states take precedence over running, because they need the user.
|
|
107
113
|
|
|
114
|
+
## Sidebar reveal
|
|
115
|
+
|
|
116
|
+
Selecting a Session or Workspace closes the palette, navigates, and then reveals
|
|
117
|
+
the target in the left Workspace browser. The browser owns reveal for its own
|
|
118
|
+
search in private React state, so the palette drives the stable DOM markers the
|
|
119
|
+
browser publishes instead: `data-row-key` identifies Workspace / Session /
|
|
120
|
+
watermark rows, `aria-expanded` reports a folded Workspace, and `role="tree"`
|
|
121
|
+
is the scrolling list. The persisted `dsh.workspace.view.v5` store supplies the
|
|
122
|
+
grouping mode, which distinguishes sibling Workspace groups from Workspace Tree
|
|
123
|
+
ancestry.
|
|
124
|
+
|
|
125
|
+
The reveal is bounded (about 2.4 s of retries): it expands the owning group and
|
|
126
|
+
its tree ancestors, raises hidden session rows through the same **Show more**
|
|
127
|
+
control the browser owns, then scrolls the group and chosen row into view with
|
|
128
|
+
`behavior: "smooth"` (or `auto` under reduced motion) and briefly rings the
|
|
129
|
+
Workspace header with a `--dsw-*` token outline. If the sidebar is a collapsed
|
|
130
|
+
rail, or the target is hidden by the current archive filter, the reveal is a
|
|
131
|
+
silent no-op — navigation still succeeds.
|
|
132
|
+
|
|
133
|
+
## Marketplace palette
|
|
134
|
+
|
|
135
|
+
The DSH Market landing page in this repository (`apps/market`) mounts the same
|
|
136
|
+
keyboard-first interaction as a live demo: `⌘K` / `Ctrl+K` searches the plugin
|
|
137
|
+
directory, opens plugin pages, and offers the install command. The palette footer
|
|
138
|
+
credits this package, and `@qzsy` packages stay pinned ahead of upstream
|
|
139
|
+
duplicates so the command palette itself is always one keystroke away.
|
|
140
|
+
|
|
108
141
|
## Shortcut ownership
|
|
109
142
|
|
|
110
143
|
`session.search` already owns `Command+K` in the shipped UI. The palette registers
|
package/dist/client.js
CHANGED
|
@@ -34,7 +34,7 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
const zh = {
|
|
35
35
|
'command.open': '打开命令面板',
|
|
36
36
|
'placeholder': '搜索会话、工作区、文件、操作…(支持拼音)',
|
|
37
|
-
'placeholder.files': '
|
|
37
|
+
'placeholder.files': '在项目 {name} 中搜索文件…',
|
|
38
38
|
'tab.all': '全部',
|
|
39
39
|
'tab.sessions': '会话',
|
|
40
40
|
'tab.workspaces': '工作区',
|
|
@@ -49,6 +49,7 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
'group.workspaces.match': '工作区',
|
|
50
50
|
'group.files': '文件',
|
|
51
51
|
'group.files.recent': '近期文件',
|
|
52
|
+
'file.project': '项目 · {name}',
|
|
52
53
|
'group.actions': '操作',
|
|
53
54
|
'status.running': '工作中',
|
|
54
55
|
'status.pending': '待处理',
|
|
@@ -89,7 +90,7 @@ window.__ModuleLoader__.load({
|
|
|
89
90
|
const en = {
|
|
90
91
|
'command.open': 'Open command palette',
|
|
91
92
|
'placeholder': 'Search sessions, workspaces, files, actions… (pinyin supported)',
|
|
92
|
-
'placeholder.files': 'Search files in
|
|
93
|
+
'placeholder.files': 'Search files in {name}…',
|
|
93
94
|
'tab.all': 'All',
|
|
94
95
|
'tab.sessions': 'Sessions',
|
|
95
96
|
'tab.workspaces': 'Workspaces',
|
|
@@ -104,6 +105,7 @@ window.__ModuleLoader__.load({
|
|
|
104
105
|
'group.workspaces.match': 'Workspaces',
|
|
105
106
|
'group.files': 'Files',
|
|
106
107
|
'group.files.recent': 'Recent files',
|
|
108
|
+
'file.project': 'Project · {name}',
|
|
107
109
|
'group.actions': 'Actions',
|
|
108
110
|
'status.running': 'Working',
|
|
109
111
|
'status.pending': 'Needs input',
|
|
@@ -229,6 +231,226 @@ window.__ModuleLoader__.load({
|
|
|
229
231
|
}
|
|
230
232
|
//#endregion
|
|
231
233
|
|
|
234
|
+
//#region project labels
|
|
235
|
+
/**
|
|
236
|
+
* Files always belong to the Workspace of the active Session. The palette
|
|
237
|
+
* projects that name into each file row so a search result never leaves the
|
|
238
|
+
* reader guessing which project it came from.
|
|
239
|
+
*/
|
|
240
|
+
function workspaceForSession(workspaces, sessionId) {
|
|
241
|
+
if (sessionId === undefined || sessionId === null) return undefined;
|
|
242
|
+
for (const workspace of workspaces?.items ?? []) {
|
|
243
|
+
if ((workspace.sessionIds ?? []).includes(sessionId)) return workspace;
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
function workspaceProjectName(workspace, t) {
|
|
248
|
+
if (workspace === undefined || workspace === null) return t('item.noWorkspace');
|
|
249
|
+
const title = typeof workspace.title === 'string' ? workspace.title.trim() : '';
|
|
250
|
+
return title !== '' ? title : basename(workspace.path ?? '');
|
|
251
|
+
}
|
|
252
|
+
function projectNameForSession(sessions, workspaces, sessionId, t) {
|
|
253
|
+
const workspace = workspaceForSession(workspaces, sessionId);
|
|
254
|
+
if (workspace !== undefined) return workspaceProjectName(workspace, t);
|
|
255
|
+
const row = sessionId === undefined ? undefined : sessions?.byId?.[sessionId];
|
|
256
|
+
if (row !== undefined && typeof row.cwd === 'string' && row.cwd !== '') return basename(row.cwd);
|
|
257
|
+
return t('item.noWorkspace');
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
|
|
261
|
+
//#region sidebar reveal
|
|
262
|
+
/**
|
|
263
|
+
* The Workspace browser owns search-result reveal in private React state, so
|
|
264
|
+
* another plugin cannot ask it to scroll. The palette instead drives the
|
|
265
|
+
* stable row markers it renders: `data-row-key` identifies Workspace,
|
|
266
|
+
* Session, and overflow rows; `aria-expanded` reports a folded Workspace;
|
|
267
|
+
* and `role="tree"` is the scrolling list. The persisted view store tells us
|
|
268
|
+
* whether descendants are nested (Workspace Tree) or sibling groups.
|
|
269
|
+
*/
|
|
270
|
+
const REVEAL_TIMEOUT_MS = 2400;
|
|
271
|
+
const REVEAL_STEP_MS = 50;
|
|
272
|
+
const REVEAL_MAX_STEPS = 48;
|
|
273
|
+
const REVEAL_PADDING = 8;
|
|
274
|
+
const REVEAL_HIGHLIGHT_MS = 1400;
|
|
275
|
+
const REVEAL_HIGHLIGHT_ATTR = 'data-dshcp-reveal';
|
|
276
|
+
const VIEW_STORE_KEY = 'dsh.workspace.view.v5';
|
|
277
|
+
/** Later reveals supersede earlier retry loops so two quick choices cannot fight. */
|
|
278
|
+
let revealSequence = 0;
|
|
279
|
+
|
|
280
|
+
/** One row by its stable key; IDs are compared literally to avoid selector escaping. */
|
|
281
|
+
function sidebarRow(key) {
|
|
282
|
+
if (typeof document === 'undefined') return null;
|
|
283
|
+
for (const element of document.querySelectorAll('[data-row-key]')) {
|
|
284
|
+
if (element.getAttribute('data-row-key') === key) return element;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
/** Whether the browser currently renders Workspace group headers. */
|
|
289
|
+
function sidebarHasGroups() {
|
|
290
|
+
if (typeof document === 'undefined') return false;
|
|
291
|
+
for (const element of document.querySelectorAll('[data-row-key]')) {
|
|
292
|
+
if ((element.getAttribute('data-row-key') ?? '').startsWith('workspace:')) return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
/** The saved grouping mode; an unreadable store falls back to flat-group behaviour. */
|
|
297
|
+
function sidebarGroupMode() {
|
|
298
|
+
try {
|
|
299
|
+
const raw = window.localStorage.getItem(VIEW_STORE_KEY);
|
|
300
|
+
if (raw === null) return undefined;
|
|
301
|
+
const parsed = JSON.parse(raw);
|
|
302
|
+
return typeof parsed?.groupBy === 'string' ? parsed.groupBy : undefined;
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Normalize separators without treating POSIX backslashes as separators. */
|
|
308
|
+
function folderPath(path) {
|
|
309
|
+
return (/^[A-Za-z]:[/\\]/u.test(path) || path.startsWith('\\\\') ? path.replaceAll('\\', '/') : path).replace(/\/+$/u, '');
|
|
310
|
+
}
|
|
311
|
+
/** Nearest registered ancestor directory, matching the browser's tree derivation. */
|
|
312
|
+
function owningParentPath(path, paths) {
|
|
313
|
+
const child = folderPath(path);
|
|
314
|
+
let owner;
|
|
315
|
+
let length = -1;
|
|
316
|
+
for (const parent of paths) {
|
|
317
|
+
const root = folderPath(parent);
|
|
318
|
+
if (root.length > length && child !== root && child.startsWith(`${root}/`)) {
|
|
319
|
+
owner = parent;
|
|
320
|
+
length = root.length;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return owner;
|
|
324
|
+
}
|
|
325
|
+
/** Workspace Tree ancestry, root first, ending with the target Workspace. */
|
|
326
|
+
function workspaceChain(workspaceId, snapshot) {
|
|
327
|
+
if (workspaceId === undefined || workspaceId === null) return [''];
|
|
328
|
+
const items = snapshot?.items ?? [];
|
|
329
|
+
const target = items.find((item) => item.workspaceId === workspaceId);
|
|
330
|
+
if (target === undefined) return [workspaceId];
|
|
331
|
+
const paths = items.map((item) => item.path).filter((path) => typeof path === 'string' && path !== '');
|
|
332
|
+
const chain = [workspaceId];
|
|
333
|
+
const seen = new Set(chain);
|
|
334
|
+
let path = target.path;
|
|
335
|
+
while (typeof path === 'string') {
|
|
336
|
+
const parentPath = owningParentPath(path, paths);
|
|
337
|
+
if (parentPath === undefined) break;
|
|
338
|
+
const parent = items.find((item) => item.path === parentPath);
|
|
339
|
+
if (parent === undefined || seen.has(parent.workspaceId)) break;
|
|
340
|
+
chain.unshift(parent.workspaceId);
|
|
341
|
+
seen.add(parent.workspaceId);
|
|
342
|
+
path = parent.path;
|
|
343
|
+
}
|
|
344
|
+
return chain;
|
|
345
|
+
}
|
|
346
|
+
/** Smoothly bring the Workspace header and its chosen Session into view together when they fit. */
|
|
347
|
+
function revealScroll(header, row) {
|
|
348
|
+
const scroller = (row ?? header)?.closest('[role="tree"]');
|
|
349
|
+
if (scroller === null || scroller === undefined) {
|
|
350
|
+
(row ?? header)?.scrollIntoView({ block: 'nearest' });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const viewport = scroller.getBoundingClientRect();
|
|
354
|
+
const headerRect = header?.getBoundingClientRect();
|
|
355
|
+
const rowRect = row?.getBoundingClientRect();
|
|
356
|
+
const headerVisible = headerRect === undefined || (headerRect.top >= viewport.top + REVEAL_PADDING && headerRect.bottom <= viewport.bottom - REVEAL_PADDING);
|
|
357
|
+
const rowVisible = rowRect === undefined || (rowRect.top >= viewport.top + REVEAL_PADDING && rowRect.bottom <= viewport.bottom - REVEAL_PADDING);
|
|
358
|
+
if (headerVisible && rowVisible) return;
|
|
359
|
+
const relativeTop = (rect) => rect.top - viewport.top + scroller.scrollTop;
|
|
360
|
+
let top;
|
|
361
|
+
if (headerRect !== undefined && rowRect !== undefined) {
|
|
362
|
+
const headerTop = relativeTop(headerRect);
|
|
363
|
+
const rowTop = relativeTop(rowRect);
|
|
364
|
+
const bothFit = rowTop + rowRect.height - headerTop + REVEAL_PADDING * 2 <= scroller.clientHeight;
|
|
365
|
+
top = bothFit ? headerTop - REVEAL_PADDING : rowTop - Math.max(REVEAL_PADDING, (scroller.clientHeight - rowRect.height) / 2);
|
|
366
|
+
} else if (headerRect !== undefined) {
|
|
367
|
+
top = relativeTop(headerRect) - REVEAL_PADDING;
|
|
368
|
+
} else if (rowRect !== undefined) {
|
|
369
|
+
top = relativeTop(rowRect) - Math.max(REVEAL_PADDING, (scroller.clientHeight - rowRect.height) / 2);
|
|
370
|
+
} else return;
|
|
371
|
+
const max = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
372
|
+
const next = Math.min(Math.max(top, 0), max);
|
|
373
|
+
if (Math.abs(next - scroller.scrollTop) < 1) return;
|
|
374
|
+
const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
375
|
+
if (typeof scroller.scrollTo === 'function') scroller.scrollTo({ top: next, behavior: reduced ? 'auto' : 'smooth' });
|
|
376
|
+
else scroller.scrollTop = next;
|
|
377
|
+
}
|
|
378
|
+
/** Briefly ring the revealed Workspace/Session without owning its classes. */
|
|
379
|
+
function revealHighlight(element) {
|
|
380
|
+
if (element === null || element === undefined) return;
|
|
381
|
+
element.setAttribute(REVEAL_HIGHLIGHT_ATTR, '');
|
|
382
|
+
window.setTimeout(() => {
|
|
383
|
+
if (element.isConnected) element.removeAttribute(REVEAL_HIGHLIGHT_ATTR);
|
|
384
|
+
}, REVEAL_HIGHLIGHT_MS);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* One reveal step. Returns `done` when the target is visible, or `wait`
|
|
388
|
+
* when a click changed the tree and another paint is required.
|
|
389
|
+
*/
|
|
390
|
+
function revealSidebarStep(sessionId, workspaceId, snapshot) {
|
|
391
|
+
const groupKey = workspaceId === undefined || workspaceId === null ? '' : workspaceId;
|
|
392
|
+
const groupMode = sidebarGroupMode();
|
|
393
|
+
const sessionRow = sessionId === undefined ? null : sidebarRow(`session:${sessionId}`);
|
|
394
|
+
if (sessionRow !== null) {
|
|
395
|
+
const header = sidebarRow(`workspace:${groupKey}`);
|
|
396
|
+
if (header !== null && header.getAttribute('aria-expanded') === 'false') {
|
|
397
|
+
header.click();
|
|
398
|
+
return 'wait';
|
|
399
|
+
}
|
|
400
|
+
revealScroll(header, sessionRow);
|
|
401
|
+
revealHighlight(header ?? sessionRow);
|
|
402
|
+
return 'done';
|
|
403
|
+
}
|
|
404
|
+
const grouped = groupMode !== 'flat' && sidebarHasGroups();
|
|
405
|
+
if (grouped) {
|
|
406
|
+
const chain = groupMode === 'workspace-tree' ? workspaceChain(workspaceId, snapshot) : [groupKey];
|
|
407
|
+
for (const key of chain) {
|
|
408
|
+
const header = sidebarRow(`workspace:${key}`);
|
|
409
|
+
if (header === null) {
|
|
410
|
+
/* In tree mode a folded ancestor keeps the child header unmounted. */
|
|
411
|
+
return groupMode === 'workspace-tree' ? 'wait' : 'done';
|
|
412
|
+
}
|
|
413
|
+
if (header.getAttribute('aria-expanded') === 'false') {
|
|
414
|
+
header.click();
|
|
415
|
+
return 'wait';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const overflow = sidebarRow(`overflow:${groupKey}`);
|
|
419
|
+
if (overflow !== null) {
|
|
420
|
+
overflow.click();
|
|
421
|
+
return 'wait';
|
|
422
|
+
}
|
|
423
|
+
const header = sidebarRow(`workspace:${groupKey}`);
|
|
424
|
+
if (header !== null) {
|
|
425
|
+
revealScroll(header, null);
|
|
426
|
+
revealHighlight(header);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return 'done';
|
|
430
|
+
}
|
|
431
|
+
/** Reveal once after the palette closes, retrying while React commits expansion. */
|
|
432
|
+
function revealSidebarTarget(sessionId, workspaceId, getSnapshot) {
|
|
433
|
+
if (typeof document === 'undefined' || typeof window === 'undefined') return;
|
|
434
|
+
const sequence = (revealSequence += 1);
|
|
435
|
+
const startedAt = Date.now();
|
|
436
|
+
let steps = 0;
|
|
437
|
+
const tick = () => {
|
|
438
|
+
if (sequence !== revealSequence) return;
|
|
439
|
+
try {
|
|
440
|
+
const outcome = revealSidebarStep(sessionId, workspaceId, getSnapshot());
|
|
441
|
+
if (outcome === 'done') return;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.warn('command palette: sidebar reveal failed', error);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
steps += 1;
|
|
447
|
+
if (steps >= REVEAL_MAX_STEPS || Date.now() - startedAt >= REVEAL_TIMEOUT_MS) return;
|
|
448
|
+
window.setTimeout(tick, REVEAL_STEP_MS);
|
|
449
|
+
};
|
|
450
|
+
window.setTimeout(tick, 20);
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
|
|
232
454
|
//#region search
|
|
233
455
|
/**
|
|
234
456
|
* The engine concatenates fields per item and returns inclusive UTF-16 hit
|
|
@@ -464,7 +686,11 @@ window.__ModuleLoader__.load({
|
|
|
464
686
|
'.dshcp-hintKey{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border:0.5px solid var(--dsw-alias-border-l2);border-radius:5px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}',
|
|
465
687
|
'.dshcp-empty{padding:30px 16px;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}',
|
|
466
688
|
'.dshcp-status{padding:12px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
|
|
467
|
-
'
|
|
689
|
+
'.dshcp-projectRow{display:flex;min-width:0;max-width:100%;margin-top:2px;overflow:hidden}',
|
|
690
|
+
'.dshcp-projectBadge{display:inline-flex;align-items:center;max-width:100%;height:19px;padding:0 7px;border-radius:999px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
691
|
+
'[data-dshcp-reveal]{animation:dshcp-reveal 1.2s var(--ds-ease-out,ease-out)}',
|
|
692
|
+
'@keyframes dshcp-reveal{0%{box-shadow:inset 0 0 0 2px var(--dsw-alias-state-business-primary)}100%{box-shadow:inset 0 0 0 2px transparent}}',
|
|
693
|
+
'@media (prefers-reduced-motion:reduce){.dshcp-backdrop,.dshcp-panel{animation:none}.dshcp-badge[data-tone=running] .dshcp-badgeDot{animation:none}[data-dshcp-reveal]{animation:none;box-shadow:inset 0 0 0 2px var(--dsw-alias-state-business-primary)}}'
|
|
468
694
|
].join('');
|
|
469
695
|
const STYLE_ATTR = 'data-dsh-command-palette';
|
|
470
696
|
function installStyles() {
|
|
@@ -513,6 +739,7 @@ window.__ModuleLoader__.load({
|
|
|
513
739
|
key: `session:${row.id}`,
|
|
514
740
|
kind: 'session',
|
|
515
741
|
sessionId: row.id,
|
|
742
|
+
workspaceId: workspace?.workspaceId,
|
|
516
743
|
title,
|
|
517
744
|
subtitle: workspaceLabel,
|
|
518
745
|
detail: workspaceLabel,
|
|
@@ -575,7 +802,7 @@ window.__ModuleLoader__.load({
|
|
|
575
802
|
}
|
|
576
803
|
return rows;
|
|
577
804
|
}
|
|
578
|
-
function fileItem(candidate) {
|
|
805
|
+
function fileItem(candidate, project) {
|
|
579
806
|
const path = candidate.path;
|
|
580
807
|
const directory = candidate.kind === 'directory';
|
|
581
808
|
const name = basename(path);
|
|
@@ -584,17 +811,19 @@ window.__ModuleLoader__.load({
|
|
|
584
811
|
kind: 'file',
|
|
585
812
|
title: directory ? `${name}/` : name,
|
|
586
813
|
subtitle: dirname(path),
|
|
814
|
+
project,
|
|
587
815
|
path,
|
|
588
816
|
directory,
|
|
589
817
|
recency: 0
|
|
590
818
|
};
|
|
591
819
|
}
|
|
592
|
-
function recentFileItem(entry, now, t) {
|
|
820
|
+
function recentFileItem(entry, now, t, project) {
|
|
593
821
|
return {
|
|
594
822
|
key: `recent:${entry.sessionId}:${entry.path}`,
|
|
595
823
|
kind: 'file',
|
|
596
824
|
title: basename(entry.path),
|
|
597
825
|
subtitle: dirname(entry.path),
|
|
826
|
+
project,
|
|
598
827
|
path: entry.path,
|
|
599
828
|
sessionId: entry.sessionId,
|
|
600
829
|
meta: relativeAge(entry.at, now, t),
|
|
@@ -714,6 +943,7 @@ window.__ModuleLoader__.load({
|
|
|
714
943
|
const sessionRows = useMemo(() => sessionItems(sessions, workspaces, now, t, statuses, gitEntries), [sessions, workspaces, now, t, statuses, gitEntries]);
|
|
715
944
|
const subagentCount = useMemo(() => sessionRows.reduce((count, row) => count + (row.subagent === true ? 1 : 0), 0), [sessionRows]);
|
|
716
945
|
const visibleSessionRows = useMemo(() => includeSubagents ? sessionRows : sessionRows.filter((row) => row.subagent !== true), [sessionRows, includeSubagents]);
|
|
946
|
+
const currentProject = useMemo(() => currentSessionId === undefined ? undefined : projectNameForSession(sessions, workspaces, currentSessionId, t), [currentSessionId, sessions, workspaces, t]);
|
|
717
947
|
const workspaceRows = useMemo(() => workspaceItems(sessions, workspaces, now, t, home, statuses, includeSubagents, gitEntries), [sessions, workspaces, now, t, home, statuses, includeSubagents, gitEntries]);
|
|
718
948
|
const gitPaths = useMemo(() => (workspaces?.items ?? []).map((workspace) => workspace.path).filter((path) => typeof path === 'string' && path.length > 0), [workspaces]);
|
|
719
949
|
useEffect(() => {
|
|
@@ -727,8 +957,8 @@ window.__ModuleLoader__.load({
|
|
|
727
957
|
return () => { cancelled = true; };
|
|
728
958
|
}, [gitPaths]);
|
|
729
959
|
const actionRows = useMemo(() => actionItems(catalog), [catalog]);
|
|
730
|
-
const recentRows = useMemo(() => recents.map((entry) => recentFileItem(entry, now, t)), [recents, now, t]);
|
|
731
|
-
const fileRows = useMemo(() => files.map((candidate) => fileItem(candidate)), [files]);
|
|
960
|
+
const recentRows = useMemo(() => recents.map((entry) => recentFileItem(entry, now, t, projectNameForSession(sessions, workspaces, entry.sessionId, t))), [recents, now, t, sessions, workspaces]);
|
|
961
|
+
const fileRows = useMemo(() => files.map((candidate) => fileItem(candidate, currentProject)), [files, currentProject]);
|
|
732
962
|
const sessionSearcher = useMemo(() => buildItemSearcher(visibleSessionRows), [visibleSessionRows]);
|
|
733
963
|
const workspaceSearcher = useMemo(() => buildItemSearcher(workspaceRows), [workspaceRows]);
|
|
734
964
|
const actionSearcher = useMemo(() => buildItemSearcher(actionRows), [actionRows]);
|
|
@@ -819,7 +1049,7 @@ window.__ModuleLoader__.load({
|
|
|
819
1049
|
if (item === undefined) return;
|
|
820
1050
|
actions.close();
|
|
821
1051
|
if (item.kind === 'session') {
|
|
822
|
-
actions.openSession(item.sessionId);
|
|
1052
|
+
actions.openSession(item.sessionId, item.workspaceId);
|
|
823
1053
|
return;
|
|
824
1054
|
}
|
|
825
1055
|
if (item.kind === 'workspace') {
|
|
@@ -948,6 +1178,10 @@ window.__ModuleLoader__.load({
|
|
|
948
1178
|
? h('span', { className: 'dshcp-subtitleRow' },
|
|
949
1179
|
h('span', { className: 'dshcp-subtitle' }, renderHighlighted(item.subtitle, subtitleRanges)),
|
|
950
1180
|
item.kind === 'session' ? gitBadge(item.git, t) : null)
|
|
1181
|
+
: null,
|
|
1182
|
+
item.kind === 'file' && typeof item.project === 'string' && item.project !== ''
|
|
1183
|
+
? h('span', { className: 'dshcp-projectRow' },
|
|
1184
|
+
h('span', { className: 'dshcp-projectBadge', title: item.project }, t('file.project', { name: item.project })))
|
|
951
1185
|
: null),
|
|
952
1186
|
item.badge !== null && item.badge !== undefined
|
|
953
1187
|
? h('span', { className: 'dshcp-badge', 'data-tone': item.badge.tone }, h('span', { className: 'dshcp-badgeDot' }), item.badge.label)
|
|
@@ -964,7 +1198,9 @@ window.__ModuleLoader__.load({
|
|
|
964
1198
|
rows.push(h('div', { key: 'files-error', className: 'dshcp-status' }, t('files.error')));
|
|
965
1199
|
}
|
|
966
1200
|
|
|
967
|
-
const placeholder = filter === 'files'
|
|
1201
|
+
const placeholder = filter === 'files'
|
|
1202
|
+
? currentSessionId === undefined ? t('empty.files.noSession') : t('placeholder.files', { name: currentProject ?? t('item.noWorkspace') })
|
|
1203
|
+
: t('placeholder');
|
|
968
1204
|
return h(React.Fragment, null,
|
|
969
1205
|
h('div', { className: 'dshcp-layer' },
|
|
970
1206
|
h('div', { className: 'dshcp-backdrop', onMouseDown: close }),
|
|
@@ -1176,8 +1412,9 @@ window.__ModuleLoader__.load({
|
|
|
1176
1412
|
toggle: () => store.set((state) => ({ open: !state.open })),
|
|
1177
1413
|
close: () => store.set({ open: false }),
|
|
1178
1414
|
home: () => ctx.remote?.$host?.home,
|
|
1179
|
-
openSession: (sessionId) => {
|
|
1415
|
+
openSession: (sessionId, workspaceId) => {
|
|
1180
1416
|
uiWorkspace.openSession(sessionId);
|
|
1417
|
+
revealSidebarTarget(sessionId, workspaceId, () => workspaces.list.getSnapshot());
|
|
1181
1418
|
},
|
|
1182
1419
|
openWorkspace: (workspaceId) => {
|
|
1183
1420
|
const snapshot = sessions.list.getSnapshot();
|
|
@@ -1186,8 +1423,14 @@ window.__ModuleLoader__.load({
|
|
|
1186
1423
|
const candidates = (workspace.sessionIds ?? [])
|
|
1187
1424
|
.filter((sessionId) => snapshot.byId[sessionId] !== undefined && snapshot.byId[sessionId].blank !== true)
|
|
1188
1425
|
.sort((left, right) => (snapshot.byId[right].updatedAt ?? 0) - (snapshot.byId[left].updatedAt ?? 0));
|
|
1189
|
-
if (candidates.length > 0)
|
|
1190
|
-
|
|
1426
|
+
if (candidates.length > 0) {
|
|
1427
|
+
uiWorkspace.openSession(candidates[0]);
|
|
1428
|
+
revealSidebarTarget(candidates[0], workspaceId, () => workspaces.list.getSnapshot());
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
uiWorkspace.openWorkspace(workspaceId)
|
|
1432
|
+
.then(() => revealSidebarTarget(undefined, workspaceId, () => workspaces.list.getSnapshot()))
|
|
1433
|
+
.catch((error) => console.warn('command palette: open workspace failed', error));
|
|
1191
1434
|
},
|
|
1192
1435
|
lookupFiles: (sessionId, query, signal) => ctx.remote.fileReferences.list(sessionId, query, signal),
|
|
1193
1436
|
openFile: (sessionId, path) => {
|
package/package.json
CHANGED