@deepseek-harness-tui/dsh-tui 0.7.0 → 0.7.2

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 (85) hide show
  1. package/README.md +9 -6
  2. package/bin/dsh-tui.js +67 -14
  3. package/cordis.patch.yml +10 -0
  4. package/lib/types/commands.js +1 -1
  5. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  6. package/lib/types/components/LoadedContextPanel.js +1 -1
  7. package/lib/types/components/design-system/ThemedText.d.ts +2 -2
  8. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  9. package/lib/types/components/design-system/ThemedText.js +1 -3
  10. package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
  11. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
  12. package/lib/types/components/sessions/SessionListRow.js +35 -0
  13. package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
  14. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
  15. package/lib/types/components/sessions/SessionPreview.js +60 -0
  16. package/lib/types/dsh-adapter/channel.d.ts +23 -4
  17. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  18. package/lib/types/dsh-adapter/channel.js +190 -75
  19. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  20. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  21. package/lib/types/dsh-adapter/compat/index.js +1 -1
  22. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
  23. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  24. package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
  25. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  26. package/lib/types/dsh-adapter/index.js +10 -1
  27. package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
  28. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
  29. package/lib/types/dsh-adapter/packaged-presets.js +90 -0
  30. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  31. package/lib/types/dsh-adapter/plugin.js +60 -4
  32. package/lib/types/dsh-adapter/presets.d.ts +16 -0
  33. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  34. package/lib/types/dsh-adapter/presets.js +23 -0
  35. package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
  36. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
  37. package/lib/types/dsh-adapter/sessions/digest.js +224 -0
  38. package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
  39. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
  40. package/lib/types/dsh-adapter/sessions/frames.js +246 -0
  41. package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
  42. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
  43. package/lib/types/dsh-adapter/sessions/header.js +62 -0
  44. package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
  45. package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
  46. package/lib/types/dsh-adapter/sessions/index.js +15 -0
  47. package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
  48. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
  49. package/lib/types/dsh-adapter/sessions/list.js +213 -0
  50. package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
  51. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
  52. package/lib/types/dsh-adapter/sessions/store.js +164 -0
  53. package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
  54. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
  55. package/lib/types/dsh-adapter/sessions/types.js +14 -0
  56. package/lib/types/i18n.d.ts +122 -14
  57. package/lib/types/i18n.d.ts.map +1 -1
  58. package/lib/types/i18n.js +37 -6
  59. package/lib/types/screens/Chat.d.ts +3 -2
  60. package/lib/types/screens/Chat.d.ts.map +1 -1
  61. package/lib/types/screens/Chat.js +28 -144
  62. package/lib/types/screens/SessionBrowser.d.ts +37 -0
  63. package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
  64. package/lib/types/screens/SessionBrowser.js +437 -0
  65. package/lib/types/sessionHistory.d.ts +0 -8
  66. package/lib/types/sessionHistory.d.ts.map +1 -1
  67. package/lib/types/sessions/format.d.ts +117 -0
  68. package/lib/types/sessions/format.d.ts.map +1 -0
  69. package/lib/types/sessions/format.js +244 -0
  70. package/lib/types/sessions/view.d.ts +125 -0
  71. package/lib/types/sessions/view.d.ts.map +1 -0
  72. package/lib/types/sessions/view.js +222 -0
  73. package/package.json +12 -5
  74. package/presets/liangshen/.dsh-tui-managed.json +5 -0
  75. package/presets/liangshen/agent.cordis.yml +389 -0
  76. package/presets/liangshen/compaction-epoch.mjs +81 -0
  77. package/presets/liangshen/custom-bash.mjs +126 -0
  78. package/presets/liangshen/instruction-hint.mjs +181 -0
  79. package/presets/liangshen/preset.yml +3 -0
  80. package/presets/liangshen/skill-search.mjs +142 -0
  81. package/presets/liangshen/tool-bootstrap.mjs +300 -0
  82. package/lib/invariant.js +0 -28
  83. package/lib/types/components/ResumePicker.d.ts +0 -26
  84. package/lib/types/components/ResumePicker.d.ts.map +0 -1
  85. package/lib/types/components/ResumePicker.js +0 -50
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Bounded reads over the JSONL backend's concatenated-Zstandard session logs.
3
+ *
4
+ * The backend stores one session as a chain of independently decodable zstd
5
+ * frames — one per durable append batch — so a log grows by concatenation and
6
+ * never rewrites committed bytes. That container is what makes a *bounded*
7
+ * read possible at all: the first few frames hold the session header and its
8
+ * opening prompt, the last few hold whatever was appended most recently, and
9
+ * nothing in between has to be touched to learn either.
10
+ *
11
+ * Why this module walks frames structurally instead of scanning for the frame
12
+ * magic: a magic scan is a heuristic (any four bytes of compressed payload can
13
+ * spell `FD2FB528`), it costs a comparison per byte, and it cannot answer the
14
+ * one question a bounded reader must ask — "is the frame at the end of my
15
+ * window complete, or did I cut it in half?". Walking the frame header and its
16
+ * block chain, as RFC 8878 §3.1.1 defines them, answers that exactly and jumps
17
+ * frame-to-frame instead of byte-to-byte. Measured over a real 31 MB corpus of
18
+ * 49 logs and 75,263 frames: identical frame set to a magic scan, zero false
19
+ * positives, and 4× faster (51 ms vs 215 ms).
20
+ *
21
+ * Why not the backend's own `scanZstdFrames`: it exists, but the package's
22
+ * `exports` map publishes only the service class, so reaching it would mean
23
+ * importing through `lib/` past the export map — coupling to a private path
24
+ * that upstream is free to move. The frame container is a published format
25
+ * (RFC 8878), so re-deriving the walk against the spec is the stable choice.
26
+ *
27
+ * Why not Node's own zstd APIs: both `zstdDecompressSync` and
28
+ * `createZstdDecompress` stop at the end of the FIRST frame — a 4.2 MB,
29
+ * 14k-event log decodes to exactly one line through either. They decode a
30
+ * frame; they do not traverse a chain.
31
+ *
32
+ * @module @deepseek-harness-tui/dsh-tui/sessions/frames
33
+ */
34
+ import { closeSync, openSync, readSync, statSync } from 'node:fs';
35
+ import { zstdDecompressSync } from 'node:zlib';
36
+ /** Zstandard frame magic, little-endian (RFC 8878 §3.1.1.1). */
37
+ const ZSTD_MAGIC = 0xfd2fb528;
38
+ /**
39
+ * Locate the end of the frame starting at `start`, without decompressing it.
40
+ *
41
+ * The walk reads the Frame_Header (descriptor, optional window/dictionary/
42
+ * content-size fields) and then each Block_Header in turn — a 3-byte
43
+ * little-endian word carrying `last_block` (1 bit), `block_type` (2 bits) and
44
+ * `block_size` (21 bits) — until the block marked last. A `Reserved` block
45
+ * type means these bytes are not a frame at all, which is how a coincidental
46
+ * magic gets rejected.
47
+ *
48
+ * @param buffer - Bytes available to the reader (may end mid-frame).
49
+ * @param start - Offset of the candidate frame's magic.
50
+ * @returns The frame's exclusive end offset, or -1 when the bytes at `start`
51
+ * are not a structurally complete frame within `buffer`.
52
+ */
53
+ export function frameEnd(buffer, start) {
54
+ let at = start;
55
+ if (at < 0 || at + 5 > buffer.length)
56
+ return -1;
57
+ if (buffer.readUInt32LE(at) !== ZSTD_MAGIC)
58
+ return -1;
59
+ at += 4;
60
+ const descriptor = buffer[at];
61
+ at += 1;
62
+ const contentSizeFlag = descriptor >> 6;
63
+ const singleSegment = (descriptor >> 5) & 1;
64
+ const hasChecksum = (descriptor >> 2) & 1;
65
+ const dictionaryIdFlag = descriptor & 3;
66
+ // Window_Descriptor is present only when the frame is not single-segment.
67
+ if (singleSegment === 0)
68
+ at += 1;
69
+ at += [0, 1, 2, 4][dictionaryIdFlag];
70
+ // Frame_Content_Size: absent (0) unless single-segment, where it is 1 byte.
71
+ at += contentSizeFlag === 0 ? singleSegment : [0, 2, 4, 8][contentSizeFlag];
72
+ if (at > buffer.length)
73
+ return -1;
74
+ for (;;) {
75
+ if (at + 3 > buffer.length)
76
+ return -1;
77
+ const header = buffer[at] | (buffer[at + 1] << 8) | (buffer[at + 2] << 16);
78
+ at += 3;
79
+ const isLast = header & 1;
80
+ const blockType = (header >> 1) & 3;
81
+ const blockSize = header >>> 3;
82
+ // 3 = Reserved. Never emitted by an encoder, so this is not a frame.
83
+ if (blockType === 3)
84
+ return -1;
85
+ // An RLE block stores one byte and repeats it `blockSize` times; Raw and
86
+ // Compressed blocks store `blockSize` bytes verbatim.
87
+ at += blockType === 1 ? 1 : blockSize;
88
+ if (at > buffer.length)
89
+ return -1;
90
+ if (isLast === 1)
91
+ break;
92
+ }
93
+ if (hasChecksum === 1)
94
+ at += 4;
95
+ return at <= buffer.length ? at : -1;
96
+ }
97
+ /**
98
+ * Walk complete frames forward from `from`.
99
+ *
100
+ * @param buffer - Bytes to walk.
101
+ * @param from - Offset to start at (must be a frame boundary).
102
+ * @param maxFrames - Stop after this many frames; the reader's cost ceiling.
103
+ * @returns Complete frames in file order. A window that ends mid-frame simply
104
+ * yields one fewer frame — the partial tail is never reported as complete.
105
+ */
106
+ export function walkFrames(buffer, from = 0, maxFrames = Number.POSITIVE_INFINITY) {
107
+ const frames = [];
108
+ let at = from;
109
+ while (at < buffer.length && frames.length < maxFrames) {
110
+ const end = frameEnd(buffer, at);
111
+ if (end < 0)
112
+ break;
113
+ frames.push({ start: at, end });
114
+ at = end;
115
+ }
116
+ return frames;
117
+ }
118
+ /**
119
+ * Re-synchronize on a frame boundary inside a window that starts mid-frame.
120
+ *
121
+ * A tail window has no boundary to start from, so the only anchor is the one
122
+ * structural fact we know about the whole file: its last frame ends exactly at
123
+ * EOF. Every magic candidate is tried in file order, and the first one whose
124
+ * frame chain lands precisely on the window's end is the true boundary — a
125
+ * coincidental magic would have to spell a valid block chain of exactly the
126
+ * right total length to be mistaken for one.
127
+ *
128
+ * @param buffer - A window whose last byte is the file's last byte.
129
+ * @returns Frames from the earliest recoverable boundary, or [] when the
130
+ * window holds no complete frame.
131
+ */
132
+ export function resyncFrames(buffer) {
133
+ for (let at = 0; at + 4 <= buffer.length; at++) {
134
+ if (buffer.readUInt32LE(at) !== ZSTD_MAGIC)
135
+ continue;
136
+ const frames = walkFrames(buffer, at);
137
+ const last = frames[frames.length - 1];
138
+ if (last !== undefined && last.end === buffer.length)
139
+ return frames;
140
+ }
141
+ return [];
142
+ }
143
+ /**
144
+ * Decode frames to JSON log lines, tolerantly.
145
+ *
146
+ * A frame that fails to decompress or a line that fails to parse is skipped
147
+ * rather than thrown: a log being appended to right now can hold a frame
148
+ * flushed without its final checksum, and a torn tail is the backend's own
149
+ * documented recovery case. A picker label is read-only UI state — degrading
150
+ * to a fallback title beats refusing to list the session.
151
+ *
152
+ * @param buffer - Bytes the frames index into.
153
+ * @param frames - Complete frame ranges within `buffer`.
154
+ * @returns Parsed envelopes in log order.
155
+ */
156
+ export function decodeFrames(buffer, frames) {
157
+ const lines = [];
158
+ for (const frame of frames) {
159
+ let text;
160
+ try {
161
+ text = zstdDecompressSync(buffer.subarray(frame.start, frame.end)).toString('utf8');
162
+ }
163
+ catch {
164
+ continue; // incomplete flush or torn frame — the rest of the log stands
165
+ }
166
+ for (const line of text.split('\n')) {
167
+ if (line.length === 0)
168
+ continue;
169
+ try {
170
+ const parsed = JSON.parse(line);
171
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
172
+ lines.push(parsed);
173
+ }
174
+ }
175
+ catch {
176
+ // A half-written line at the tail; earlier lines remain valid.
177
+ }
178
+ }
179
+ }
180
+ return lines;
181
+ }
182
+ /**
183
+ * Size and mtime of a log, or undefined when it is gone.
184
+ * @param path - Absolute artifact path.
185
+ */
186
+ export function fileFacts(path) {
187
+ try {
188
+ const stats = statSync(path);
189
+ return { bytes: stats.size, modifiedAt: stats.mtimeMs };
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ }
195
+ /**
196
+ * Read a window from one end of a file without loading the whole thing.
197
+ *
198
+ * @param path - Absolute artifact path.
199
+ * @param bytes - Window budget; the whole file is read when it is smaller.
200
+ * @param end - Read the last `bytes` instead of the first.
201
+ * @returns The window, plus whether it covers the entire file (which tells a
202
+ * head reader that its last frame cannot be truncated).
203
+ */
204
+ export function readWindow(path, bytes, end = false) {
205
+ const facts = fileFacts(path);
206
+ if (facts === undefined)
207
+ return undefined;
208
+ const length = Math.min(bytes, facts.bytes);
209
+ if (length === 0)
210
+ return { buffer: Buffer.alloc(0), whole: true };
211
+ const buffer = Buffer.alloc(length);
212
+ let handle;
213
+ try {
214
+ handle = openSync(path, 'r');
215
+ }
216
+ catch {
217
+ return undefined;
218
+ }
219
+ let read;
220
+ try {
221
+ read = readSync(handle, buffer, 0, length, end ? facts.bytes - length : 0);
222
+ }
223
+ catch {
224
+ return undefined;
225
+ }
226
+ finally {
227
+ closeSync(handle);
228
+ }
229
+ // A short read is not an error: the frame walk simply sees fewer bytes and
230
+ // reports one fewer complete frame. Reporting `whole` honestly is what
231
+ // matters — a tail reader must know whether it may assume a boundary.
232
+ return { buffer: read === length ? buffer : buffer.subarray(0, read), whole: read === facts.bytes };
233
+ }
234
+ /**
235
+ * Decode a window read from the END of a file.
236
+ *
237
+ * A tail window has no frame boundary to start from unless it happens to
238
+ * cover the whole file, so it re-synchronizes; a whole-file window is simply
239
+ * walked.
240
+ *
241
+ * @param window - A window whose last byte is the file's last byte.
242
+ * @returns Log lines from the trailing frames, oldest first.
243
+ */
244
+ export function decodeTail(window) {
245
+ return decodeFrames(window.buffer, window.whole ? walkFrames(window.buffer) : resyncFrames(window.buffer));
246
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Reading the persistence header — total functions over foreign data.
3
+ *
4
+ * `sessionPersistence.list()` is a service resolved from the running context,
5
+ * so its results are foreign values that happen to be typed. Every accessor
6
+ * here therefore narrows structurally and returns a fallback rather than
7
+ * throwing: one malformed header must cost that session its metadata, never
8
+ * the whole listing. This mirrors the discipline the trajectory guards already
9
+ * apply to session events.
10
+ *
11
+ * @module @deepseek-harness-tui/dsh-tui/sessions/header
12
+ */
13
+ import type { SessionKind } from './types.js';
14
+ /** The header fields this feature reads, all optional at runtime. */
15
+ export interface RawSessionHeader {
16
+ readonly id: string;
17
+ readonly cwd: string | undefined;
18
+ readonly createdAt: number | undefined;
19
+ readonly parentSession: string | undefined;
20
+ readonly origin: string | undefined;
21
+ readonly delegationDepth: number | undefined;
22
+ readonly seedLength: number | undefined;
23
+ readonly agentPreset: string | undefined;
24
+ }
25
+ /**
26
+ * Narrow one listed header.
27
+ *
28
+ * @param value - A header as the persistence service returned it.
29
+ * @returns The fields this feature uses, or undefined when the value carries
30
+ * no usable session id — the one field nothing can substitute for.
31
+ */
32
+ export declare function readHeader(value: unknown): RawSessionHeader | undefined;
33
+ /**
34
+ * Decide what a session is, from its header alone.
35
+ *
36
+ * Precedence is `origin` first, lineage second, and that order is the whole
37
+ * correctness argument: a `/rewind` fork records `parentSession` exactly like
38
+ * a delegated run does, and only `origin` separates them. Upstream documents
39
+ * `origin` as "coarse product classification for a session created as a
40
+ * subagent child … presentation metadata", which is precisely this decision.
41
+ *
42
+ * `delegationDepth` is optional upstream, so a sub-agent whose header omits it
43
+ * is reported at depth 1: it is a delegated child by `origin`, and 1 is the
44
+ * shallowest depth that can be.
45
+ *
46
+ * @param header - A narrowed header.
47
+ * @returns The session's kind.
48
+ */
49
+ export declare function classify(header: RawSessionHeader): SessionKind;
50
+ //# sourceMappingURL=header.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/sessions/header.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;CACzC;AAYD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAevE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW,CAY9D"}
@@ -0,0 +1,62 @@
1
+ /** A finite number, or undefined for anything else (NaN and ±Infinity included). */
2
+ function finiteNumber(value) {
3
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
4
+ }
5
+ /** A non-empty string, or undefined. */
6
+ function text(value) {
7
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
8
+ }
9
+ /**
10
+ * Narrow one listed header.
11
+ *
12
+ * @param value - A header as the persistence service returned it.
13
+ * @returns The fields this feature uses, or undefined when the value carries
14
+ * no usable session id — the one field nothing can substitute for.
15
+ */
16
+ export function readHeader(value) {
17
+ if (value === null || typeof value !== 'object')
18
+ return undefined;
19
+ const record = value;
20
+ const id = text(record['id']);
21
+ if (id === undefined)
22
+ return undefined;
23
+ return {
24
+ id,
25
+ cwd: text(record['cwd']),
26
+ createdAt: finiteNumber(record['createdAt']),
27
+ parentSession: text(record['parentSession']),
28
+ origin: text(record['origin']),
29
+ delegationDepth: finiteNumber(record['delegationDepth']),
30
+ seedLength: finiteNumber(record['seedLength']),
31
+ agentPreset: text(record['agentPreset']),
32
+ };
33
+ }
34
+ /**
35
+ * Decide what a session is, from its header alone.
36
+ *
37
+ * Precedence is `origin` first, lineage second, and that order is the whole
38
+ * correctness argument: a `/rewind` fork records `parentSession` exactly like
39
+ * a delegated run does, and only `origin` separates them. Upstream documents
40
+ * `origin` as "coarse product classification for a session created as a
41
+ * subagent child … presentation metadata", which is precisely this decision.
42
+ *
43
+ * `delegationDepth` is optional upstream, so a sub-agent whose header omits it
44
+ * is reported at depth 1: it is a delegated child by `origin`, and 1 is the
45
+ * shallowest depth that can be.
46
+ *
47
+ * @param header - A narrowed header.
48
+ * @returns The session's kind.
49
+ */
50
+ export function classify(header) {
51
+ if (header.origin === 'subagent') {
52
+ return {
53
+ kind: 'subagent',
54
+ parent: header.parentSession,
55
+ depth: header.delegationDepth ?? 1,
56
+ };
57
+ }
58
+ if (header.parentSession !== undefined) {
59
+ return { kind: 'fork', parent: header.parentSession };
60
+ }
61
+ return { kind: 'root' };
62
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Persisted-session metadata — the adapter-side barrel.
3
+ *
4
+ * This directory is the only part of the session browser allowed to know how
5
+ * sessions are stored: the persistence service's shape, the log's frame
6
+ * container, the header's lineage fields. The browser screen and its
7
+ * components are pure UI over the types re-exported here and never touch a
8
+ * session log directly, which is what `verify-adapter-boundary` enforces.
9
+ *
10
+ * @module @deepseek-harness-tui/dsh-tui/sessions
11
+ */
12
+ export { classify, readHeader, type RawSessionHeader } from './header.js';
13
+ export { digestSession, previewSession } from './digest.js';
14
+ export { listSummaries, locateSession, type SessionSource } from './list.js';
15
+ export { noteBranch, readIndex, writeIndex, type IndexEntry, type SessionIndex } from './store.js';
16
+ export type { PreviewEntry, SessionDigest, SessionKind, SessionSummary, SessionTitle, TitleSource, } from './types.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/sessions/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE3D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAE5E,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAElG,YAAY,EACV,YAAY,EACZ,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Persisted-session metadata — the adapter-side barrel.
3
+ *
4
+ * This directory is the only part of the session browser allowed to know how
5
+ * sessions are stored: the persistence service's shape, the log's frame
6
+ * container, the header's lineage fields. The browser screen and its
7
+ * components are pure UI over the types re-exported here and never touch a
8
+ * session log directly, which is what `verify-adapter-boundary` enforces.
9
+ *
10
+ * @module @deepseek-harness-tui/dsh-tui/sessions
11
+ */
12
+ export { classify, readHeader } from './header.js';
13
+ export { digestSession, previewSession } from './digest.js';
14
+ export { listSummaries, locateSession } from './list.js';
15
+ export { noteBranch, readIndex, writeIndex } from './store.js';
@@ -0,0 +1,41 @@
1
+ import type { SessionSummary } from './types.js';
2
+ /**
3
+ * The slice of `ctx.sessionPersistence` this module uses.
4
+ *
5
+ * Structural and fully optional: the service is resolved from a running
6
+ * context whose packages may be a version apart from ours, and a listing that
7
+ * degrades is worth more than one that throws.
8
+ */
9
+ export interface SessionSource {
10
+ /** Headers plus per-log change tokens — the contract built for this. */
11
+ listSnapshots?: (signal?: AbortSignal) => Promise<readonly unknown[]>;
12
+ /** Headers alone, for a backend or version without snapshots. */
13
+ list?: (signal?: AbortSignal) => Promise<readonly unknown[]>;
14
+ /** Absolute artifact path for one header; absent for storeless backends. */
15
+ locate?: (meta: unknown) => unknown;
16
+ }
17
+ /**
18
+ * Read every stored session into a complete summary.
19
+ *
20
+ * @param source - The persistence service.
21
+ * @param signal - Optional cancellation for the backend's own listing work.
22
+ * @returns One summary per stored session, most recently active first. No
23
+ * filtering of any kind is applied — sub-agent runs and sessions with no
24
+ * conversation are present and labelled as such.
25
+ */
26
+ export declare function listSummaries(source: SessionSource, signal?: AbortSignal): Promise<readonly SessionSummary[]>;
27
+ /**
28
+ * Resolve one session's artifact path.
29
+ *
30
+ * Listing headers is a first-line-only read per log — about 2 ms across a
31
+ * fifty-session history — so the preview pane resolves its target this way
32
+ * rather than making every summary carry a filesystem path it has no business
33
+ * knowing about.
34
+ *
35
+ * @param source - The persistence service.
36
+ * @param sessionId - Session to locate.
37
+ * @returns The absolute artifact path, or undefined when the backend owns no
38
+ * per-session file or the session is gone.
39
+ */
40
+ export declare function locateSession(source: SessionSource, sessionId: string, signal?: AbortSignal): Promise<string | undefined>;
41
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/sessions/list.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,aAAa,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;IACrE,iEAAiE;IACjE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;IAC5D,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAA;CACpC;AA0ED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,EACrB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC,CA8FpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,aAAa,EACrB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Building the session list.
3
+ *
4
+ * One resolution path produces one complete, honestly-classified record per
5
+ * stored session — every kind, empties included — and callers decide what to
6
+ * show. That split is deliberate: the old picker filtered while it resolved,
7
+ * so "hide sub-agent runs" and "resolve a title" were the same pass and
8
+ * neither could change without disturbing the other. Here the browser can
9
+ * toggle sub-agent runs into view, or offer to clean up boot artifacts,
10
+ * without re-deriving anything.
11
+ *
12
+ * Cost: one `stat` per session always, plus one bounded log read per session
13
+ * whose revision moved since the last listing. On a warm index that is zero
14
+ * log reads. The path this replaces decompressed every frame of the twenty
15
+ * most recent logs on every open — 3.9 s over a 31 MB history.
16
+ *
17
+ * @module @deepseek-harness-tui/dsh-tui/sessions/list
18
+ */
19
+ import { basename } from 'node:path';
20
+ import { digestSession } from './digest.js';
21
+ import { fileFacts } from './frames.js';
22
+ import { classify, readHeader } from './header.js';
23
+ import { findSessionLogFile } from '../compat/sessionLog.js';
24
+ import { readIndex, writeIndex } from './store.js';
25
+ import { readLastUsed } from '../../sessionHistory.js';
26
+ /** Pull `{ header, revision }` out of one `listSnapshots()` element. */
27
+ function readSnapshot(value) {
28
+ if (value === null || typeof value !== 'object')
29
+ return undefined;
30
+ const record = value;
31
+ const raw = record['header'];
32
+ const header = readHeader(raw);
33
+ if (header === undefined)
34
+ return undefined;
35
+ const revision = record['revision'];
36
+ return { header, raw, revision: typeof revision === 'string' ? revision : undefined };
37
+ }
38
+ /**
39
+ * Enumerate stored sessions.
40
+ *
41
+ * Prefers `listSnapshots()` because its revision is the backend's own answer
42
+ * to "has this log changed", and falls back to `list()` when the resolved
43
+ * service predates it — in which case the change token is derived from the
44
+ * file's own size and mtime further down. Both are honest change tokens for an
45
+ * append-only log; only the authority differs.
46
+ */
47
+ async function enumerate(source, signal) {
48
+ if (typeof source.listSnapshots === 'function') {
49
+ const snapshots = await source.listSnapshots(signal);
50
+ return snapshots.map(readSnapshot).filter((entry) => entry !== undefined);
51
+ }
52
+ if (typeof source.list === 'function') {
53
+ const headers = await source.list(signal);
54
+ return headers
55
+ .map((raw) => {
56
+ const header = readHeader(raw);
57
+ return header === undefined ? undefined : { header, raw, revision: undefined };
58
+ })
59
+ .filter((entry) => entry !== undefined);
60
+ }
61
+ return [];
62
+ }
63
+ /**
64
+ * Absolute artifact path for one session.
65
+ *
66
+ * The backend's own `locate()` is authoritative and is asked first. The
67
+ * fallback scans the session roots for the id, which is what the compat layer
68
+ * has always done and is deliberately independent of the backend's
69
+ * workspace-key scheme — so a runtime whose persistence service predates
70
+ * `locate`, or whose key sanitization changes, still resolves.
71
+ *
72
+ * A backend that stores no per-session artifact (SQLite) answers neither, and
73
+ * its sessions are summarized from their headers alone.
74
+ */
75
+ function locate(source, raw, sessionId) {
76
+ if (typeof source.locate === 'function') {
77
+ let location;
78
+ try {
79
+ location = source.locate(raw);
80
+ }
81
+ catch {
82
+ location = undefined;
83
+ }
84
+ if (location !== null && typeof location === 'object') {
85
+ const path = location['path'];
86
+ if (typeof path === 'string' && path.length > 0)
87
+ return path;
88
+ }
89
+ }
90
+ return findSessionLogFile(sessionId);
91
+ }
92
+ /**
93
+ * Read every stored session into a complete summary.
94
+ *
95
+ * @param source - The persistence service.
96
+ * @param signal - Optional cancellation for the backend's own listing work.
97
+ * @returns One summary per stored session, most recently active first. No
98
+ * filtering of any kind is applied — sub-agent runs and sessions with no
99
+ * conversation are present and labelled as such.
100
+ */
101
+ export async function listSummaries(source, signal) {
102
+ let listed;
103
+ try {
104
+ listed = await enumerate(source, signal);
105
+ }
106
+ catch {
107
+ return [];
108
+ }
109
+ // Children are counted from the same listing rather than by walking logs:
110
+ // lineage lives in the header, so a parent's sub-agent count is free.
111
+ const children = new Map();
112
+ for (const entry of listed) {
113
+ if (entry.header.origin !== 'subagent')
114
+ continue;
115
+ const parent = entry.header.parentSession;
116
+ if (parent === undefined)
117
+ continue;
118
+ children.set(parent, (children.get(parent) ?? 0) + 1);
119
+ }
120
+ const index = readIndex();
121
+ const next = new Map();
122
+ const lastUsed = readLastUsed();
123
+ let changed = false;
124
+ const summaries = [];
125
+ for (const { header, raw, revision } of listed) {
126
+ const cached = index.get(header.id);
127
+ const path = locate(source, raw, header.id);
128
+ const facts = path === undefined ? undefined : fileFacts(path);
129
+ // Falls back to the file's own identity when the backend offered no token.
130
+ const token = revision ?? (facts === undefined ? undefined : `${facts.bytes}:${facts.modifiedAt}`);
131
+ let derived;
132
+ if (cached?.derived !== undefined && token !== undefined && cached.derived.revision === token) {
133
+ derived = cached.derived;
134
+ }
135
+ else if (path !== undefined && token !== undefined) {
136
+ const digest = digestSession(path, header.cwd ?? '');
137
+ derived = {
138
+ revision: token,
139
+ title: digest.title?.text ?? '',
140
+ titleSource: digest.title?.source ?? 'fallback',
141
+ hasPrompt: digest.hasPrompt,
142
+ model: digest.model,
143
+ label: digest.label,
144
+ };
145
+ changed = true;
146
+ }
147
+ // Carry every entry that holds anything worth keeping — including a pure
148
+ // cache hit, which must survive into the next index or the following
149
+ // listing would re-derive everything it just reused.
150
+ if (derived !== undefined || cached?.branch !== undefined) {
151
+ next.set(header.id, { derived, branch: cached?.branch });
152
+ }
153
+ const createdAt = header.createdAt ?? facts?.modifiedAt ?? 0;
154
+ summaries.push({
155
+ id: header.id,
156
+ kind: classify(header),
157
+ title: {
158
+ text: derived?.title !== undefined && derived.title.length > 0
159
+ ? derived.title
160
+ : basename(header.cwd ?? '') || header.id.slice(0, 8),
161
+ source: derived?.titleSource ?? 'fallback',
162
+ },
163
+ cwd: header.cwd ?? '',
164
+ createdAt,
165
+ updatedAt: Math.max(facts?.modifiedAt ?? 0, lastUsed[header.id] ?? 0, createdAt),
166
+ bytes: facts?.bytes,
167
+ // Without a readable artifact nothing can be proven empty, and hiding a
168
+ // real session is the worse error — so an unreadable log is listed.
169
+ hasPrompt: derived?.hasPrompt ?? true,
170
+ agentPreset: header.agentPreset,
171
+ model: derived?.model,
172
+ label: derived?.label,
173
+ branch: cached?.branch,
174
+ childCount: children.get(header.id) ?? 0,
175
+ });
176
+ }
177
+ // Entries for sessions the backend no longer lists are dropped here; that is
178
+ // the whole of the cache's garbage collection, and it runs on every listing.
179
+ if (changed || next.size !== index.size)
180
+ writeIndex(next);
181
+ // A total order, not just a sort key. `updatedAt` is dominated by the log's
182
+ // mtime, and sessions written inside the same millisecond tie on it — which
183
+ // would leave their relative order down to whatever the backend happened to
184
+ // enumerate first, so the same history could list differently twice in a
185
+ // row. Creation time breaks the tie, and the id breaks that.
186
+ return summaries.sort((left, right) => right.updatedAt - left.updatedAt ||
187
+ right.createdAt - left.createdAt ||
188
+ (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
189
+ }
190
+ /**
191
+ * Resolve one session's artifact path.
192
+ *
193
+ * Listing headers is a first-line-only read per log — about 2 ms across a
194
+ * fifty-session history — so the preview pane resolves its target this way
195
+ * rather than making every summary carry a filesystem path it has no business
196
+ * knowing about.
197
+ *
198
+ * @param source - The persistence service.
199
+ * @param sessionId - Session to locate.
200
+ * @returns The absolute artifact path, or undefined when the backend owns no
201
+ * per-session file or the session is gone.
202
+ */
203
+ export async function locateSession(source, sessionId, signal) {
204
+ let listed;
205
+ try {
206
+ listed = await enumerate(source, signal);
207
+ }
208
+ catch {
209
+ return undefined;
210
+ }
211
+ const match = listed.find(entry => entry.header.id === sessionId);
212
+ return match === undefined ? undefined : locate(source, match.raw, sessionId);
213
+ }