dsh-reference-anything 0.4.0 → 0.4.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 CHANGED
@@ -70,18 +70,16 @@ Type `@`, search across enabled sources, and insert the selected reference into
70
70
 
71
71
  **The External conversations group** — and only that group — uses OpenCLI to reach AI chat sessions you are already logged into. By default it stores conversation titles locally and the agent fetches remote content on demand; an optional offline-mirror mode stores the latest complete bodies locally instead. **Local agent conversations need none of that**: those transcripts are already files on your own disk, so that group reads them directly, with no browser, no OpenCLI, and nothing mirrored.
72
72
 
73
- > [!IMPORTANT]
74
- > v0.4.0 targets DSH `>=0.1.2-rc.1` and uses the RC1 Skills Remote, Session snapshot API, native `@` trigger menu, official file/session Remotes, and native Composer reference rendering. It is incompatible with DSH versions older than `0.1.2-rc.1`.
75
- > The `0.3.x` line remains on the current stable DSH SDK (`0.1.0-rc.8 ~ 0.1.1-rc.2`). Due to breaking changes, starting with version `v0.4.0`, development releases will target DSH `v0.1.2-rc.1` or newer, use the npm `alpha` tag, and no longer support older DSH releases.
76
-
77
73
  > [!NOTE]
78
74
  > DSH is currently in Beta, so its underlying capabilities and interfaces may change as it evolves. This plugin will adapt alongside those changes. Because of some current DSH limitations, parts of the implementation may not yet be ideal; we will continue to follow DSH updates and improve the plugin over time. See the relevant sections below for specific limitations and usage notes.
79
75
 
80
76
  ## 📰 News
81
77
 
78
+ - **2026-09-25 · v0.4.1** — We're back!!🎉🎉 Improved local-agent session titles by reading Codex's own `session_index.jsonl` and added Pi/Codex title support. Restored the native DSH handling for `@` menu group headings to avoid translucent overlap and unnecessary custom blur rendering; bounded index reads to 16 MiB for predictable resource use.
79
+
82
80
  - **2026-09-03 · v0.4.0** — **Compatibility: DSH `>=0.1.2-rc.1`.** Migrated Skills discovery to the DSH RC1 Skills Remote, restored historical reference grants through the RC1 Session snapshot API, and verified the plugin against DSH `0.1.2-rc.1`.
83
81
 
84
- - **2026-08-28 · v0.3.3** — Improved dark-theme backgrounds for the reference menu and settings panels, added project screenshot metadata, and refreshed the documentation. This is the final release line targeting the current stable DeepSeek Harness SDK; the next development release, `v0.4.0`, will migrate to DeepSeek Harness `v0.1.2-rc.1` under the npm `alpha` tag and will not support older DSH releases.
82
+ - **2026-08-28 · v0.3.3** — Improved dark-theme backgrounds for the reference menu and settings panels, added project screenshot metadata, and refreshed the documentation. This was the final release line targeting the current stable DeepSeek Harness SDK; `v0.4.0` migrated to DeepSeek Harness `v0.1.2-rc.1` under the npm `alpha` tag and does not support older DSH releases.
85
83
  - **2026-08-25 · v0.3.2** — Added local-agent conversation references, with detected session counts and custom-folder support. Web AI sync can be age-limited; cloud drives now support folder browsing and quick download-folder access. Settings include update notes, and local-agent tool details stay concise unless expanded.
86
84
  - **2026-08-20 · v0.3.0** — Completed the native DSH `@` integration: five independently configurable sources, official file/session Remotes, native Composer references with source logos, in-place expand/collapse and sync actions, and a one-click switch between Reference Anything and the official DSH `@` list. The legacy `dsh-file:` protocol and custom Composer interaction layer were removed.
87
85
 
@@ -110,7 +108,7 @@ Install the stable `0.3.x` line for the current stable DSH SDK:
110
108
  dsh plugin --profile web add dsh-reference-anything@latest
111
109
  ```
112
110
 
113
- Install `v0.4.0` on DSH `>=0.1.2-rc.1`:
111
+ Install `v0.4.1` on DSH `>=0.1.2-rc.1`:
114
112
 
115
113
  ```powershell
116
114
  dsh plugin --profile web add dsh-reference-anything@alpha
package/lib/client.js CHANGED
@@ -29617,8 +29617,6 @@ var css = `
29617
29617
  caret, selection, wrapping, and message geometry. */
29618
29618
  [data-composer-card] [role="listbox"]:has([role="presentation"][data-source]){box-sizing:border-box!important;width:100%!important;min-width:0!important;max-width:100%!important}
29619
29619
  [data-trigger-menu]:has(> [role="listbox"] [role="presentation"][data-source]){border-radius:22px}
29620
- [data-composer-card] [role="listbox"] [role="presentation"][data-source]{position:sticky;top:0;z-index:2;box-sizing:border-box;padding-top:6px!important;background:var(--dsw-specific-menu,var(--dsw-alias-background-primary,var(--dsw-alias-bg-layer-1,#fff)))}
29621
- [data-composer-card] [role="listbox"] [role="presentation"][data-source]:not(:first-child){margin-top:8px}
29622
29620
  [data-composer-card] [role="listbox"] [role="option"]{scroll-margin-block-start:30px;scroll-margin-block-end:4px}
29623
29621
  [data-composer-card] [role="listbox"]>[data-dsh-ref-menu-settling]{overflow-anchor:none!important}
29624
29622
  [data-composer-card] [role="listbox"]>[data-dsh-ref-menu-settling] [role="option"][aria-selected="false"]:hover{background:transparent!important}
@@ -82,9 +82,11 @@ export const piAdapter = {
82
82
  createdAt ??= parseTimestamp(record['timestamp']);
83
83
  continue;
84
84
  }
85
- // A rename writes another `session_info`, so the newest one wins.
86
- if (record['type'] === 'session_info')
87
- named = stringField(record, 'name') ?? named;
85
+ // A rename writes another `session_info`, so the newest one wins. Some
86
+ // Pi-compatible writers call the same field `title`.
87
+ if (record['type'] === 'session_info') {
88
+ named = stringField(record, 'name') ?? stringField(record, 'title') ?? named;
89
+ }
88
90
  }
89
91
  for (const line of headLines) {
90
92
  const record = parseRecord(line);
@@ -643,7 +643,9 @@ function foldsToNothing(adapter, lines, options) {
643
643
  return adapter.flush(state, options).length === 0;
644
644
  }
645
645
  function labelFor(descriptor, bookmark) {
646
- const title = bookmark?.title.trim() ?? '';
646
+ // Some file-backed agents expose a title index beside the transcript. It is
647
+ // fresher than a persisted first-prompt bookmark and must win when present.
648
+ const title = descriptor.title?.trim() || bookmark?.title.trim() || '';
647
649
  return title === '' ? basename(descriptor.relPath) : title;
648
650
  }
649
651
  /** The schema's own defaults, as a plain object. */
@@ -40,6 +40,15 @@ export interface TranscriptDescriptor {
40
40
  readonly relPath: string;
41
41
  readonly mtimeMs: number;
42
42
  readonly size: number;
43
+ /**
44
+ * A title supplied by an index beside a file-backed transcript.
45
+ *
46
+ * Codex keeps its user-facing session title in `~/.codex/session_index.jsonl`,
47
+ * while the rollout itself only contains the opening prompt. This value is
48
+ * kept on the descriptor so a fresh index title can override an older
49
+ * first-prompt bookmark without changing the transcript adapter contract.
50
+ */
51
+ readonly title?: string;
43
52
  /**
44
53
  * The session row, for a database-backed format only.
45
54
  *
@@ -17,8 +17,8 @@
17
17
  * @module dsh-reference-anything/local-agent/scan
18
18
  */
19
19
  import { createReadStream } from 'node:fs';
20
- import { open, readdir, stat } from 'node:fs/promises';
21
- import { join, relative } from 'node:path';
20
+ import { open, readdir, readFile, stat } from 'node:fs/promises';
21
+ import { basename, join, relative, resolve } from 'node:path';
22
22
  import { ReferenceAnythingError } from "../../errors.js";
23
23
  import { withDatabase } from "./sqlite.js";
24
24
  import { isQueryAdapter } from "./types.js";
@@ -26,6 +26,7 @@ import { isQueryAdapter } from "./types.js";
26
26
  export const SESSION_SEPARATOR = '#';
27
27
  /** Directory depth walked below each root. */
28
28
  const MAX_DEPTH = 6;
29
+ const MAX_CODEX_TITLE_INDEX_BYTES = 16 * 1024 * 1024;
29
30
  /** Probe windows, in bytes, tried in order until enough lines are recovered. */
30
31
  const PROBE_STEPS = [32 * 1024, 256 * 1024, 2 * 1024 * 1024];
31
32
  /** Chunk size for walking a large transcript backwards from its end. */
@@ -48,12 +49,15 @@ export async function listTranscripts(roots, adapters, maxTranscripts, signal) {
48
49
  const adapter = adapters.get(root.kind);
49
50
  if (adapter === undefined)
50
51
  continue;
51
- await walk(root, root.path, 0, adapter, found, signal, maxTranscripts);
52
+ const titles = root.kind === 'codex' && !isQueryAdapter(adapter)
53
+ ? await readCodexTitleIndex(root.path)
54
+ : undefined;
55
+ await walk(root, root.path, 0, adapter, found, signal, maxTranscripts, titles);
52
56
  }
53
57
  found.sort((left, right) => right.mtimeMs - left.mtimeMs);
54
58
  return found.slice(0, maxTranscripts);
55
59
  }
56
- async function walk(root, dir, depth, adapter, found, signal, maxSessions = 200) {
60
+ async function walk(root, dir, depth, adapter, found, signal, maxSessions = 200, titles) {
57
61
  signal?.throwIfAborted();
58
62
  if (depth > MAX_DEPTH)
59
63
  return;
@@ -63,7 +67,7 @@ async function walk(root, dir, depth, adapter, found, signal, maxSessions = 200)
63
67
  for (const entry of entries) {
64
68
  const path = join(dir, entry.name);
65
69
  if (entry.isDirectory()) {
66
- await walk(root, path, depth + 1, adapter, found, signal, maxSessions);
70
+ await walk(root, path, depth + 1, adapter, found, signal, maxSessions, titles);
67
71
  continue;
68
72
  }
69
73
  if (!entry.isFile())
@@ -78,9 +82,71 @@ async function walk(root, dir, depth, adapter, found, signal, maxSessions = 200)
78
82
  found.push(...await expandSessions(root.kind, path, relPath, stats.mtimeMs, stats.size, adapter, maxSessions));
79
83
  continue;
80
84
  }
81
- found.push({ kind: root.kind, path, relPath, mtimeMs: stats.mtimeMs, size: stats.size });
85
+ found.push({
86
+ kind: root.kind,
87
+ path,
88
+ relPath,
89
+ mtimeMs: stats.mtimeMs,
90
+ size: stats.size,
91
+ ...titles === undefined ? {} : titleForCodexPath(path, titles),
92
+ });
82
93
  }
83
94
  }
95
+ /** Read the Codex title index once per configured sessions root. */
96
+ async function readCodexTitleIndex(sessionsRoot) {
97
+ const indexPath = resolve(sessionsRoot, '..', 'session_index.jsonl');
98
+ const indexStats = await stat(indexPath).catch(() => undefined);
99
+ // Keep discovery bounded; an unexpectedly large index must not turn a
100
+ // keystroke-triggered scan into an unbounded allocation.
101
+ if (indexStats === undefined || indexStats.size > MAX_CODEX_TITLE_INDEX_BYTES)
102
+ return undefined;
103
+ const raw = await readFile(indexPath, 'utf8').catch(() => undefined);
104
+ if (raw === undefined)
105
+ return undefined;
106
+ const byPath = new Map();
107
+ const bySessionId = new Map();
108
+ for (const line of raw.split(/\r?\n/u)) {
109
+ if (line.trim() === '')
110
+ continue;
111
+ let record;
112
+ try {
113
+ record = JSON.parse(line);
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ if (typeof record !== 'object' || record === null)
119
+ continue;
120
+ const row = record;
121
+ const title = typeof row['thread_name'] === 'string' ? row['thread_name'].trim() : '';
122
+ if (title === '')
123
+ continue;
124
+ const id = typeof row['id'] === 'string' ? row['id'].trim() : '';
125
+ if (id !== '')
126
+ bySessionId.set(id, title);
127
+ const rolloutPath = typeof row['rollout_path'] === 'string' ? row['rollout_path'].trim() : '';
128
+ if (rolloutPath !== '')
129
+ byPath.set(normalizeIndexedPath(rolloutPath), title);
130
+ }
131
+ return { byPath, bySessionId };
132
+ }
133
+ function titleForCodexPath(path, index) {
134
+ const direct = index.byPath.get(normalizeIndexedPath(path));
135
+ if (direct !== undefined)
136
+ return { title: direct };
137
+ const match = basename(path).match(/-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:_[^.]*)?\.jsonl$/iu);
138
+ const sessionId = match?.[1];
139
+ const title = sessionId === undefined ? undefined : index.bySessionId.get(sessionId);
140
+ return title === undefined ? {} : { title };
141
+ }
142
+ /** Normalize Windows paths from the index and from the active filesystem. */
143
+ function normalizeIndexedPath(path) {
144
+ return path
145
+ .replace(/^\\\\\?\\/u, '')
146
+ .replace(/[\\/]+/gu, '\\')
147
+ .replace(/\\$/u, '')
148
+ .toLocaleLowerCase();
149
+ }
84
150
  /**
85
151
  * Turn one database into a descriptor per conversation inside it.
86
152
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-reference-anything",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Reference commands, skills, workspace files, DSH sessions, local agent transcripts, cloud-drive files, and mirrored web-chat conversations from one DeepSeek Harness @ menu",
5
5
  "license": "MIT",
6
6
  "publishConfig": {