@toddzheng024/dscode-bundle 0.7.11 → 0.7.13

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 (154) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/i18n/messages.mjs +6 -6
  5. package/plugins/session-metrics/view.mjs +16 -5
  6. package/plugins/tui-tools/index.mjs +14 -1
  7. package/vendor/tui/lib/app.mjs +6674 -0
  8. package/vendor/tui/lib/approval.mjs +122 -0
  9. package/vendor/tui/lib/attachments.mjs +218 -0
  10. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  11. package/vendor/tui/lib/authorization.mjs +111 -0
  12. package/vendor/tui/lib/commands.mjs +123 -0
  13. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  14. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  15. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  16. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  17. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  18. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  19. package/vendor/tui/lib/editor-keys.mjs +346 -0
  20. package/vendor/tui/lib/editor.mjs +50 -0
  21. package/vendor/tui/lib/fork.mjs +24 -0
  22. package/vendor/tui/lib/git-workflow.mjs +278 -0
  23. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  24. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  25. package/vendor/tui/lib/index.mjs +2198 -0
  26. package/vendor/tui/lib/input-split.mjs +170 -0
  27. package/vendor/tui/lib/internals.mjs +90 -0
  28. package/vendor/tui/lib/invariant.mjs +22 -0
  29. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  30. package/vendor/tui/lib/keyboard.mjs +283 -0
  31. package/vendor/tui/lib/language-panel.mjs +41 -0
  32. package/vendor/tui/lib/locales/en.mjs +495 -0
  33. package/vendor/tui/lib/locales/zh.mjs +495 -0
  34. package/vendor/tui/lib/mentions.mjs +150 -0
  35. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  36. package/vendor/tui/lib/models.mjs +215 -0
  37. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  38. package/vendor/tui/lib/permissions.mjs +45 -0
  39. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  40. package/vendor/tui/lib/presets.mjs +46 -0
  41. package/vendor/tui/lib/provider-settings.mjs +600 -0
  42. package/vendor/tui/lib/questions.mjs +116 -0
  43. package/vendor/tui/lib/rainbow.mjs +181 -0
  44. package/vendor/tui/lib/render/animations.mjs +665 -0
  45. package/vendor/tui/lib/render/editor.mjs +437 -0
  46. package/vendor/tui/lib/render/export.mjs +119 -0
  47. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  48. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  49. package/vendor/tui/lib/render/inspector.mjs +93 -0
  50. package/vendor/tui/lib/render/lines.mjs +538 -0
  51. package/vendor/tui/lib/render/markdown.mjs +592 -0
  52. package/vendor/tui/lib/render/projection.mjs +1862 -0
  53. package/vendor/tui/lib/render/status.mjs +591 -0
  54. package/vendor/tui/lib/render/text.mjs +157 -0
  55. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  56. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  57. package/vendor/tui/lib/render/usage.mjs +336 -0
  58. package/vendor/tui/lib/render/width.mjs +191 -0
  59. package/vendor/tui/lib/session-directory.mjs +329 -0
  60. package/vendor/tui/lib/session-query.mjs +184 -0
  61. package/vendor/tui/lib/session-switch.mjs +51 -0
  62. package/vendor/tui/lib/settings-file.mjs +75 -0
  63. package/vendor/tui/lib/skills.mjs +119 -0
  64. package/vendor/tui/lib/startup.mjs +111 -0
  65. package/vendor/tui/lib/store.mjs +129 -0
  66. package/vendor/tui/lib/subagents.mjs +200 -0
  67. package/vendor/tui/lib/terminal-title.mjs +186 -0
  68. package/vendor/tui/lib/theme-panel.mjs +60 -0
  69. package/vendor/tui/lib/theme.mjs +379 -0
  70. package/vendor/tui/lib/update-panel.mjs +239 -0
  71. package/vendor/tui/lib/update.mjs +101 -0
  72. package/vendor/tui/lib/version.mjs +105 -0
  73. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  74. package/vendor/tui/LICENSE +0 -21
  75. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  76. package/vendor/tui/dscode-email/cli.mjs +0 -32
  77. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  78. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  79. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  80. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  81. package/vendor/tui/dscode-email/imap.mjs +0 -136
  82. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  83. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  84. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  85. package/vendor/tui/dscode-email/store.mjs +0 -32
  86. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  87. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  88. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  89. package/vendor/tui/index.mjs +0 -44483
  90. package/vendor/tui/invariant.mjs +0 -21
  91. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  92. package/vendor/tui/session-query.mjs +0 -150
  93. package/vendor/tui/startup.mjs +0 -109
  94. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  95. package/vendor/tui/types/app.d.ts +0 -348
  96. package/vendor/tui/types/approval.d.ts +0 -59
  97. package/vendor/tui/types/attachments.d.ts +0 -52
  98. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  99. package/vendor/tui/types/authorization.d.ts +0 -36
  100. package/vendor/tui/types/commands.d.ts +0 -52
  101. package/vendor/tui/types/editor-keys.d.ts +0 -105
  102. package/vendor/tui/types/editor.d.ts +0 -6
  103. package/vendor/tui/types/fork.d.ts +0 -8
  104. package/vendor/tui/types/git-workflow.d.ts +0 -121
  105. package/vendor/tui/types/index.d.ts +0 -223
  106. package/vendor/tui/types/input-split.d.ts +0 -54
  107. package/vendor/tui/types/internals.d.ts +0 -26
  108. package/vendor/tui/types/invariant.d.ts +0 -15
  109. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  110. package/vendor/tui/types/keyboard.d.ts +0 -80
  111. package/vendor/tui/types/language-panel.d.ts +0 -12
  112. package/vendor/tui/types/locales/en.d.ts +0 -450
  113. package/vendor/tui/types/locales/zh.d.ts +0 -9
  114. package/vendor/tui/types/mentions.d.ts +0 -85
  115. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  116. package/vendor/tui/types/models.d.ts +0 -133
  117. package/vendor/tui/types/permissions.d.ts +0 -27
  118. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  119. package/vendor/tui/types/presets.d.ts +0 -22
  120. package/vendor/tui/types/provider-settings.d.ts +0 -239
  121. package/vendor/tui/types/questions.d.ts +0 -54
  122. package/vendor/tui/types/rainbow.d.ts +0 -69
  123. package/vendor/tui/types/render/animations.d.ts +0 -307
  124. package/vendor/tui/types/render/editor.d.ts +0 -163
  125. package/vendor/tui/types/render/export.d.ts +0 -9
  126. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  127. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  128. package/vendor/tui/types/render/inspector.d.ts +0 -62
  129. package/vendor/tui/types/render/lines.d.ts +0 -86
  130. package/vendor/tui/types/render/markdown.d.ts +0 -29
  131. package/vendor/tui/types/render/projection.d.ts +0 -587
  132. package/vendor/tui/types/render/status.d.ts +0 -196
  133. package/vendor/tui/types/render/text.d.ts +0 -64
  134. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  135. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  136. package/vendor/tui/types/render/usage.d.ts +0 -113
  137. package/vendor/tui/types/render/width.d.ts +0 -29
  138. package/vendor/tui/types/session-directory.d.ts +0 -190
  139. package/vendor/tui/types/session-query.d.ts +0 -92
  140. package/vendor/tui/types/session-switch.d.ts +0 -17
  141. package/vendor/tui/types/settings-file.d.ts +0 -41
  142. package/vendor/tui/types/skills.d.ts +0 -47
  143. package/vendor/tui/types/startup.d.ts +0 -65
  144. package/vendor/tui/types/store.d.ts +0 -58
  145. package/vendor/tui/types/subagents.d.ts +0 -70
  146. package/vendor/tui/types/terminal-title.d.ts +0 -66
  147. package/vendor/tui/types/theme-panel.d.ts +0 -24
  148. package/vendor/tui/types/theme.d.ts +0 -434
  149. package/vendor/tui/types/update-panel.d.ts +0 -49
  150. package/vendor/tui/types/update.d.ts +0 -75
  151. package/vendor/tui/types/version.d.ts +0 -19
  152. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  153. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  154. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,329 @@
1
+ /** Lightweight session-directory projection for the /resume picker. */
2
+ import { basename, dirname, resolve } from 'node:path';
3
+ import { realpathSync } from 'node:fs';
4
+ import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session';
5
+ import { t } from './i18n.mjs';
6
+ /** Case-insensitive filesystems (Windows, macOS) compare paths by lowercased form. */
7
+ const CASE_INSENSITIVE_FS = process.platform === 'win32' || process.platform === 'darwin';
8
+ /** True only for delegated subagents; ordinary forks also carry lineage. */
9
+ export function isSubagentSession(header) {
10
+ return header.origin === 'subagent';
11
+ }
12
+ function comparablePath(value) {
13
+ const resolved = resolve(value);
14
+ // Codex's paths_match_after_normalization pattern: canonicalize through the
15
+ // filesystem when the path exists (resolving symlinks, subst drives, and
16
+ // junctions that plain `resolve` keeps distinct), falling back to lexical
17
+ // resolution when the directory no longer does.
18
+ const fold = (path) => CASE_INSENSITIVE_FS ? path.toLowerCase() : path;
19
+ try {
20
+ return fold(realpathSync(resolved));
21
+ }
22
+ catch {
23
+ return fold(resolved);
24
+ }
25
+ }
26
+ /** Platform-consistent path equality for session cwd comparisons. */
27
+ function samePath(left, right) {
28
+ if (left === undefined)
29
+ return false;
30
+ return comparablePath(left) === comparablePath(right);
31
+ }
32
+ /**
33
+ * Unique header match by exact id or unique id prefix (root and subagent
34
+ * headers alike); the caller applies any lineage gate.
35
+ * @param headers - the persisted headers.
36
+ * @param wanted - the id or id prefix.
37
+ * @returns the uniquely matched header.
38
+ * @throws when nothing matches or the prefix is ambiguous.
39
+ */
40
+ export function matchSessionId(headers, wanted) {
41
+ const exact = headers.filter(header => header.id === wanted);
42
+ const matches = exact.length > 0 ? exact : headers.filter(header => header.id.startsWith(wanted));
43
+ if (matches.length === 0)
44
+ throw new Error(`no persisted session matches "${wanted}"`);
45
+ if (matches.length > 1) {
46
+ throw new Error(`session prefix "${wanted}" is ambiguous (${matches.length} matches): use more of the id`);
47
+ }
48
+ return matches[0];
49
+ }
50
+ /** The newest persisted ROOT session pinned to this cwd, or undefined. */
51
+ export function newestRootForCwd(headers, cwd) {
52
+ const local = headers
53
+ .filter(header => !isSubagentSession(header) && samePath(header.cwd, cwd))
54
+ .sort((left, right) => right.createdAt - left.createdAt);
55
+ return local[0];
56
+ }
57
+ /**
58
+ * Filter/sort header-only records. No session log is loaded here. Sorting is
59
+ * by LAST ACTIVITY (`updated` — artifact mtime when the caller resolved one,
60
+ * else createdAt), matching the codex resume picker's default UpdatedAt
61
+ * ordering: a session you kept talking in outranks one created later but idle.
62
+ * @param records - the header-only records.
63
+ * @param options - filter/sort options.
64
+ * @param updated - per-session last-activity timestamps, when resolved.
65
+ */
66
+ export function projectSessionRows(records, options, updated) {
67
+ const needle = options.query.trim().toLowerCase();
68
+ return records
69
+ .filter(record => options.sessions === 'all' || !isSubagentSession(record.header))
70
+ .filter(record => options.cwd === 'all' || samePath(record.header.cwd, options.currentCwd))
71
+ .map(record => {
72
+ const cwd = record.header.cwd ?? '';
73
+ const subagent = isSubagentSession(record.header);
74
+ const activity = updated?.get(record.header.id);
75
+ return {
76
+ id: record.header.id,
77
+ createdAt: record.header.createdAt,
78
+ updatedAt: activity === undefined || !Number.isFinite(activity) || activity < record.header.createdAt
79
+ ? record.header.createdAt
80
+ : activity,
81
+ cwd,
82
+ workspace: cwd === '' ? '(no workspace)' : basename(cwd),
83
+ parent: record.header.parentSession,
84
+ subagent,
85
+ resumable: !subagent,
86
+ live: record.live,
87
+ persisted: record.persisted,
88
+ preset: record.header.agentPreset ?? 'standard',
89
+ };
90
+ })
91
+ .filter(row => needle === '' || `${row.id} ${row.cwd} ${row.workspace} ${row.preset}`.toLowerCase().includes(needle))
92
+ .sort((left, right) => options.sort === 'newest'
93
+ ? right.updatedAt - left.updatedAt || right.createdAt - left.createdAt
94
+ : left.updatedAt - right.updatedAt || left.createdAt - right.createdAt);
95
+ }
96
+ /** Merge page-local title observations without disturbing directory order. */
97
+ export function mergeSessionTitles(rows, observations) {
98
+ const titles = new Map();
99
+ for (const observation of observations) {
100
+ if (observation.status !== 'fulfilled')
101
+ continue;
102
+ const title = observation.value?.title?.title;
103
+ if (title !== undefined && title.trim() !== '')
104
+ titles.set(observation.sessionId, title);
105
+ }
106
+ return rows.map(row => titles.has(row.id) ? { ...row, title: titles.get(row.id) } : row);
107
+ }
108
+ /**
109
+ * Encode a session id the way the JSONL backend does for its on-disk layout
110
+ * (`encodeSegment`: safe units literal, everything else `~XXXX`). Used to
111
+ * validate and derive session directories — a local copy of the pure upstream
112
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
113
+ */
114
+ export function encodeSessionSegment(raw) {
115
+ if (raw.length === 0)
116
+ throw new Error('cannot encode an empty path segment');
117
+ if (raw === '.')
118
+ return '~002E';
119
+ if (raw === '..')
120
+ return '~002E~002E';
121
+ let out = '';
122
+ for (let i = 0; i < raw.length; i += 1) {
123
+ const code = raw.charCodeAt(i);
124
+ const ch = String.fromCharCode(code);
125
+ if (ch !== '~' && /^[A-Za-z0-9._-]$/u.test(ch)) {
126
+ out += ch;
127
+ }
128
+ else {
129
+ out += `~${code.toString(16).toUpperCase().padStart(4, '0')}`;
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ /**
135
+ * Encode a project cwd the way the JSONL backend groups sessions on disk
136
+ * (`projectKey`: separators collapse to one `-`, everything else mirrors
137
+ * `encodeSegment`, bounded to 251 chars). A local copy of the pure upstream
138
+ * contract, kept in sync with `session-persistence-jsonl/src/format.ts`.
139
+ */
140
+ export function encodeProjectKey(cwd) {
141
+ if (cwd.length === 0)
142
+ throw new Error('cannot encode an empty project path');
143
+ let readable = '';
144
+ let separatorRun = false;
145
+ for (let i = 0; i < cwd.length; i += 1) {
146
+ const code = cwd.charCodeAt(i);
147
+ const ch = String.fromCharCode(code);
148
+ if (ch === '/' || ch === '\\' || ch === ':') {
149
+ if (!separatorRun)
150
+ readable += '-';
151
+ separatorRun = true;
152
+ }
153
+ else if (ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)) {
154
+ readable += ch;
155
+ separatorRun = false;
156
+ }
157
+ else {
158
+ readable += `~${code.toString(16).toUpperCase().padStart(4, '0')}`;
159
+ separatorRun = false;
160
+ }
161
+ }
162
+ const slug = readable.replace(/^-+/, '') || 'root';
163
+ return `--${slug.slice(0, 251)}--`;
164
+ }
165
+ /** The project-level directory name the JSONL backend uses for a missing cwd. */
166
+ const NO_CWD_DIRECTORY = '_no-cwd';
167
+ /**
168
+ * Derive one session's artifact directory under the JSONL backend root,
169
+ * mirroring the upstream `<root>/<projectKey(cwd)>/<encodeSegment(id)>/`
170
+ * layout (0.1.5 `sessionDir`/`projectDir`).
171
+ * @param root - the JSONL backend's configured session root.
172
+ * @param cwd - the session's pinned working directory, when the header has one.
173
+ * @param id - the session id.
174
+ * @returns the absolute session directory path.
175
+ */
176
+ export function sessionDirectoryFor(root, cwd, id) {
177
+ const project = cwd === undefined || cwd === '' ? NO_CWD_DIRECTORY : encodeProjectKey(cwd);
178
+ return resolve(root, project, encodeSessionSegment(id));
179
+ }
180
+ /**
181
+ * The canonical session-log artifact filenames the JSONL backend may create:
182
+ * format v0 writes the bare `session.jsonl` name; v1+ write
183
+ * `session.vN.jsonl`, each generation optionally zstd-compressed. Multiple
184
+ * immutable generations may coexist in one session directory (0.1.5). The
185
+ * range follows the installed session package's `SESSION_FORMAT_VERSION`, so
186
+ * a future generation joins the enumeration with the dependency bump.
187
+ */
188
+ export function sessionArtifactNames() {
189
+ const names = ['session.jsonl', 'session.jsonl.zstd'];
190
+ for (let version = 1; version <= SESSION_FORMAT_VERSION; version += 1) {
191
+ names.push(`session.v${version}.jsonl`, `session.v${version}.jsonl.zstd`);
192
+ }
193
+ return names;
194
+ }
195
+ /** Canonical generation-log filenames as a lookup set (bare v0 or `vN`-suffixed, ± zstd). */
196
+ const SESSION_ARTIFACT_NAME_SET = new Set(sessionArtifactNames());
197
+ /** True for one canonical session-log artifact filename the backend may own. */
198
+ export function isSessionArtifactName(name) {
199
+ return SESSION_ARTIFACT_NAME_SET.has(name);
200
+ }
201
+ /**
202
+ * Guard a derived session directory before deletion (codex's scoped-path
203
+ * check, adapted to the JSONL layout): the directory's base name must be
204
+ * exactly `encodeSegment(id)` beneath its project grouping.
205
+ * @param dir - the derived session artifact directory.
206
+ * @param id - the session id the directory claims to belong to.
207
+ * @returns the guarded directory, or undefined when the layout is unexpected.
208
+ */
209
+ export function sessionArtifactDirectory(dir, id) {
210
+ if (basename(dir) !== encodeSessionSegment(id))
211
+ return undefined;
212
+ if (basename(dirname(dir)) === NO_CWD_DIRECTORY)
213
+ return dir;
214
+ return /^--.*--$|^~/.test(basename(dirname(dir))) ? dir : undefined;
215
+ }
216
+ /**
217
+ * The JSONL backend's configured session root, when the mounted backend
218
+ * exposes one. The upstream service contract dropped `locate()` in 0.1.5
219
+ * (artifact paths are backend-private; only refusal diagnostics carry them),
220
+ * so the TUI derives artifact paths from the backend's public plugin config.
221
+ * Backends without a JSONL-style config (or a foreign shape) yield undefined
222
+ * and callers degrade: mtime sorting falls back to createdAt and /delete
223
+ * refuses, exactly as before.
224
+ */
225
+ export function jsonlSessionRoot(persistence) {
226
+ const root = persistence?.config?.root;
227
+ return typeof root === 'string' && root !== '' ? root : undefined;
228
+ }
229
+ /**
230
+ * Collect one session's deletion subtree: the id plus every record whose
231
+ * parent chain leads to it (codex deletes subagent threads with their root).
232
+ * @param records - the full directory listing.
233
+ * @param id - the root session id to delete.
234
+ * @returns the ids to delete, root first.
235
+ */
236
+ export function collectDeletionSubtree(records, id) {
237
+ const parentOf = new Map();
238
+ for (const record of records)
239
+ parentOf.set(record.header.id, record.header.parentSession);
240
+ const doomed = new Set([id]);
241
+ // Iterate to a fixed point: children may be listed before their parents.
242
+ for (let pass = 0; pass < 2; pass += 1) {
243
+ for (const candidate of parentOf.keys()) {
244
+ if (doomed.has(candidate))
245
+ continue;
246
+ let ancestor = parentOf.get(candidate);
247
+ let depth = 0;
248
+ while (ancestor !== undefined && depth < 64) {
249
+ if (doomed.has(ancestor)) {
250
+ doomed.add(candidate);
251
+ break;
252
+ }
253
+ ancestor = parentOf.get(ancestor);
254
+ depth += 1;
255
+ }
256
+ }
257
+ }
258
+ return [...doomed];
259
+ }
260
+ /**
261
+ * Plan one session-subtree deletion with NO filesystem side effects: collect
262
+ * the doomed lineage, refuse when the root or ANY member is live (a live
263
+ * child would outlive its deleted parent) or missing from the listing, and
264
+ * order the result children-first so the executor can never leave a deleted
265
+ * parent behind surviving children. Artifact-location guards stay at the
266
+ * call site; this is the pure preflight they complete.
267
+ * @param records - the full directory listing.
268
+ * @param id - the root session id to delete.
269
+ * @returns the ordered plan, or a user-facing refusal reason.
270
+ */
271
+ export function planSessionDeletion(records, id) {
272
+ const target = records.find(record => record.header.id === id);
273
+ if (target === undefined)
274
+ return { ok: false, reason: `no persisted session matches "${id}"` };
275
+ if (target.live)
276
+ return { ok: false, reason: 'cannot delete a live session — it is open in this or another process' };
277
+ const doomed = collectDeletionSubtree(records, id);
278
+ const byId = new Map(records.map(record => [record.header.id, record]));
279
+ const parentOf = new Map();
280
+ for (const record of records)
281
+ parentOf.set(record.header.id, record.header.parentSession);
282
+ const nodes = [];
283
+ for (const candidate of doomed) {
284
+ const record = byId.get(candidate);
285
+ if (record === undefined)
286
+ return { ok: false, reason: `no persisted session matches "${candidate}"` };
287
+ if (record.live) {
288
+ return {
289
+ ok: false,
290
+ reason: `cannot delete: child session ${candidate.slice(-12)} is live — close it (and any process using it) first`,
291
+ };
292
+ }
293
+ let depth = 0;
294
+ let ancestor = parentOf.get(candidate);
295
+ while (ancestor !== undefined && depth < 64) {
296
+ depth += 1;
297
+ ancestor = parentOf.get(ancestor);
298
+ }
299
+ nodes.push({ id: candidate, depth });
300
+ }
301
+ // Deepest first: a mid-deletion failure then leaves the shallowest lineage
302
+ // intact, never a deleted parent with surviving children.
303
+ nodes.sort((left, right) => right.depth - left.depth);
304
+ return { ok: true, nodes };
305
+ }
306
+ /**
307
+ * Codex-style relative time for session rows ("now", "5m ago", "3h ago",
308
+ * "2d ago"; older than a week falls back to the local date).
309
+ * @param timestamp - epoch milliseconds of the last activity.
310
+ * @param now - the pinned reference clock (one value per list render).
311
+ */
312
+ export function formatRelativeTime(timestamp, now) {
313
+ const seconds = Math.round((now - timestamp) / 1000);
314
+ if (seconds < 60)
315
+ return t('time.justNow');
316
+ const minutes = Math.round(seconds / 60);
317
+ if (minutes < 60)
318
+ return t('time.minutesAgo', { n: minutes });
319
+ const hours = Math.round(minutes / 60);
320
+ if (hours < 24)
321
+ return t('time.hoursAgo', { n: hours });
322
+ const days = Math.round(hours / 24);
323
+ if (days < 7)
324
+ return t('time.daysAgo', { n: days });
325
+ const date = new Date(timestamp);
326
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
327
+ const day = `${date.getDate()}`.padStart(2, '0');
328
+ return `${date.getFullYear()}-${month}-${day}`;
329
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Skip-tolerant session-query engine for this terminal.
3
+ *
4
+ * The upstream SqliteSessionQueryEngine reconciliation observes EVERY
5
+ * persisted session before each search, and ONE unreadable source (for
6
+ * example a pre-release session artifact the frozen format codecs reject)
7
+ * fails the whole pass with SESSION_QUERY_PERSISTENCE_FAILED — every
8
+ * cross-session search dies because of a single old file nobody opened
9
+ * otherwise. This subclass overrides only the observation loop so an
10
+ * unreadable source is skipped with a warning and the rest of the corpus
11
+ * indexes normally; skipped sessions are retried on later reconciliations
12
+ * and rejoin automatically once a host that can read them is installed.
13
+ *
14
+ * Vendored surface note: `_observeStable` and its module-local helpers are
15
+ * private upstream; this file re-declares the observation loop against the
16
+ * pinned @deepseek-ai line (see package.json peers) and must be re-checked
17
+ * whenever that line moves. The engine class and the tool boundary also
18
+ * share ONE physical parent-package instance through this bundle, which
19
+ * restores instanceof-based typed error messages on the search path.
20
+ */
21
+ import { createHash } from 'node:crypto';
22
+ import SqliteSessionQueryEngine from '@deepseek-ai/dsh-session-query-sqlite';
23
+ import { assertSessionHeadersCompatible, buildSessionEventSearchDocuments, readColdSessionLog, SessionQueryError, } from '@deepseek-ai/dsh-session-query';
24
+ const STABLE_OBSERVATION_ATTEMPTS = 2;
25
+ function assertNotAborted(signal) {
26
+ if (signal?.aborted) {
27
+ throw new SessionQueryError('session-search aborted', 'SESSION_QUERY_ABORTED');
28
+ }
29
+ }
30
+ function isAbort(error) {
31
+ return error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED';
32
+ }
33
+ function errorMessage(error) {
34
+ return error instanceof Error ? error.message : 'unknown error';
35
+ }
36
+ function observeSession(header, inheritedEventCount, events) {
37
+ const detachedHeader = structuredClone(header);
38
+ const detachedEvents = events.map(event => structuredClone(event));
39
+ return {
40
+ header: detachedHeader,
41
+ inheritedEventCount,
42
+ documents: buildSessionEventSearchDocuments(detachedHeader.id, detachedEvents),
43
+ fingerprint: createHash('sha256')
44
+ .update(JSON.stringify({ header: detachedHeader, inheritedEventCount, events: detachedEvents }))
45
+ .digest('base64url'),
46
+ };
47
+ }
48
+ function sameHeader(a, b) {
49
+ return a.id === b.id
50
+ && a.createdAt === b.createdAt
51
+ && a.cwd === b.cwd
52
+ && a.parentSession === b.parentSession
53
+ && a.isSeeded === b.isSeeded
54
+ && (a.delegationDepth ?? 0) === (b.delegationDepth ?? 0)
55
+ && a.agentPreset === b.agentPreset;
56
+ }
57
+ function materializePersistenceSnapshots(snapshots) {
58
+ // `Array.isArray` narrows a declared array type to `any[]`, which would let
59
+ // every later access escape the type system. Step through `unknown` so the
60
+ // guard stays a runtime assertion and the element type stays declared.
61
+ const candidate = snapshots;
62
+ if (!Array.isArray(candidate))
63
+ throw new Error('persistence snapshots must be an array');
64
+ const result = new Map();
65
+ for (const snapshot of candidate) {
66
+ if (typeof snapshot.revision !== 'string') {
67
+ throw new Error('persistence snapshot revision must be a string');
68
+ }
69
+ const header = structuredClone(snapshot.header);
70
+ if (result.has(header.id)) {
71
+ throw new Error(`persistence listed duplicate session "${header.id}"`);
72
+ }
73
+ result.set(header.id, { header, revision: snapshot.revision });
74
+ }
75
+ return result;
76
+ }
77
+ function samePersistenceSnapshots(before, after) {
78
+ if (before.size !== after.size)
79
+ return false;
80
+ for (const [id, first] of before) {
81
+ const second = after.get(id);
82
+ if (second === undefined || first.revision !== second.revision || !sameHeader(first.header, second.header))
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+ /**
88
+ * The skip-tolerant observation pass: structurally the upstream loop, with
89
+ * the per-source cold read wrapped so one unreadable session degrades to a
90
+ * warning instead of failing every search. Exported for unit tests with an
91
+ * injectable cold reader.
92
+ */
93
+ export async function observeStableWithSkip(engine, indexed, signal, readCold = readColdSessionLog) {
94
+ for (let attempt = 0; attempt < STABLE_OBSERVATION_ATTEMPTS; attempt += 1) {
95
+ assertNotAborted(signal);
96
+ const persistenceBinding = engine._persistenceBinding;
97
+ const persistence = persistenceBinding.service;
98
+ const initiallyLive = new Set(engine.ctx.sessions.list().map(session => session.id));
99
+ let persisted = new Map();
100
+ if (persistence !== undefined) {
101
+ try {
102
+ const canReuseIndexed = engine._lastPersistenceIdentity === undefined
103
+ || engine._lastPersistenceIdentity === persistenceBinding.identity;
104
+ const listOptions = signal === undefined ? undefined : { signal };
105
+ const before = await persistence.list(listOptions);
106
+ assertNotAborted(signal);
107
+ persisted = materializePersistenceSnapshots(before);
108
+ for (const entry of persisted.values()) {
109
+ if (canReuseIndexed && indexed.get(entry.header.id)?.revision === entry.revision)
110
+ continue;
111
+ if (initiallyLive.has(entry.header.id) || engine.ctx.sessions.get(entry.header.id) !== undefined)
112
+ continue;
113
+ assertNotAborted(signal);
114
+ // The skip: a session whose cold log fails to migrate or decode
115
+ // stays OUT of the index with one warning; the remaining corpus
116
+ // indexes normally. `loaded` stays undefined exactly like a
117
+ // not-yet-read entry, so the stable-snapshot comparison and the
118
+ // live-preferred merge below are unaffected.
119
+ try {
120
+ const loaded = await readCold(persistence, entry.header.id, signal);
121
+ assertNotAborted(signal);
122
+ assertSessionHeadersCompatible(entry.header, loaded.header);
123
+ entry.loaded = observeSession(loaded.header, loaded.inheritedEventCount, loaded.events);
124
+ }
125
+ catch (error) {
126
+ if (isAbort(error) || signal?.aborted)
127
+ throw error;
128
+ if (engine._persistenceBinding !== persistenceBinding)
129
+ break;
130
+ entry.unreadable = errorMessage(error);
131
+ engine.ctx.logger?.warn('session-search skipped unreadable session %s: %s', entry.header.id, entry.unreadable);
132
+ }
133
+ }
134
+ assertNotAborted(signal);
135
+ const afterSnapshots = await persistence.list(listOptions);
136
+ assertNotAborted(signal);
137
+ const after = materializePersistenceSnapshots(afterSnapshots);
138
+ if (!samePersistenceSnapshots(persisted, after))
139
+ continue;
140
+ if (engine._persistenceBinding !== persistenceBinding)
141
+ continue;
142
+ }
143
+ catch (error) {
144
+ if (isAbort(error) || signal?.aborted) {
145
+ throw new SessionQueryError('session-search aborted', 'SESSION_QUERY_ABORTED', { cause: error });
146
+ }
147
+ if (engine._persistenceBinding !== persistenceBinding)
148
+ continue;
149
+ if (error instanceof SessionQueryError)
150
+ throw error;
151
+ throw new SessionQueryError(`session-search persistence observation failed: ${errorMessage(error)}`, 'SESSION_QUERY_PERSISTENCE_FAILED', { cause: error });
152
+ }
153
+ }
154
+ const live = new Map();
155
+ for (const session of engine.ctx.sessions.list()) {
156
+ const observed = observeSession(session.header, session.inheritedEventCount, session.snapshotEvents());
157
+ const durable = persisted.get(session.id);
158
+ if (durable !== undefined && durable.loaded === undefined) {
159
+ // A live owner always wins over a skipped durable copy.
160
+ live.set(session.id, observed);
161
+ continue;
162
+ }
163
+ if (durable !== undefined)
164
+ assertSessionHeadersCompatible(observed.header, durable.header);
165
+ live.set(session.id, observed);
166
+ }
167
+ const sameLive = initiallyLive.size === live.size && [...initiallyLive].every(id => live.has(id));
168
+ if (!sameLive)
169
+ continue;
170
+ return { persistenceBinding, persisted, live };
171
+ }
172
+ throw new SessionQueryError('session-search persistence observation did not stabilize after one retry', 'SESSION_QUERY_PERSISTENCE_FAILED');
173
+ }
174
+ // The opaque-base cast keeps the private `_observeStable` override legal in
175
+ // TypeScript while inheriting every runtime static (inject, Config, Service
176
+ // metadata) from the real engine class.
177
+ const EngineBase = SqliteSessionQueryEngine;
178
+ /** The engine this bundle mounts in place of the base `session-query-sqlite` row. */
179
+ export class SkipTolerantSessionQueryEngine extends EngineBase {
180
+ async _observeStable(indexed, signal) {
181
+ return await observeStableWithSkip(this, indexed, signal);
182
+ }
183
+ }
184
+ export default SkipTolerantSessionQueryEngine;
@@ -0,0 +1,51 @@
1
+ /** Latest-wins, idle-bound queue for safe Agent session changes. */
2
+ export class SessionSwitchQueue {
3
+ pending;
4
+ pumping = false;
5
+ execute;
6
+ failed;
7
+ constructor(execute, failed) {
8
+ this.execute = execute;
9
+ this.failed = failed;
10
+ }
11
+ /** Queue a request; a later request replaces any request still waiting. */
12
+ request(activity, value) {
13
+ this.pending = { activity, value };
14
+ const outcome = activity.status === 'running' || this.pumping ? 'queued' : 'started';
15
+ if (!this.pumping)
16
+ void this.pump();
17
+ return outcome;
18
+ }
19
+ /** Cancel only work that has not begun activation. */
20
+ cancel() {
21
+ if (this.pending === undefined)
22
+ return false;
23
+ this.pending = undefined;
24
+ return true;
25
+ }
26
+ async pump() {
27
+ this.pumping = true;
28
+ try {
29
+ while (this.pending !== undefined) {
30
+ const observed = this.pending;
31
+ await observed.activity.whenIdle();
32
+ // Another request replaced this one while the turn was converging.
33
+ if (this.pending !== observed)
34
+ continue;
35
+ this.pending = undefined;
36
+ try {
37
+ await this.execute(observed.value);
38
+ }
39
+ catch (error) {
40
+ this.failed(error);
41
+ }
42
+ }
43
+ }
44
+ finally {
45
+ this.pumping = false;
46
+ // A request may land between the loop condition and finally.
47
+ if (this.pending !== undefined)
48
+ void this.pump();
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Serialized, crash-atomic persistence for the small user-level JSON files
3
+ * under the DSH home (statusline.json, theme.json). Two guarantees the bare
4
+ * floating `writeFile` path could not give:
5
+ *
6
+ * 1. Every save is appended to ONE chain, so rapid consecutive edits land
7
+ * in submission order and the last snapshot is the one on disk (parallel
8
+ * floating writes let an older snapshot finish last and win).
9
+ * 2. Each write goes to a sibling temp file first and is renamed into
10
+ * place, so a crash mid-write can never leave a half-written JSON
11
+ * document behind.
12
+ *
13
+ * The chain itself never rejects: a failed write is reported to that
14
+ * save's caller while later saves keep their turn.
15
+ *
16
+ * @module @deepseek-ai/dsh-code/settings-file
17
+ */
18
+ import { randomUUID } from 'node:crypto';
19
+ import { mkdir, rename, writeFile } from 'node:fs/promises';
20
+ import { dirname } from 'node:path';
21
+ /**
22
+ * Run one file operation with a bounded retry: one initial try plus at
23
+ * most `retries` more. Creating or replacing a file can fail transiently
24
+ * with EPERM/EACCES while an antivirus scanner or search indexer holds
25
+ * it — the standard graceful-fs remedy, not a workaround for a
26
+ * persistent permission problem. A save that still fails leaves its
27
+ * uniquely named temp file behind, so repeated crashed saves accumulate
28
+ * distinct leftovers rather than corrupting a shared one.
29
+ */
30
+ async function withTransientRetry(operation, retries = 5) {
31
+ for (let attempt = 0;; attempt += 1) {
32
+ try {
33
+ await operation();
34
+ return;
35
+ }
36
+ catch (error) {
37
+ const code = error.code;
38
+ if (attempt >= retries || (code !== 'EPERM' && code !== 'EACCES'))
39
+ throw error;
40
+ await new Promise(resolve => setTimeout(resolve, 30 * (attempt + 1)));
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Write one file atomically: create the parent directory, write to a
46
+ * uniquely named temp file, and rename it into place. A crash midway
47
+ * can never leave a half-written document behind. Unique temp names
48
+ * keep concurrent writers (two terminals, two chains in one process)
49
+ * from sharing one temp path.
50
+ */
51
+ export async function writeFileAtomically(path, text) {
52
+ await mkdir(dirname(path), { recursive: true });
53
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
54
+ await withTransientRetry(() => writeFile(temp, text, 'utf8'));
55
+ await withTransientRetry(() => rename(temp, path));
56
+ }
57
+ /**
58
+ * Create the shared settings-write chain. One instance per process keeps
59
+ * every user-level JSON file mutually serialized.
60
+ * @returns the persistence handle.
61
+ */
62
+ export function createUserSettingsPersistence() {
63
+ let chain = Promise.resolve();
64
+ return {
65
+ save(path, text) {
66
+ const write = chain.then(() => writeFileAtomically(path, text));
67
+ // A failed write must not break the chain for later saves.
68
+ chain = write.catch(() => { });
69
+ return write;
70
+ },
71
+ flush() {
72
+ return chain;
73
+ },
74
+ };
75
+ }