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

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 (119) hide show
  1. package/README.md +3 -3
  2. package/bin/dsh-tui.js +24 -3
  3. package/cordis.patch.yml +24 -11
  4. package/cordis.yml +9 -1
  5. package/lib/types/commands.d.ts.map +1 -1
  6. package/lib/types/commands.js +2 -1
  7. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  8. package/lib/types/components/GoalTodoPanel.js +6 -1
  9. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  10. package/lib/types/components/HistorySearchDialog.js +5 -1
  11. package/lib/types/components/MessageList.d.ts +3 -1
  12. package/lib/types/components/MessageList.d.ts.map +1 -1
  13. package/lib/types/components/MessageList.js +9 -5
  14. package/lib/types/components/PluginSceneBoundary.d.ts +31 -0
  15. package/lib/types/components/PluginSceneBoundary.d.ts.map +1 -0
  16. package/lib/types/components/PluginSceneBoundary.js +29 -0
  17. package/lib/types/components/PromptInput.d.ts.map +1 -1
  18. package/lib/types/components/PromptInput.js +52 -74
  19. package/lib/types/components/SearchBox.d.ts +4 -0
  20. package/lib/types/components/SearchBox.d.ts.map +1 -1
  21. package/lib/types/components/SearchBox.js +101 -2
  22. package/lib/types/components/SkillsPicker.d.ts +18 -0
  23. package/lib/types/components/SkillsPicker.d.ts.map +1 -0
  24. package/lib/types/components/SkillsPicker.js +50 -0
  25. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
  26. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
  27. package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
  28. package/lib/types/components/SplitDiffView.d.ts +28 -0
  29. package/lib/types/components/SplitDiffView.d.ts.map +1 -0
  30. package/lib/types/components/SplitDiffView.js +417 -0
  31. package/lib/types/components/WorkingSpinner.d.ts +3 -1
  32. package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
  33. package/lib/types/components/WorkingSpinner.js +2 -2
  34. package/lib/types/components/design-system/ThemedText.d.ts +7 -1
  35. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  36. package/lib/types/components/design-system/ThemedText.js +2 -2
  37. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +3 -1
  38. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  39. package/lib/types/components/messages/AssistantToolUseMessage.js +17 -7
  40. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  41. package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
  42. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  43. package/lib/types/components/questions/PlanReviewPanel.js +12 -3
  44. package/lib/types/dsh-adapter/channel.d.ts +78 -0
  45. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  46. package/lib/types/dsh-adapter/channel.js +165 -14
  47. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  48. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  49. package/lib/types/dsh-adapter/contract.js +1 -0
  50. package/lib/types/dsh-adapter/index.d.ts +4 -0
  51. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  52. package/lib/types/dsh-adapter/index.js +1 -0
  53. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -1
  54. package/lib/types/dsh-adapter/packaged-presets.js +5 -2
  55. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  56. package/lib/types/dsh-adapter/plugin.js +66 -3
  57. package/lib/types/dsh-adapter/promptDebug.d.ts +10 -0
  58. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -0
  59. package/lib/types/dsh-adapter/promptDebug.js +148 -0
  60. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  61. package/lib/types/dsh-adapter/providerWizard.js +3 -1
  62. package/lib/types/dsh-adapter/scenes.d.ts +75 -0
  63. package/lib/types/dsh-adapter/scenes.d.ts.map +1 -0
  64. package/lib/types/dsh-adapter/scenes.js +78 -0
  65. package/lib/types/dsh-adapter/settings-sections.d.ts +116 -0
  66. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -0
  67. package/lib/types/dsh-adapter/settings-sections.js +63 -0
  68. package/lib/types/dsh-adapter/settingsEditor.d.ts +128 -0
  69. package/lib/types/dsh-adapter/settingsEditor.d.ts.map +1 -0
  70. package/lib/types/dsh-adapter/settingsEditor.js +244 -0
  71. package/lib/types/i18n.d.ts +169 -18
  72. package/lib/types/i18n.d.ts.map +1 -1
  73. package/lib/types/i18n.js +45 -6
  74. package/lib/types/ink/components/Text.d.ts +9 -1
  75. package/lib/types/ink/components/Text.d.ts.map +1 -1
  76. package/lib/types/ink/components/Text.js +7 -6
  77. package/lib/types/ink/ink.d.ts.map +1 -1
  78. package/lib/types/ink/ink.js +4 -2
  79. package/lib/types/ink/log-update.d.ts.map +1 -1
  80. package/lib/types/ink/log-update.js +76 -9
  81. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  82. package/lib/types/ink/parse-keypress.js +4 -1
  83. package/lib/types/ink/terminal.d.ts +40 -4
  84. package/lib/types/ink/terminal.d.ts.map +1 -1
  85. package/lib/types/ink/terminal.js +57 -5
  86. package/lib/types/ink/termio/tokenize.d.ts +6 -0
  87. package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
  88. package/lib/types/ink/termio/tokenize.js +18 -4
  89. package/lib/types/jsx-runtime.d.ts +2 -0
  90. package/lib/types/jsx-runtime.d.ts.map +1 -0
  91. package/lib/types/jsx-runtime.js +13 -0
  92. package/lib/types/modelRoute.d.ts +10 -2
  93. package/lib/types/modelRoute.d.ts.map +1 -1
  94. package/lib/types/modelRoute.js +33 -4
  95. package/lib/types/scenes.d.ts +3 -0
  96. package/lib/types/scenes.d.ts.map +1 -0
  97. package/lib/types/scenes.js +4 -0
  98. package/lib/types/screens/Chat.d.ts.map +1 -1
  99. package/lib/types/screens/Chat.js +182 -26
  100. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  101. package/lib/types/screens/SessionBrowser.js +30 -15
  102. package/lib/types/screens/Settings.d.ts +20 -0
  103. package/lib/types/screens/Settings.d.ts.map +1 -0
  104. package/lib/types/screens/Settings.js +333 -0
  105. package/lib/types/screens/StatusLine.js +1 -1
  106. package/lib/types/sessions/view.d.ts.map +1 -1
  107. package/lib/types/sessions/view.js +13 -1
  108. package/lib/types/settings-sections.d.ts +3 -0
  109. package/lib/types/settings-sections.d.ts.map +1 -0
  110. package/lib/types/settings-sections.js +4 -0
  111. package/lib/types/theme.d.ts +12 -0
  112. package/lib/types/theme.d.ts.map +1 -1
  113. package/lib/types/theme.js +36 -0
  114. package/lib/types/update.d.ts +21 -7
  115. package/lib/types/update.d.ts.map +1 -1
  116. package/lib/types/update.js +66 -16
  117. package/lib/types/utils/paths.d.ts.map +1 -1
  118. package/lib/types/utils/paths.js +4 -1
  119. package/package.json +49 -9
@@ -300,6 +300,17 @@ export function createChannel(ctx, initialAgent, options) {
300
300
  // the degraded-boot warning for profile launches.
301
301
  const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
302
302
  const commandTrees = ctx.get('tuiCommandTrees');
303
+ // The `/settings` screen reads its host on EVERY render, so the host must
304
+ // be a stable object: a fresh literal per call would re-fire the screen's
305
+ // host-keyed effects endlessly (render → new host → effect → state →
306
+ // render). The underlying services are fixed for the channel's lifetime,
307
+ // so compute once and cache.
308
+ let settingsHostCache;
309
+ let settingsHostResolved = false;
310
+ // Plugin scene runtime (optional service, same degradation rule as
311
+ // tuiWorkspaces/tuiCommandTrees): mounted by the bundle patch's
312
+ // dsh-tui-scenes row; absent the row, `pluginScene` simply stays undefined.
313
+ const sceneRuntime = ctx.get('tuiScenes');
303
314
  // Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
304
315
  // atom-less → the built-in default/plan/full cycle (sessionModes.ts).
305
316
  const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
@@ -641,6 +652,7 @@ export function createChannel(ctx, initialAgent, options) {
641
652
  modeIndex: 0,
642
653
  workingActivity: undefined,
643
654
  activityFrames: options.activityFrames,
655
+ diffLayout: options.diffLayout ?? 'auto',
644
656
  activityEnabled: options.activity !== false,
645
657
  contextBarEnabled: options.contextBar !== false,
646
658
  agentPreset: options.agentPreset,
@@ -1452,6 +1464,12 @@ export function createChannel(ctx, initialAgent, options) {
1452
1464
  }
1453
1465
  }, item.timeoutMs);
1454
1466
  },
1467
+ setDiffLayout(layout) {
1468
+ if (layout === state.diffLayout)
1469
+ return;
1470
+ state.diffLayout = layout;
1471
+ state.emit();
1472
+ },
1455
1473
  setActivityFrames(name) {
1456
1474
  if (!isPresetName(name)) {
1457
1475
  state.notify(t('unknown-activity-preset', { name }), { color: 'error' });
@@ -1561,6 +1579,84 @@ export function createChannel(ctx, initialAgent, options) {
1561
1579
  return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
1562
1580
  .then(lists => lists.flat());
1563
1581
  },
1582
+ async listSkills() {
1583
+ // snapshot() over list(): only a COMPLETE observation is authoritative
1584
+ // (same contract as the skill-command merge above) — a partial catalog
1585
+ // must surface as "failed", not as a misleading near-empty picker.
1586
+ const registry = skillRegistryFor(agent);
1587
+ if (registry === undefined)
1588
+ return [];
1589
+ try {
1590
+ const observation = await registry.snapshot(skillViewOptions(agent));
1591
+ if (!observation.complete)
1592
+ return undefined;
1593
+ return observation.skills.map(skill => ({
1594
+ name: skill.name,
1595
+ description: skill.description,
1596
+ userInvocable: isUserInvocable(skill),
1597
+ source: skill.source,
1598
+ }));
1599
+ }
1600
+ catch {
1601
+ return undefined;
1602
+ }
1603
+ },
1604
+ async describeCredential(ref) {
1605
+ const credentials = ctx.get('credentials');
1606
+ if (!credentials)
1607
+ return undefined;
1608
+ return credentials.describe(ref);
1609
+ },
1610
+ settingsHost() {
1611
+ if (settingsHostResolved)
1612
+ return settingsHostCache;
1613
+ settingsHostResolved = true;
1614
+ // The `/settings` screen's runtime surface, over the same dsh-base
1615
+ // seams the `/provider` wizard uses: settings (namespace descriptors +
1616
+ // revision-fenced mutate) and credentials (secret writes). Structurally
1617
+ // typed like the other optional seams in this file.
1618
+ const settings = ctx.get('settings');
1619
+ const credentials = ctx.get('credentials');
1620
+ if (!settings)
1621
+ return undefined;
1622
+ settingsHostCache = {
1623
+ listNamespaces() {
1624
+ // redactSecrets: the screen never renders a secret literal — secret
1625
+ // fields are write-only controls over the credentials seam.
1626
+ return settings.describe({ redactSecrets: true }).map(descriptor => ({
1627
+ ns: descriptor.ns,
1628
+ revision: descriptor.revision,
1629
+ applies: descriptor.applies,
1630
+ value: descriptor.value,
1631
+ user: descriptor.user,
1632
+ }));
1633
+ },
1634
+ write(ns, ops, expectedRevision) {
1635
+ return settings.mutate(ns, ops, expectedRevision);
1636
+ },
1637
+ async credentialConfigured(ref) {
1638
+ // The environment shadows the store (providerSetup.envShadows), so
1639
+ // an env-provided key counts as configured.
1640
+ if (process.env[ref] !== undefined)
1641
+ return true;
1642
+ return credentials !== undefined && (await credentials.resolve(ref)) !== undefined;
1643
+ },
1644
+ async writeCredential(ref, value) {
1645
+ if (!credentials)
1646
+ throw new Error('credentials service unavailable');
1647
+ await credentials.set(ref, value);
1648
+ },
1649
+ };
1650
+ return settingsHostCache;
1651
+ },
1652
+ settingsSections() {
1653
+ const sections = ctx.get('tuiSettingsSections');
1654
+ return sections?.list() ?? [];
1655
+ },
1656
+ subscribeSettingsSections(listener) {
1657
+ const sections = ctx.get('tuiSettingsSections');
1658
+ return sections?.subscribe(listener) ?? (() => { });
1659
+ },
1564
1660
  providerSetup() {
1565
1661
  // The `/provider` wizard's runtime surface, over the dsh-base seams:
1566
1662
  // settings (profile persistence), credentials (key storage) and the
@@ -1757,6 +1853,13 @@ export function createChannel(ctx, initialAgent, options) {
1757
1853
  runExternalCommand(name, rawInput) {
1758
1854
  return executeRegistryCommand(name, rawInput);
1759
1855
  },
1856
+ pluginScene: sceneRuntime?.active,
1857
+ openPluginScene(id) {
1858
+ return sceneRuntime?.open(id) ?? false;
1859
+ },
1860
+ closePluginScene() {
1861
+ sceneRuntime?.close();
1862
+ },
1760
1863
  pushLocal(title, lines) {
1761
1864
  state.rows.push({ id: nextRowId++, kind: 'local', text: title });
1762
1865
  for (const line of lines) {
@@ -1937,6 +2040,7 @@ export function createChannel(ctx, initialAgent, options) {
1937
2040
  },
1938
2041
  releaseContributions() {
1939
2042
  releaseSkillCommands();
2043
+ unsubscribeScenes?.();
1940
2044
  },
1941
2045
  traceEvents() {
1942
2046
  // Immutable per-append snapshot (dsh-session caches the frozen array);
@@ -2232,25 +2336,38 @@ export function createChannel(ctx, initialAgent, options) {
2232
2336
  const dispose = commandService.register({
2233
2337
  name,
2234
2338
  description,
2235
- // The injected body is the payload; recording the (empty) raw input
2236
- // would only duplicate the command name into the session log.
2339
+ // The invocation line is re-submitted as a user message (kernel
2340
+ // path) or replaced by the injected body (fallback) — recording
2341
+ // the raw input here too would duplicate it in the session log.
2237
2342
  recordInput: false,
2238
- handler: async ({ agent: invoker, signal }) => {
2343
+ handler: async ({ agent: invoker, rawInput, signal }) => {
2344
+ // Kernel gesture path: the `skill` tool and dsh-tool-skill's
2345
+ // pre-step boundary mount together, so a visible `skill` tool
2346
+ // means the boundary scans this agent's user messages for the
2347
+ // `/name` gesture and injects the rendered body host-side —
2348
+ // the same architecture as the web client's ui-skill. Routing
2349
+ // through it keeps the user's args in the transcript message
2350
+ // (rawInput rides along instead of being swallowed by the
2351
+ // command layer) and matches the kernel's own adjudication.
2352
+ const tools = ctx.get('tools');
2353
+ if (tools?.get('skill', invoker) !== undefined) {
2354
+ deliverUserText(`/${name}${rawInput}`, 'followup');
2355
+ // Silent success: the submitted message is the feedback.
2356
+ return { kind: 'success' };
2357
+ }
2358
+ // Fallback for compositions without dsh-tool-skill (e.g. the
2359
+ // minimal preset): inject the rendered body directly, in the
2360
+ // official user-explicit invocation shape (dsh-skill's
2361
+ // SkillInvocationSource).
2239
2362
  const view = { ...skillViewOptions(invoker), signal };
2240
2363
  const skill = await skillRegistryFor(invoker)?.get(name, view);
2241
2364
  if (skill === undefined || !isUserInvocable(skill)) {
2242
2365
  return { kind: 'error', text: t('skill-unavailable', { name }) };
2243
2366
  }
2244
- // The official user-explicit invocation shape (dsh-skill's
2245
- // SkillInvocationSource): the rendered body rides as instructions
2246
- // the model follows, and transcript consumers present it from the
2247
- // source metadata instead of re-parsing model-facing text.
2248
2367
  invoker.followup(createUserMessage({
2249
2368
  content: [{ type: 'text', text: renderSkillContent(skill) }],
2250
2369
  source: { kind: 'skill-invocation', name, form: 'instructions' },
2251
2370
  }));
2252
- // Silent success: the agent visibly starts working on the skill,
2253
- // which is the feedback (CC shows no banner either).
2254
2371
  return { kind: 'success' };
2255
2372
  },
2256
2373
  });
@@ -2265,6 +2382,17 @@ export function createChannel(ctx, initialAgent, options) {
2265
2382
  ctx.on('skills/change', () => {
2266
2383
  void refreshSkillCommands();
2267
2384
  });
2385
+ /**
2386
+ * Mirror the scene runtime's active scene into channel state, so screens
2387
+ * swap to it through the ordinary version-bump re-render instead of a
2388
+ * second subscription channel.
2389
+ */
2390
+ const unsubscribeScenes = sceneRuntime?.subscribe(() => {
2391
+ if (state.pluginScene === sceneRuntime.active)
2392
+ return;
2393
+ state.pluginScene = sceneRuntime.active;
2394
+ state.emit();
2395
+ });
2268
2396
  /** See {@link Channel.releaseContributions}. */
2269
2397
  const releaseSkillCommands = () => {
2270
2398
  if (skillCommandsRetry !== undefined)
@@ -2595,12 +2723,18 @@ ${output}
2595
2723
  break;
2596
2724
  }
2597
2725
  case 'assistant/message': {
2598
- const row = ensureStreaming(event.seq);
2599
- row.time = event.time;
2600
2726
  const text = textOf(event.data.message.content);
2601
- if (text)
2602
- row.text = text;
2603
- row.streaming = false;
2727
+ // Reasoning/tool-only steps emit no text: creating an assistant row
2728
+ // anyway leaves an empty `●` bullet in the transcript. A pre-existing
2729
+ // streaming row always has text (ensureStreaming is only reached on
2730
+ // non-empty text deltas), so only create one when text arrives.
2731
+ const row = streaming ?? (text ? ensureStreaming(event.seq) : undefined);
2732
+ if (row !== undefined) {
2733
+ row.time = event.time;
2734
+ if (text)
2735
+ row.text = text;
2736
+ row.streaming = false;
2737
+ }
2604
2738
  streaming = undefined;
2605
2739
  if (reasoning !== undefined) {
2606
2740
  // Seal, don't fold: the per-step duration settles here, but the
@@ -2997,6 +3131,23 @@ ${output}
2997
3131
  }),
2998
3132
  ];
2999
3133
  };
3134
+ // Subagents inherit provider/model from AgentOptions, but resumed TUI
3135
+ // agents can legitimately carry their route only in persisted request
3136
+ // headers. Their child scopes do not share this channel's per-agent
3137
+ // ModelSelectionRef, so fill an otherwise incomplete first request from
3138
+ // the active route. Keep complete child-specific routes authoritative.
3139
+ ctx.on('agent/request', async (_payload, next) => {
3140
+ const resolved = await next();
3141
+ if (typeof resolved.provider === 'string' && resolved.provider.length > 0 &&
3142
+ typeof resolved.model === 'string' && resolved.model.length > 0) {
3143
+ return resolved;
3144
+ }
3145
+ return {
3146
+ ...resolved,
3147
+ provider: state.provider,
3148
+ model: state.model,
3149
+ };
3150
+ });
3000
3151
  bindAgent();
3001
3152
  // Cordis owns the Channel lifetime. Rebinding handles the common case;
3002
3153
  // this effect closes the final timer when the Channel's context unloads.
@@ -5,7 +5,7 @@ export declare const UPSTREAM_VALIDATED_VERSION = "0.1.0-rc.6";
5
5
  */
6
6
  export declare const UPSTREAM_FRAMEWORK_MAJORS: Record<string, number>;
7
7
  /** Official packages the adapter consumes at runtime or as types. */
8
- export declare const UPSTREAM_BLESSED_PACKAGES: readonly ["@deepseek-ai/cordis", "@deepseek-ai/schemastery", "@deepseek-ai/dsh-invariants", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-agent-presets", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-cordis-host-runner", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-persona", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-storage", "@deepseek-ai/dsh-storage-domain", "@deepseek-ai/dsh-storage-json", "@deepseek-ai/dsh-workspace", "@deepseek-ai/dsh-system-prompt", "@deepseek-ai/dsh-terminal", "@deepseek-ai/dsh-terminal-bash", "@deepseek-ai/dsh-tool-ask-user", "@deepseek-ai/dsh-tool-bash-persistent", "@deepseek-ai/dsh-tool-cordis", "@deepseek-ai/dsh-user-approval", "@deepseek-ai/dsh-user-questions"];
8
+ export declare const UPSTREAM_BLESSED_PACKAGES: readonly ["@deepseek-ai/cordis", "@deepseek-ai/schemastery", "@deepseek-ai/dsh-invariants", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-agent-presets", "@deepseek-ai/dsh-atomic-write", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-cordis-host-runner", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-persona", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-storage", "@deepseek-ai/dsh-storage-domain", "@deepseek-ai/dsh-storage-json", "@deepseek-ai/dsh-workspace", "@deepseek-ai/dsh-system-prompt", "@deepseek-ai/dsh-terminal", "@deepseek-ai/dsh-terminal-bash", "@deepseek-ai/dsh-tool-ask-user", "@deepseek-ai/dsh-tool-bash-persistent", "@deepseek-ai/dsh-tool-cordis", "@deepseek-ai/dsh-user-approval", "@deepseek-ai/dsh-user-questions"];
9
9
  export interface UpstreamDriftEntry {
10
10
  package: string;
11
11
  installed: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/contract.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,0BAA0B,eAAe,CAAA;AAEtD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAG5D,CAAA;AAED,qEAAqE;AACrE,eAAO,MAAM,yBAAyB,6vBAyB5B,CAAA;AAEV,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;CAClB;AAWD,wBAAgB,yBAAyB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAgB9E;AAOD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,kBAAkB,EAAE,CAsBpD"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/contract.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,0BAA0B,eAAe,CAAA;AAEtD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAG5D,CAAA;AAED,qEAAqE;AACrE,eAAO,MAAM,yBAAyB,8xBA0B5B,CAAA;AAEV,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;CAClB;AAWD,wBAAgB,yBAAyB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAgB9E;AAOD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,kBAAkB,EAAE,CAsBpD"}
@@ -28,6 +28,7 @@ export const UPSTREAM_BLESSED_PACKAGES = [
28
28
  '@deepseek-ai/dsh-agent',
29
29
  '@deepseek-ai/dsh-agent-instructions',
30
30
  '@deepseek-ai/dsh-agent-presets',
31
+ '@deepseek-ai/dsh-atomic-write',
31
32
  '@deepseek-ai/dsh-commands',
32
33
  '@deepseek-ai/dsh-cordis-host-runner',
33
34
  '@deepseek-ai/dsh-llm',
@@ -62,6 +62,10 @@ export interface Config {
62
62
  * persisted in `~/.dsh-tui/agent-preset.json` wins, then the roster
63
63
  * default (`standard`). */
64
64
  preset?: string;
65
+ /** Edit/Write diff presentation: `auto` picks side-by-side on wide
66
+ * terminals (≥110 cols) and unified below; `split`/`unified` force one
67
+ * layout. Editable live from the `/settings` screen. */
68
+ diffLayout?: 'auto' | 'split' | 'unified';
65
69
  /** Shift+Tab session-mode cycle (array order IS the cycle order; index 0
66
70
  * is the unmarked base mode). Each entry bundles any subset of the
67
71
  * `plan`/`sandbox`/`approval` atoms; absent → the built-in
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,eAAO,MAAM,IAAI,YAAY,CAAA;AAU7B,eAAO,MAAM,MAAM,UAAa,CAAA;AAEhC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;yEAKqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;gDAC4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;gFAE4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;sBAIkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;4BAGwB;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;wDAEoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;kFAC8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;gCAG4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;yDAGqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CA0BhC,CAAA;AAEF;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,eAAO,MAAM,IAAI,YAAY,CAAA;AAU7B,eAAO,MAAM,MAAM,UAAa,CAAA;AAEhC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;yEAKqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;gDAC4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;gFAE4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;sBAIkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;4BAGwB;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;wDAEoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;kFAC8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;gCAG4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;6DAEyD;IACzD,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACzC;;;yDAGqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CA2BhC,CAAA;AAEF;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
@@ -27,6 +27,7 @@ export const Config = Schema.object({
27
27
  fullscreen: Schema.boolean().default(false),
28
28
  lang: Schema.string().required(false),
29
29
  preset: Schema.string().required(false),
30
+ diffLayout: Schema.union(['auto', 'split', 'unified']).default('auto'),
30
31
  modes: Schema.array(Schema.object({
31
32
  id: Schema.string(),
32
33
  label: Schema.string().required(false),
@@ -1 +1 @@
1
- {"version":3,"file":"packaged-presets.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/packaged-presets.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAmBD,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,CAQ9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,GAAG,oBAAoB,EAAE,CAkDjG"}
1
+ {"version":3,"file":"packaged-presets.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/packaged-presets.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAmBD,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,CAQ9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,GAAG,oBAAoB,EAAE,CAqDjG"}
@@ -56,7 +56,10 @@ export function ensurePackagedPresets(options = {}) {
56
56
  const target = join(targetRoot, id);
57
57
  if (!existsSync(target)) {
58
58
  mkdirSync(targetRoot, { recursive: true });
59
- cpSync(source, target, { recursive: true, force: false, errorOnExist: true });
59
+ // `filter` forces the JS copy path (see src/utils/paths.ts): the native
60
+ // cpSync fast path fails with EIO or crashes under a non-ASCII home
61
+ // directory such as `C:\Users\米`.
62
+ cpSync(source, target, { recursive: true, force: false, errorOnExist: true, filter: () => true });
60
63
  results.push({ id, status: 'installed' });
61
64
  continue;
62
65
  }
@@ -72,7 +75,7 @@ export function ensurePackagedPresets(options = {}) {
72
75
  const suffix = `${process.pid}-${randomUUID()}`;
73
76
  const staged = join(targetRoot, `.${id}.staged-${suffix}`);
74
77
  const backup = join(targetRoot, `.${id}.backup-${suffix}`);
75
- cpSync(source, staged, { recursive: true, force: false, errorOnExist: true });
78
+ cpSync(source, staged, { recursive: true, force: false, errorOnExist: true, filter: () => true });
76
79
  try {
77
80
  renameSync(target, backup);
78
81
  renameSync(staged, target);
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,wBAAwB,CAAA;AAGhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AA6BnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2bvE;AA6GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5B,YAAY,EAAE,KAAK,CAAA;CACpB,GAAG,OAAO,CAQV"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,wBAAwB,CAAA;AAGhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AA8BnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkgBvE;AA6GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5B,YAAY,EAAE,KAAK,CAAA;CACpB,GAAG,OAAO,CAQV"}
@@ -3,12 +3,15 @@ import React from 'react';
3
3
  import { SessionId } from '@deepseek-ai/dsh-session';
4
4
  import UserQuestionService from '@deepseek-ai/dsh-user-questions';
5
5
  import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user';
6
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
7
+ import Schema from '@deepseek-ai/schemastery';
6
8
  import { createChannel } from './channel.js';
7
9
  import { createChildStderrReporter, installChildStderrGuard } from './childStderr.js';
8
10
  import { logForDebugging } from '../utils/debug.js';
9
11
  import { QuestionStore } from './questions.js';
10
12
  import { ApprovalStore } from './approvals.js';
11
13
  import { registerPackagedSkills } from './packaged-skills.js';
14
+ import { registerPromptDebug } from './promptDebug.js';
12
15
  import { readActivityFrames } from '../activityPrefs.js';
13
16
  import { readModelPref } from '../modelPrefs.js';
14
17
  import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
@@ -130,6 +133,9 @@ export async function apply(ctx, config) {
130
133
  // Packaged skills (/audit, /bug, …): contribute them through the host's
131
134
  // skill registry so they resolve with zero manual copying.
132
135
  registerPackagedSkills(ctx);
136
+ // `/debug-prompt` snapshots the final provider-neutral request at the
137
+ // llm/stream boundary, after every prompt and tool contributor has run.
138
+ registerPromptDebug(ctx);
133
139
  // Yield to an incumbent provider instead of crashing the whole plugin tree
134
140
  // (issue #98): the harness allows exactly ONE user-questions provider per
135
141
  // context, and stacking this TUI onto a profile that already carries
@@ -211,6 +217,14 @@ export async function apply(ctx, config) {
211
217
  '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
218
  }
213
219
  const workspaceService = mountedWorkspaceService ?? createLocalWorkspaceRuntime();
220
+ // Same skew guard for the plugin-scene registry (dsh-tui-scenes row): the
221
+ // channel degrades to never opening scenes when the service is absent, so
222
+ // say why on profile launches — a plugin's open() otherwise fails with only
223
+ // its own warn to go on.
224
+ if (ctx.get('tuiScenes') === undefined && resolveDshProfileName() !== undefined) {
225
+ ctx.logger.warn('dsh-tui: tuiScenes service is not mounted; plugin scenes will never open. ' +
226
+ 'The bundle patch is older than the installed dsh-tui package — update the globally installed dsh-tui launcher to match the profile (issue #183).');
227
+ }
214
228
  const initialWorkspace = requestedWorkspace === undefined
215
229
  ? undefined
216
230
  : await workspaceService.resolve(requestedWorkspace);
@@ -267,8 +281,53 @@ export async function apply(ctx, config) {
267
281
  // Shift+Tab session-mode cycle (undefined → the built-in default/
268
282
  // plan/full cycle in sessionModes.ts).
269
283
  modes: config.modes,
284
+ // Edit/Write diff presentation (schema default 'auto'); the /settings
285
+ // screen edits this key live through the dsh-tui namespace.
286
+ diffLayout: config.diffLayout,
270
287
  handle,
271
288
  });
289
+ // Register the dsh-tui settings namespace so the /settings screen can
290
+ // edit it (the section below was '命名空间未注册' without this): the
291
+ // user layer in settings.yaml wins over cordis.yml's diffLayout, and
292
+ // watch() lands commits on the live channel — no recompose needed.
293
+ ctx.inject(['settings'], (settingsCtx) => {
294
+ const scope = settingsCtx.settings.register(settingsNamespace('dsh-tui'), Schema.object({
295
+ diffLayout: Schema.union(['auto', 'split', 'unified']).default('auto'),
296
+ }));
297
+ const applyLayout = (value) => {
298
+ channel.setDiffLayout(value.diffLayout ?? config.diffLayout ?? 'auto');
299
+ };
300
+ applyLayout(scope.get());
301
+ scope.watch(next => {
302
+ applyLayout(next);
303
+ });
304
+ });
305
+ // The /settings screen's own section: the dsh-tui namespace comes from
306
+ // this plugin's Config schema, and the declared select writes diffLayout
307
+ // back through the settings service's revision-fenced mutate.
308
+ const settingsSections = ctx.get('tuiSettingsSections');
309
+ if (settingsSections !== undefined) {
310
+ const unregister = settingsSections.register({
311
+ ns: 'dsh-tui',
312
+ title: 'dsh-tui',
313
+ fields: [
314
+ {
315
+ path: ['diffLayout'],
316
+ label: 'Diff layout',
317
+ descriptions: { zh: 'diff 布局' },
318
+ hint: 'Edit/Write tool cards: auto picks by terminal width, or force one layout.',
319
+ hintDescriptions: { zh: 'Edit/Write 工具卡的 diff 呈现:auto 按终端宽度选择,或强制一种布局。' },
320
+ kind: 'select',
321
+ options: [
322
+ { value: 'auto', label: 'Auto (by width)', descriptions: { zh: '自动(按宽度)' } },
323
+ { value: 'split', label: 'Side-by-side', descriptions: { zh: '双栏对照' } },
324
+ { value: 'unified', label: 'Unified', descriptions: { zh: '统一式' } },
325
+ ],
326
+ },
327
+ ],
328
+ });
329
+ ctx.effect(() => unregister);
330
+ }
272
331
  // DSH approval seam: the permission layer asks ApprovalService.request(),
273
332
  // which dispatches an `approval/request` waterfall. With no answerer the
274
333
  // chain falls through to the fail-closed 'unavailable', so register this
@@ -309,6 +368,7 @@ export async function apply(ctx, config) {
309
368
  let instance;
310
369
  let exited = false;
311
370
  let updateRequested = false;
371
+ let updateTargetVersion;
312
372
  // The profile this process was booted with (`dsh --profile <name>`); dsh
313
373
  // exposes it nowhere else, and /update must update the installation the
314
374
  // user is actually running, not a hard-coded one.
@@ -336,7 +396,7 @@ export async function apply(ctx, config) {
336
396
  catch {
337
397
  // Resume persistence is best effort and must never block an update.
338
398
  }
339
- void finishExit(ctx, instance, config.fullscreen === true, 'Updating @deepseek-harness-tui/dsh-tui and restarting…', undefined, () => runUpdate(ctx, profile, channel.agentId));
399
+ void finishExit(ctx, instance, config.fullscreen === true, 'Updating @deepseek-harness-tui/dsh-tui and restarting…', undefined, () => runUpdate(ctx, profile, channel.agentId, updateTargetVersion));
340
400
  return;
341
401
  }
342
402
  // Judge against the live session behind the channel (channel.agentId),
@@ -392,6 +452,9 @@ export async function apply(ctx, config) {
392
452
  if (target.kind === 'unknown') {
393
453
  channel.notify(t('update-check-failed'));
394
454
  }
455
+ else {
456
+ updateTargetVersion = target.latest;
457
+ }
395
458
  channel.notify(t('update-starting'));
396
459
  updateRequested = true;
397
460
  handleExit();
@@ -665,13 +728,13 @@ function writeStream(stream, data) {
665
728
  }
666
729
  });
667
730
  }
668
- function runUpdate(ctx, profile, sessionId) {
731
+ function runUpdate(ctx, profile, sessionId, targetVersion) {
669
732
  disposeRootAndThen(ctx, () => {
670
733
  if (profile === undefined) {
671
734
  process.stderr.write(`\n${t('update-aborted-no-profile')}\n`);
672
735
  process.exit(1);
673
736
  }
674
- void updateTuiAndRestart(sessionId, profile).then(({ updateCode, restartCode }) => {
737
+ void updateTuiAndRestart(sessionId, profile, targetVersion).then(({ updateCode, restartCode }) => {
675
738
  if (updateCode !== 0) {
676
739
  process.stderr.write(`\ndsh-tui update failed (exit ${updateCode}). Your session is preserved — resume with:\n` +
677
740
  `${resumeCommand(profile, sessionId)}\n\n`);
@@ -0,0 +1,10 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const PROMPT_DEBUG_FILENAME = ".dsh-prompt-debug.json";
3
+ /**
4
+ * Register `/debug-prompt` and retain every final request observed for each
5
+ * live session. The command writes one private, atomic snapshot into the
6
+ * receiving session's workspace; it never records credentials, transport
7
+ * headers, or AbortSignals.
8
+ */
9
+ export declare function registerPromptDebug(ctx: Context): void;
10
+ //# sourceMappingURL=promptDebug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptDebug.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/promptDebug.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAMlD,eAAO,MAAM,qBAAqB,2BAA2B,CAAA;AAmF7D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA4FtD"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * On-demand capture of every final AgentLoop request context observed for a
3
+ * live session.
4
+ *
5
+ * The LLM waterfall is the last provider-neutral boundary before adapter
6
+ * dispatch. Capturing there records the fully assembled system prompt,
7
+ * messages, and tool schemas after prompt/tool plugins have contributed.
8
+ * @module @deepseek-harness-tui/dsh-tui/prompt-debug
9
+ */
10
+ import { join, resolve } from 'node:path';
11
+ import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
12
+ import { isAgentLoopRequest } from '@deepseek-ai/dsh-llm';
13
+ export const PROMPT_DEBUG_FILENAME = '.dsh-prompt-debug.json';
14
+ function latestPosition(agent) {
15
+ for (let index = agent.session.events.length - 1; index >= 0; index -= 1) {
16
+ const event = agent.session.events[index];
17
+ if (event?.type === 'step/start')
18
+ return event.data;
19
+ }
20
+ return undefined;
21
+ }
22
+ function latestRequestHeaderReason(agent) {
23
+ for (let index = agent.session.events.length - 1; index >= 0; index -= 1) {
24
+ const event = agent.session.events[index];
25
+ if (event?.type === 'request/header')
26
+ return event.data.reason;
27
+ }
28
+ return undefined;
29
+ }
30
+ function turnCompleted(agent, turn) {
31
+ return agent.session.events.some(event => event.type === 'turn/end' && event.data.turn === turn);
32
+ }
33
+ function captureRequest(agent, options, requestIndex, position, attempt) {
34
+ const requestHeaderReason = latestRequestHeaderReason(agent);
35
+ return {
36
+ capturedAt: new Date().toISOString(),
37
+ requestIndex,
38
+ ...position,
39
+ attempt,
40
+ ...(requestHeaderReason === undefined ? {} : { requestHeaderReason }),
41
+ finalLlmContext: {
42
+ provider: options.provider,
43
+ model: options.model,
44
+ ...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
45
+ ...(options.system === undefined ? {} : { system: options.system }),
46
+ messages: options.messages,
47
+ ...(options.tools === undefined ? {} : { tools: options.tools }),
48
+ ...(options.temperature === undefined ? {} : { temperature: options.temperature }),
49
+ ...(options.maxTokens === undefined ? {} : { maxTokens: options.maxTokens }),
50
+ ...(options.stop === undefined ? {} : { stop: options.stop }),
51
+ },
52
+ };
53
+ }
54
+ /**
55
+ * Register `/debug-prompt` and retain every final request observed for each
56
+ * live session. The command writes one private, atomic snapshot into the
57
+ * receiving session's workspace; it never records credentials, transport
58
+ * headers, or AbortSignals.
59
+ */
60
+ export function registerPromptDebug(ctx) {
61
+ const commands = ctx.get('commands');
62
+ const agents = ctx.get('agents');
63
+ if (commands === undefined || agents === undefined || ctx.get('llm') === undefined)
64
+ return;
65
+ const captureStartedAt = new Date().toISOString();
66
+ const captures = new Map();
67
+ ctx.on('llm/stream', (options, next) => {
68
+ if (!isAgentLoopRequest(options) || options.sessionId === undefined)
69
+ return next();
70
+ const agent = agents.get(options.sessionId);
71
+ if (agent === undefined)
72
+ return next();
73
+ const position = latestPosition(agent);
74
+ if (position === undefined)
75
+ return next();
76
+ const sessionId = String(options.sessionId);
77
+ const capture = captures.get(sessionId) ?? { requests: [] };
78
+ if (!captures.has(sessionId))
79
+ captures.set(sessionId, capture);
80
+ const previous = capture.requests.findLast(request => request.turn === position.turn && request.step === position.step);
81
+ const attempt = (previous?.attempt ?? 0) + 1;
82
+ capture.requests.push(captureRequest(agent, options, capture.requests.length + 1, position, attempt));
83
+ return next();
84
+ });
85
+ // Replaced agents are no longer addressable by `/debug-prompt`; release
86
+ // their potentially large message/tool snapshots with the agent lifecycle.
87
+ ctx.on('agent/disposed', ({ agent }) => {
88
+ captures.delete(String(agent.session.id));
89
+ });
90
+ ctx.effect(() => commands.register({
91
+ name: 'debug-prompt',
92
+ description: 'Write every observed final LLM request context for this session to the workspace root',
93
+ recordInput: false,
94
+ handler: async ({ agent, rawInput }) => {
95
+ if (rawInput.trim().length > 0) {
96
+ return { kind: 'error', text: 'Usage: /debug-prompt' };
97
+ }
98
+ const sessionId = String(agent.session.id);
99
+ const capture = captures.get(sessionId);
100
+ if (capture === undefined || capture.requests.length === 0) {
101
+ return {
102
+ kind: 'error',
103
+ text: 'No final LLM request is available for this session. Send one prompt, wait for it to finish, then run /debug-prompt.',
104
+ };
105
+ }
106
+ const latest = capture.requests.at(-1);
107
+ if (!turnCompleted(agent, latest.turn)) {
108
+ return { kind: 'error', text: 'The latest model turn is still running. Wait for it to finish, then run /debug-prompt.' };
109
+ }
110
+ const root = resolve(agent.session.header.cwd ?? process.cwd());
111
+ const output = join(root, PROMPT_DEBUG_FILENAME);
112
+ const turns = new Map();
113
+ for (const request of capture.requests) {
114
+ const turn = turns.get(request.turn) ?? { turn: request.turn, requests: [] };
115
+ if (!turns.has(request.turn))
116
+ turns.set(request.turn, turn);
117
+ turn.requests.push(request);
118
+ }
119
+ const document = {
120
+ schemaVersion: 2,
121
+ generatedAt: new Date().toISOString(),
122
+ captureStartedAt,
123
+ captureScope: 'Requests observed by this dsh-tui process; requests from before a session resume cannot be reconstructed.',
124
+ warning: 'Sensitive debug data: contains system prompts, conversation messages, tool schemas, and tool results. It intentionally excludes credentials and transport headers.',
125
+ sessionId,
126
+ turnCount: turns.size,
127
+ requestCount: capture.requests.length,
128
+ turns: [...turns.values()].map(turn => ({
129
+ ...turn,
130
+ requestCount: turn.requests.length,
131
+ })),
132
+ };
133
+ try {
134
+ await writeFileAtomic(output, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600 });
135
+ }
136
+ catch (error) {
137
+ return {
138
+ kind: 'error',
139
+ text: `Could not write prompt debug file: ${error instanceof Error ? error.message : String(error)}`,
140
+ };
141
+ }
142
+ return {
143
+ kind: 'success',
144
+ text: `Wrote ${capture.requests.length} final LLM request${capture.requests.length === 1 ? '' : 's'} to ${output}. The file contains sensitive conversation and prompt data.`,
145
+ };
146
+ },
147
+ }));
148
+ }