@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.
Files changed (3) hide show
  1. package/README.md +36 -3
  2. package/dist/client.js +255 -12
  3. 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. File matching stays
53
- Host-ranked path matching (see limitations).
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 the current workspace…',
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
- '@media (prefers-reduced-motion:reduce){.dshcp-backdrop,.dshcp-panel{animation:none}.dshcp-badge[data-tone=running] .dshcp-badgeDot{animation:none}}'
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' && currentSessionId === undefined ? t('empty.files.noSession') : t('placeholder');
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) uiWorkspace.openSession(candidates[0]);
1190
- else uiWorkspace.openWorkspace(workspaceId).catch((error) => console.warn('command palette: open workspace failed', error));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qzsy/dsh-command-palette",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Command+K palette for recent sessions, workspaces, files, and actions with pinyin search and live session status.",
5
5
  "type": "module",
6
6
  "license": "MIT",