@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
@@ -13,7 +13,8 @@ import { readActivityFrames } from '../activityPrefs.js';
13
13
  import { readModelPref } from '../modelPrefs.js';
14
14
  import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
15
15
  import { readPresetPref } from '../presetPrefs.js';
16
- import { composePreset, resolvePersistedPreset, runningPresetOf } from './presets.js';
16
+ import { composePreset, filterMinimalPresetTools, resolvePersistedPreset, runningPresetOf } from './presets.js';
17
+ import { ensurePackagedPresets } from './packaged-presets.js';
17
18
  import { ensureLegacySessionEventTypes } from './compat/index.js';
18
19
  import { clearResumeTarget, writeResumeTarget } from '../sessionHistory.js';
19
20
  import { resolveSessionCwd } from '../utils/workspaceRoot.js';
@@ -22,6 +23,7 @@ import { isLang, resolveStartupLang, setLang, t } from '../i18n.js';
22
23
  import { detectLegacyEnv, migrateLegacyDataDir, RENAMED_ENV } from '../utils/paths.js';
23
24
  import { Chat } from '../screens/Chat.js';
24
25
  import { attachSessionToWorkspace } from './workspace.js';
26
+ import { createLocalWorkspaceRuntime } from './workspaces.js';
25
27
  import { render, ThemeProvider, AlternateScreen } from '../ui.js';
26
28
  import instances from '../ink/instances.js';
27
29
  import { cursorMove, DISABLE_KITTY_KEYBOARD, DISABLE_MODIFY_OTHER_KEYS, DISABLE_WIN32_INPUT_MODE } from '../ink/termio/csi.js';
@@ -40,6 +42,22 @@ export async function apply(ctx, config) {
40
42
  if (!process.stdout.isTTY) {
41
43
  throw new Error('dsh-tui requires an interactive terminal (stdout must be a TTY).');
42
44
  }
45
+ // The official profile launcher owns the system preset root and replaces
46
+ // any bundle-supplied roots at boot. Install dsh-tui's bundled presets via
47
+ // the roster's supported user-root seam before resolving the first agent.
48
+ // Never overwrite an existing directory unless it carries our marker.
49
+ try {
50
+ for (const result of ensurePackagedPresets()) {
51
+ if (result.status === 'conflict') {
52
+ ctx.logger.warn(`dsh-tui: packaged preset "${result.id}" was not installed because an unmanaged preset already uses that id`);
53
+ }
54
+ }
55
+ }
56
+ catch (error) {
57
+ // A read-only home must not make the whole terminal unusable; the other
58
+ // official and user presets remain available.
59
+ ctx.logger.warn(`dsh-tui: unable to install packaged presets (${error instanceof Error ? error.message : String(error)})`);
60
+ }
43
61
  // Data-directory rename (~/.dsh-cc → ~/.dsh-tui, issue #120): copy the
44
62
  // legacy directory before ANY preference read below (resolveStartupLang
45
63
  // already touches lang.json). Copy, not move — old launchers keep working
@@ -98,6 +116,16 @@ export async function apply(ctx, config) {
98
116
  // the inject proxy.
99
117
  const userQuestions = ctx.get('userQuestions') ?? new UserQuestionService(ctx);
100
118
  ctx.plugin(toolAskUser);
119
+ // The host-level tool mount above is intentional for the TUI and for user
120
+ // presets, but the official Minimal preset is a strict two-tool trajectory
121
+ // (persistent bash + str_replace_editor). Filter only that preset at the
122
+ // final assembly boundary. Reading the session on every assembly also makes
123
+ // blank-session /preset switches and resumed sessions behave correctly.
124
+ ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
125
+ const assembled = await next();
126
+ const presetId = context.agent === undefined ? undefined : runningPresetOf(context.agent.session);
127
+ return filterMinimalPresetTools(assembled, presetId);
128
+ });
101
129
  const questionStore = new QuestionStore();
102
130
  // Packaged skills (/audit, /bug, …): contribute them through the host's
103
131
  // skill registry so they resolve with zero manual copying.
@@ -170,9 +198,22 @@ export async function apply(ctx, config) {
170
198
  // see the repository, not an arbitrary launch subdirectory. Resolved ONCE
171
199
  // here — the agent meta and the channel must agree.
172
200
  const requestedWorkspace = config.workspace ?? process.env.DSH_TUI_WORKSPACE_TARGET;
201
+ // Degraded boot (issue #183): a stale bundle patch without the
202
+ // dsh-tui-workspaces row leaves the service unmounted; resolve startup
203
+ // targets through the local-only runtime (provider URIs then fail loud
204
+ // below instead of crashing on an undefined service). A profile launch
205
+ // without the service means the patch came from an older dsh-tui copy
206
+ // than the running code — warn once so the skew is diagnosable. Bare
207
+ // embedders (no --profile) take the same fallback by design, silently.
208
+ const mountedWorkspaceService = ctx.get('tuiWorkspaces');
209
+ if (mountedWorkspaceService === undefined && resolveDshProfileName() !== undefined) {
210
+ ctx.logger.warn('dsh-tui: tuiWorkspaces service is not mounted; /workspace runs with the local-only fallback. ' +
211
+ 'The bundle patch is older than the installed dsh-tui package — update the globally installed dsh-tui launcher to match the profile (issue #183).');
212
+ }
213
+ const workspaceService = mountedWorkspaceService ?? createLocalWorkspaceRuntime();
173
214
  const initialWorkspace = requestedWorkspace === undefined
174
215
  ? undefined
175
- : await ctx.tuiWorkspaces.resolve(requestedWorkspace);
216
+ : await workspaceService.resolve(requestedWorkspace);
176
217
  if (requestedWorkspace !== undefined && initialWorkspace === undefined) {
177
218
  throw new Error(`dsh-tui: unsupported or unavailable workspace target: ${requestedWorkspace}`);
178
219
  }
@@ -241,6 +282,15 @@ export async function apply(ctx, config) {
241
282
  ctx.on('approval/request', (req, next) => String(req.agent.id) === channel.agentId ? approvalStore.park(req) : next());
242
283
  ctx.effect(() => () => approvalStore.settleAll('cancelled'));
243
284
  }
285
+ // Positional command-line arguments are the initial prompt (issue #53):
286
+ // `dsh-tui "run the tests"` forwards positionals through the dsh CLI,
287
+ // which mounts them as ctx.cmdlineArgs. Submit once the channel exists —
288
+ // delivery goes through the normal pending/inbox chain, so no special
289
+ // timing is needed; flag-shaped leftovers are not prompt text.
290
+ const cmdlineArgs = ctx.cmdlineArgs?.args;
291
+ const initialPrompt = cmdlineArgs?.filter(arg => !arg.startsWith('-')).join(' ').trim();
292
+ if (initialPrompt)
293
+ channel.submit(initialPrompt);
244
294
  // Attach the stderr reporter to the live channel and flush anything a
245
295
  // startup-spawned server produced while the channel didn't exist yet.
246
296
  notifyStderr = (text, options) => channel.notify(text, options);
@@ -318,8 +368,9 @@ export async function apply(ctx, config) {
318
368
  channel,
319
369
  questionStore,
320
370
  approvalStore,
321
- // The trajectory scene enters the alt screen itself in inline mode; in
322
- // fullscreen the tree is already wrapped below, so it must not nest.
371
+ // Full-screen surfaces inside Chat — the trajectory scene and the session
372
+ // browser — enter the alt screen themselves in inline mode; in fullscreen
373
+ // the tree is already wrapped below, so they must not nest.
323
374
  fullscreen: config.fullscreen === true,
324
375
  onExit: () => handleExit(),
325
376
  // Only a `dsh --profile <name>` launch has a profile installation for
@@ -365,8 +416,13 @@ export async function apply(ctx, config) {
365
416
  // but flag it as teardown first so the settling waitUntilExit does not
366
417
  // run the user-exit sequence: no resume marker, no disposeRootAndExit,
367
418
  // the process stays alive and the recomposed tree re-mounts the TUI.
419
+ // Hand back what the channel contributed to host registries on the way out:
420
+ // the command registry scopes a registration to ITS own context, so the
421
+ // skill commands (issue #86) would survive this exact recompose and the
422
+ // re-mounted channel would find the names taken, freezing its menu.
368
423
  ctx.effect(() => () => {
369
424
  funnel.markTeardown();
425
+ channel.releaseContributions();
370
426
  instance?.unmount();
371
427
  });
372
428
  // The TUI is the front door: when the user unmounts it (Ctrl+C), dispose
@@ -18,6 +18,7 @@
18
18
  import type { Context } from '@deepseek-ai/cordis';
19
19
  import type { AgentSetup } from '@deepseek-ai/dsh-agent';
20
20
  import type { SessionId } from '@deepseek-ai/dsh-session';
21
+ import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt';
21
22
  /** One roster entry, as returned by `agentPresets.list()`/`resolve()`. */
22
23
  export interface AgentPresetInfo {
23
24
  readonly id: string;
@@ -93,6 +94,21 @@ export declare function runningPresetOf(session: {
93
94
  data: unknown;
94
95
  }[];
95
96
  }): string | undefined;
97
+ /**
98
+ * Keep the official Minimal preset's model-facing contract at exactly two
99
+ * tools. The TUI mounts ask_user_question at the host layer so every other
100
+ * preset (including user presets) can use its questionnaire UI; host-layer
101
+ * tools otherwise merge into Minimal's scoped catalog as a third tool.
102
+ *
103
+ * This is a per-assembly filter rather than a startup-time decision because
104
+ * one TUI process can resume, create, or recompose sessions under different
105
+ * presets. The session log remains the source of truth for the active preset.
106
+ *
107
+ * @param assembly - Fully assembled prompt inputs for one model request.
108
+ * @param presetId - Preset recorded for the requesting session.
109
+ * @returns The original assembly, except ask_user_question is absent in Minimal.
110
+ */
111
+ export declare function filterMinimalPresetTools(assembly: PromptAssembly, presetId: string | undefined): PromptAssembly;
96
112
  /**
97
113
  * Read a service the way a joined agent sees it: through the preset scope
98
114
  * chain when a roster is mounted (preset realms hide e.g. `compaction` from
@@ -1 +1 @@
1
- {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/presets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAGzD,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAA;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,IAAI,IAAI,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC9C,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC/D,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAClE,yEAAyE;IACzE,UAAU,CAAC,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,OAAO,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3D;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAEnE;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmBhG;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqB5G;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,MAAM,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,MAAM,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACnD,GAAG,MAAM,GAAG,SAAS,CAErB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAKpG"}
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/presets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAKpE,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAA;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,+DAA+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,IAAI,IAAI,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC9C,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC/D,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAClE,yEAAyE;IACzE,UAAU,CAAC,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,OAAO,CAAA;KAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC3D;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAEnE;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAmBhG;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqB5G;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,MAAM,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,MAAM,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CACnD,GAAG,MAAM,GAAG,SAAS,CAErB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CAM/G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAKpG"}
@@ -16,6 +16,7 @@
16
16
  * before presets existed.
17
17
  */
18
18
  import { resolveSessionPreset } from '@deepseek-ai/dsh-agent-presets';
19
+ const ASK_USER_TOOL = 'ask_user_question';
19
20
  /**
20
21
  * The mounted preset roster, or undefined when the composition has no
21
22
  * `agent-presets` row (bare boots) — optional-service access via `ctx.get`.
@@ -94,6 +95,28 @@ export async function resolvePersistedPreset(ctx, sessionId) {
94
95
  export function runningPresetOf(session) {
95
96
  return resolveSessionPreset(session);
96
97
  }
98
+ /**
99
+ * Keep the official Minimal preset's model-facing contract at exactly two
100
+ * tools. The TUI mounts ask_user_question at the host layer so every other
101
+ * preset (including user presets) can use its questionnaire UI; host-layer
102
+ * tools otherwise merge into Minimal's scoped catalog as a third tool.
103
+ *
104
+ * This is a per-assembly filter rather than a startup-time decision because
105
+ * one TUI process can resume, create, or recompose sessions under different
106
+ * presets. The session log remains the source of truth for the active preset.
107
+ *
108
+ * @param assembly - Fully assembled prompt inputs for one model request.
109
+ * @param presetId - Preset recorded for the requesting session.
110
+ * @returns The original assembly, except ask_user_question is absent in Minimal.
111
+ */
112
+ export function filterMinimalPresetTools(assembly, presetId) {
113
+ if (presetId !== 'minimal' || !assembly.tools.some(tool => tool.name === ASK_USER_TOOL))
114
+ return assembly;
115
+ return {
116
+ ...assembly,
117
+ tools: assembly.tools.filter(tool => tool.name !== ASK_USER_TOOL),
118
+ };
119
+ }
97
120
  /**
98
121
  * Read a service the way a joined agent sees it: through the preset scope
99
122
  * chain when a roster is mounted (preset realms hide e.g. `compaction` from
@@ -0,0 +1,29 @@
1
+ import type { PreviewEntry, SessionDigest } from './types.js';
2
+ /** Head window budget. Eight times the measured worst-case prompt offset. */
3
+ export declare const HEAD_WINDOW_BYTES: number;
4
+ /** Head frame ceiling — a cost bound independent of how the bytes compress. */
5
+ export declare const HEAD_MAX_FRAMES = 128;
6
+ /** Tail window budget. Wider than the head: trailing frames carry payloads. */
7
+ export declare const TAIL_WINDOW_BYTES: number;
8
+ /**
9
+ * Read both windows of one session log.
10
+ *
11
+ * @param path - Absolute artifact path.
12
+ * @param cwd - Working directory, for the last-resort title.
13
+ * @returns The digest. An unreadable log still yields a usable record: the
14
+ * title falls back to the directory basename and says so through its source.
15
+ */
16
+ export declare function digestSession(path: string, cwd: string): SessionDigest;
17
+ /**
18
+ * The last exchanges of a session, for the browser's preview pane.
19
+ *
20
+ * Bounded like everything else here: the preview shows the end of the
21
+ * conversation because that is what the tail window holds, and because the end
22
+ * is what tells you whether this is the session you meant.
23
+ *
24
+ * @param path - Absolute artifact path.
25
+ * @param limit - How many entries to keep, newest last.
26
+ * @returns Entries in log order.
27
+ */
28
+ export declare function previewSession(path: string, limit: number): PreviewEntry[];
29
+ //# sourceMappingURL=digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/sessions/digest.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAgB,MAAM,YAAY,CAAA;AAE3E,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,QAAY,CAAA;AAC1C,+EAA+E;AAC/E,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB,QAAa,CAAA;AA+F3C;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CA8CtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,EAAE,CAyB1E"}
@@ -0,0 +1,224 @@
1
+ /**
2
+ * What a bounded read can learn about a session log.
3
+ *
4
+ * Two windows, one at each end, and never anything in between:
5
+ *
6
+ * - The HEAD holds the session envelope, the boot policy events, and the
7
+ * opening prompt. Measured across a real corpus, the first user prompt lands
8
+ * within 8,107 bytes of the start (524 in its `agent/inbox/spliced` form),
9
+ * so a 64 KB window carries an eightfold margin.
10
+ * - The TAIL holds whatever was appended most recently: the current title
11
+ * (titles are re-emitted, and the last one wins), the model of the last
12
+ * request, and the last exchanges for the preview.
13
+ *
14
+ * Titles carry their own provenance, so this module does not have to guess.
15
+ * A title written by a provider records `source.kind: 'provider'`; the TUI's
16
+ * own rename paths append `{ title }` with no source at all. That difference
17
+ * is the evidence behind {@link SessionTitle.source}, which is why the picker
18
+ * can dim a fallback and explain a name instead of merely displaying one.
19
+ *
20
+ * @module @deepseek-harness-tui/dsh-tui/sessions/digest
21
+ */
22
+ import { basename } from 'node:path';
23
+ import { decodeFrames, decodeTail, readWindow, walkFrames } from './frames.js';
24
+ /** Head window budget. Eight times the measured worst-case prompt offset. */
25
+ export const HEAD_WINDOW_BYTES = 64 * 1024;
26
+ /** Head frame ceiling — a cost bound independent of how the bytes compress. */
27
+ export const HEAD_MAX_FRAMES = 128;
28
+ /** Tail window budget. Wider than the head: trailing frames carry payloads. */
29
+ export const TAIL_WINDOW_BYTES = 128 * 1024;
30
+ /** Longest preview excerpt kept per message, in characters. */
31
+ const PREVIEW_CHARS = 400;
32
+ /** The first text block of a message `content` payload. */
33
+ function textOfContent(content) {
34
+ if (typeof content === 'string')
35
+ return content.trim() || undefined;
36
+ if (!Array.isArray(content))
37
+ return undefined;
38
+ for (const block of content) {
39
+ if (block === null || typeof block !== 'object')
40
+ continue;
41
+ const record = block;
42
+ if (record['type'] !== 'text')
43
+ continue;
44
+ const value = record['text'];
45
+ if (typeof value === 'string' && value.trim().length > 0)
46
+ return value.trim();
47
+ }
48
+ return undefined;
49
+ }
50
+ /**
51
+ * Whether a message's `source` marks it as typed by the person at the
52
+ * keyboard. Plugin injections, instruction snapshots, skill catalogues and
53
+ * sub-agent reports all arrive as user-role messages too, and counting them
54
+ * would report a conversation where none happened.
55
+ */
56
+ function isHumanSource(source) {
57
+ if (source === undefined || source === null)
58
+ return true;
59
+ if (typeof source !== 'object')
60
+ return false;
61
+ return source['kind'] === 'user';
62
+ }
63
+ /** The human prompt carried by one log line, in either of its two forms. */
64
+ function humanPrompt(line) {
65
+ const data = line['data'];
66
+ if (data === null || typeof data !== 'object')
67
+ return undefined;
68
+ const record = data;
69
+ if (line['type'] === 'user/message') {
70
+ return isHumanSource(record['source']) ? textOfContent(record['content']) : undefined;
71
+ }
72
+ // The inbox splice precedes the durable user/message and reaches the log
73
+ // several frames earlier, which is what keeps the head window small.
74
+ if (line['type'] === 'agent/inbox/spliced') {
75
+ const inserted = record['inserted'];
76
+ if (!Array.isArray(inserted))
77
+ return undefined;
78
+ for (const message of inserted) {
79
+ if (message === null || typeof message !== 'object')
80
+ continue;
81
+ const entry = message;
82
+ if (entry['role'] !== 'user' || !isHumanSource(entry['source']))
83
+ continue;
84
+ const text = textOfContent(entry['content']);
85
+ if (text !== undefined)
86
+ return text;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ /** A `session/title` payload, with the provenance that classifies it. */
92
+ function titleOf(line) {
93
+ if (line['type'] !== 'session/title')
94
+ return undefined;
95
+ const data = line['data'];
96
+ if (data === null || typeof data !== 'object')
97
+ return undefined;
98
+ const record = data;
99
+ const text = record['title'];
100
+ if (typeof text !== 'string' || text.trim().length === 0)
101
+ return undefined;
102
+ const source = record['source'];
103
+ const byProvider = source !== null &&
104
+ typeof source === 'object' &&
105
+ source['kind'] === 'provider';
106
+ return { text: text.trim(), source: byProvider ? 'auto' : 'renamed' };
107
+ }
108
+ /** The route recorded by a `request/context` event. */
109
+ function modelOf(line) {
110
+ if (line['type'] !== 'request/context')
111
+ return undefined;
112
+ const data = line['data'];
113
+ if (data === null || typeof data !== 'object')
114
+ return undefined;
115
+ const model = data['model'];
116
+ return typeof model === 'string' && model.length > 0 ? model : undefined;
117
+ }
118
+ /** The label a delegated run was started under. */
119
+ function labelOf(line) {
120
+ if (line['type'] !== 'subagent/descriptor')
121
+ return undefined;
122
+ const data = line['data'];
123
+ if (data === null || typeof data !== 'object')
124
+ return undefined;
125
+ const label = data['label'];
126
+ return typeof label === 'string' && label.trim().length > 0 ? label.trim() : undefined;
127
+ }
128
+ /** Epoch-ms of a log line, when it carries one. */
129
+ function timeOf(line) {
130
+ const time = line['time'];
131
+ return typeof time === 'number' && Number.isFinite(time) ? time : undefined;
132
+ }
133
+ /**
134
+ * Read both windows of one session log.
135
+ *
136
+ * @param path - Absolute artifact path.
137
+ * @param cwd - Working directory, for the last-resort title.
138
+ * @returns The digest. An unreadable log still yields a usable record: the
139
+ * title falls back to the directory basename and says so through its source.
140
+ */
141
+ export function digestSession(path, cwd) {
142
+ const head = readWindow(path, HEAD_WINDOW_BYTES);
143
+ if (head === undefined) {
144
+ return { title: undefined, hasPrompt: false, model: undefined, label: undefined };
145
+ }
146
+ const headLines = decodeFrames(head.buffer, walkFrames(head.buffer, 0, HEAD_MAX_FRAMES));
147
+ let prompt;
148
+ let headTitle;
149
+ let label;
150
+ for (const line of headLines) {
151
+ prompt ??= humanPrompt(line);
152
+ headTitle ??= titleOf(line);
153
+ label ??= labelOf(line);
154
+ }
155
+ // Absence of a prompt only means "empty" when the window actually saw the
156
+ // whole log. A log too large for the window has a conversation in it by
157
+ // construction, and erring toward listing it is the safe direction: hiding
158
+ // a real session is a defect, showing a boot artifact is a nuisance.
159
+ const hasPrompt = prompt !== undefined || !head.whole;
160
+ // A head window that already covered the whole log IS the tail.
161
+ const tail = head.whole ? undefined : readWindow(path, TAIL_WINDOW_BYTES, true);
162
+ const tailLines = tail === undefined ? headLines : decodeTail(tail);
163
+ let tailTitle;
164
+ let model;
165
+ for (const line of tailLines) {
166
+ const title = titleOf(line);
167
+ if (title !== undefined)
168
+ tailTitle = title;
169
+ const route = modelOf(line);
170
+ if (route !== undefined)
171
+ model = route;
172
+ }
173
+ const resolved = tailTitle ??
174
+ headTitle ??
175
+ (prompt === undefined ? undefined : { text: prompt, source: 'prompt' });
176
+ return {
177
+ title: resolved ?? { text: basename(cwd), source: 'fallback' },
178
+ hasPrompt,
179
+ model,
180
+ label,
181
+ };
182
+ }
183
+ /**
184
+ * The last exchanges of a session, for the browser's preview pane.
185
+ *
186
+ * Bounded like everything else here: the preview shows the end of the
187
+ * conversation because that is what the tail window holds, and because the end
188
+ * is what tells you whether this is the session you meant.
189
+ *
190
+ * @param path - Absolute artifact path.
191
+ * @param limit - How many entries to keep, newest last.
192
+ * @returns Entries in log order.
193
+ */
194
+ export function previewSession(path, limit) {
195
+ const window = readWindow(path, TAIL_WINDOW_BYTES, true);
196
+ if (window === undefined)
197
+ return [];
198
+ const lines = decodeTail(window);
199
+ const entries = [];
200
+ for (const line of lines) {
201
+ const data = line['data'];
202
+ if (data === null || typeof data !== 'object')
203
+ continue;
204
+ const record = data;
205
+ if (line['type'] === 'user/message') {
206
+ if (!isHumanSource(record['source']))
207
+ continue;
208
+ const text = textOfContent(record['content']);
209
+ if (text !== undefined)
210
+ entries.push({ role: 'user', text: text.slice(0, PREVIEW_CHARS), at: timeOf(line) });
211
+ continue;
212
+ }
213
+ if (line['type'] === 'assistant/message') {
214
+ const message = record['message'];
215
+ if (message === null || typeof message !== 'object')
216
+ continue;
217
+ const text = textOfContent(message['content']);
218
+ if (text !== undefined) {
219
+ entries.push({ role: 'assistant', text: text.slice(0, PREVIEW_CHARS), at: timeOf(line) });
220
+ }
221
+ }
222
+ }
223
+ return entries.slice(-limit);
224
+ }
@@ -0,0 +1,100 @@
1
+ /** Byte range of one structurally complete frame; `end` is exclusive. */
2
+ export interface FrameRange {
3
+ readonly start: number;
4
+ readonly end: number;
5
+ }
6
+ /**
7
+ * Locate the end of the frame starting at `start`, without decompressing it.
8
+ *
9
+ * The walk reads the Frame_Header (descriptor, optional window/dictionary/
10
+ * content-size fields) and then each Block_Header in turn — a 3-byte
11
+ * little-endian word carrying `last_block` (1 bit), `block_type` (2 bits) and
12
+ * `block_size` (21 bits) — until the block marked last. A `Reserved` block
13
+ * type means these bytes are not a frame at all, which is how a coincidental
14
+ * magic gets rejected.
15
+ *
16
+ * @param buffer - Bytes available to the reader (may end mid-frame).
17
+ * @param start - Offset of the candidate frame's magic.
18
+ * @returns The frame's exclusive end offset, or -1 when the bytes at `start`
19
+ * are not a structurally complete frame within `buffer`.
20
+ */
21
+ export declare function frameEnd(buffer: Buffer, start: number): number;
22
+ /**
23
+ * Walk complete frames forward from `from`.
24
+ *
25
+ * @param buffer - Bytes to walk.
26
+ * @param from - Offset to start at (must be a frame boundary).
27
+ * @param maxFrames - Stop after this many frames; the reader's cost ceiling.
28
+ * @returns Complete frames in file order. A window that ends mid-frame simply
29
+ * yields one fewer frame — the partial tail is never reported as complete.
30
+ */
31
+ export declare function walkFrames(buffer: Buffer, from?: number, maxFrames?: number): FrameRange[];
32
+ /**
33
+ * Re-synchronize on a frame boundary inside a window that starts mid-frame.
34
+ *
35
+ * A tail window has no boundary to start from, so the only anchor is the one
36
+ * structural fact we know about the whole file: its last frame ends exactly at
37
+ * EOF. Every magic candidate is tried in file order, and the first one whose
38
+ * frame chain lands precisely on the window's end is the true boundary — a
39
+ * coincidental magic would have to spell a valid block chain of exactly the
40
+ * right total length to be mistaken for one.
41
+ *
42
+ * @param buffer - A window whose last byte is the file's last byte.
43
+ * @returns Frames from the earliest recoverable boundary, or [] when the
44
+ * window holds no complete frame.
45
+ */
46
+ export declare function resyncFrames(buffer: Buffer): FrameRange[];
47
+ /** One decoded log line, still untyped — the caller owns interpretation. */
48
+ export type LogLine = Record<string, unknown>;
49
+ /**
50
+ * Decode frames to JSON log lines, tolerantly.
51
+ *
52
+ * A frame that fails to decompress or a line that fails to parse is skipped
53
+ * rather than thrown: a log being appended to right now can hold a frame
54
+ * flushed without its final checksum, and a torn tail is the backend's own
55
+ * documented recovery case. A picker label is read-only UI state — degrading
56
+ * to a fallback title beats refusing to list the session.
57
+ *
58
+ * @param buffer - Bytes the frames index into.
59
+ * @param frames - Complete frame ranges within `buffer`.
60
+ * @returns Parsed envelopes in log order.
61
+ */
62
+ export declare function decodeFrames(buffer: Buffer, frames: readonly FrameRange[]): LogLine[];
63
+ /** A file's size and last-write time, read once for both. */
64
+ export interface FileFacts {
65
+ readonly bytes: number;
66
+ readonly modifiedAt: number;
67
+ }
68
+ /**
69
+ * Size and mtime of a log, or undefined when it is gone.
70
+ * @param path - Absolute artifact path.
71
+ */
72
+ export declare function fileFacts(path: string): FileFacts | undefined;
73
+ /**
74
+ * Read a window from one end of a file without loading the whole thing.
75
+ *
76
+ * @param path - Absolute artifact path.
77
+ * @param bytes - Window budget; the whole file is read when it is smaller.
78
+ * @param end - Read the last `bytes` instead of the first.
79
+ * @returns The window, plus whether it covers the entire file (which tells a
80
+ * head reader that its last frame cannot be truncated).
81
+ */
82
+ export declare function readWindow(path: string, bytes: number, end?: boolean): {
83
+ buffer: Buffer;
84
+ whole: boolean;
85
+ } | undefined;
86
+ /**
87
+ * Decode a window read from the END of a file.
88
+ *
89
+ * A tail window has no frame boundary to start from unless it happens to
90
+ * cover the whole file, so it re-synchronizes; a whole-file window is simply
91
+ * walked.
92
+ *
93
+ * @param window - A window whose last byte is the file's last byte.
94
+ * @returns Log lines from the trailing frames, oldest first.
95
+ */
96
+ export declare function decodeTail(window: {
97
+ buffer: Buffer;
98
+ whole: boolean;
99
+ }): LogLine[];
100
+ //# sourceMappingURL=frames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/sessions/frames.ts"],"names":[],"mappings":"AAuCA,yEAAyE;AACzE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAsC9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,SAAI,EACR,SAAS,SAA2B,GACnC,UAAU,EAAE,CAUd;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAQzD;AAED,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE7C;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,EAAE,CAsBrF;AAED,6DAA6D;AAC7D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAO7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,UAAQ,GACV;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAwBhD;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,EAAE,CAKhF"}