@agi-fans/dsh-tui 0.17.0 → 0.19.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 (100) hide show
  1. package/lib/CHANGELOG.md +67 -1
  2. package/lib/chrome/box.d.ts +2 -0
  3. package/lib/chrome/box.js +5 -1
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/main-screen-renderer.d.ts +61 -11
  6. package/lib/chrome/main-screen-renderer.js +1674 -71
  7. package/lib/chrome/main-screen-renderer.js.map +1 -1
  8. package/lib/chrome/markdown.js +21 -13
  9. package/lib/chrome/markdown.js.map +1 -1
  10. package/lib/chrome/renderer.d.ts +75 -0
  11. package/lib/chrome/renderer.js.map +1 -1
  12. package/lib/chrome/status-config.d.ts +4 -1
  13. package/lib/chrome/status-config.js.map +1 -1
  14. package/lib/chrome/theme.d.ts +18 -0
  15. package/lib/chrome/theme.js +18 -0
  16. package/lib/chrome/theme.js.map +1 -1
  17. package/lib/chrome/tool-args.d.ts +24 -1
  18. package/lib/chrome/tool-args.js +40 -1
  19. package/lib/chrome/tool-args.js.map +1 -1
  20. package/lib/chrome/tool-renderers.d.ts +11 -0
  21. package/lib/chrome/tool-renderers.js +55 -7
  22. package/lib/chrome/tool-renderers.js.map +1 -1
  23. package/lib/chrome/welcome-tips.js +1 -1
  24. package/lib/chrome/welcome-tips.js.map +1 -1
  25. package/lib/commands/auth.js +4 -2
  26. package/lib/commands/auth.js.map +1 -1
  27. package/lib/commands/jobs.d.ts +2 -2
  28. package/lib/commands/jobs.js +2 -2
  29. package/lib/commands/jobs.js.map +1 -1
  30. package/lib/definition.d.ts +62 -7
  31. package/lib/definition.js.map +1 -1
  32. package/lib/index.d.ts +1 -1
  33. package/lib/index.js +1 -1
  34. package/lib/index.js.map +1 -1
  35. package/lib/input/keybindings-config.d.ts +1 -1
  36. package/lib/input/keybindings-config.js +2 -1
  37. package/lib/input/keybindings-config.js.map +1 -1
  38. package/lib/input/keys.d.ts +7 -0
  39. package/lib/input/keys.js +11 -4
  40. package/lib/input/keys.js.map +1 -1
  41. package/lib/runtime/provider-local.d.ts +63 -11
  42. package/lib/runtime/provider-local.js +506 -91
  43. package/lib/runtime/provider-local.js.map +1 -1
  44. package/lib/runtime/session-runtime.d.ts +9 -1
  45. package/lib/runtime/session-runtime.js +2 -2
  46. package/lib/runtime/session-runtime.js.map +1 -1
  47. package/lib/runtime/startup-notices.js +6 -9
  48. package/lib/runtime/startup-notices.js.map +1 -1
  49. package/lib/runtime/tool-presentation.js +44 -31
  50. package/lib/runtime/tool-presentation.js.map +1 -1
  51. package/lib/session/feature-toggles.d.ts +100 -0
  52. package/lib/session/feature-toggles.js +170 -0
  53. package/lib/session/feature-toggles.js.map +1 -0
  54. package/lib/session/fold-policy.d.ts +21 -0
  55. package/lib/session/fold-policy.js +15 -0
  56. package/lib/session/fold-policy.js.map +1 -0
  57. package/lib/session/job-notice.d.ts +6 -2
  58. package/lib/session/job-notice.js +44 -3
  59. package/lib/session/job-notice.js.map +1 -1
  60. package/lib/session/session-configuration.d.ts +0 -3
  61. package/lib/session/session-configuration.js +0 -4
  62. package/lib/session/session-configuration.js.map +1 -1
  63. package/lib/session/session-controller.d.ts +6 -4
  64. package/lib/session/session-controller.js +147 -55
  65. package/lib/session/session-controller.js.map +1 -1
  66. package/lib/session/session-library.d.ts +20 -0
  67. package/lib/session/session-library.js +52 -0
  68. package/lib/session/session-library.js.map +1 -1
  69. package/lib/session/subagent-roster.js +3 -3
  70. package/lib/session/subagent-roster.js.map +1 -1
  71. package/lib/session/subagent-tracker.d.ts +2 -9
  72. package/lib/session/subagent-tracker.js +3 -4
  73. package/lib/session/subagent-tracker.js.map +1 -1
  74. package/lib/session/tui-settings.d.ts +46 -18
  75. package/lib/session/tui-settings.js +59 -39
  76. package/lib/session/tui-settings.js.map +1 -1
  77. package/lib/views/at-complete.d.ts +2 -0
  78. package/lib/views/at-complete.js +2 -2
  79. package/lib/views/at-complete.js.map +1 -1
  80. package/lib/views/event-views.d.ts +26 -2
  81. package/lib/views/event-views.js +162 -48
  82. package/lib/views/event-views.js.map +1 -1
  83. package/lib/views/hotkeys.d.ts +2 -0
  84. package/lib/views/hotkeys.js +7 -3
  85. package/lib/views/hotkeys.js.map +1 -1
  86. package/lib/views/settings-list.d.ts +12 -6
  87. package/lib/views/settings-list.js +41 -18
  88. package/lib/views/settings-list.js.map +1 -1
  89. package/lib/views/transcript-export.js +0 -2
  90. package/lib/views/transcript-export.js.map +1 -1
  91. package/lib/views/transcript-render.d.ts +133 -18
  92. package/lib/views/transcript-render.js +1179 -112
  93. package/lib/views/transcript-render.js.map +1 -1
  94. package/lib/views/transcript-types.d.ts +79 -1
  95. package/lib/views/transcript-types.js +11 -0
  96. package/lib/views/transcript-types.js.map +1 -1
  97. package/lib/views/workspace-changes.d.ts +27 -0
  98. package/lib/views/workspace-changes.js +128 -0
  99. package/lib/views/workspace-changes.js.map +1 -0
  100. package/package.json +40 -38
@@ -7,34 +7,36 @@
7
7
  * @module @agi-fans/dsh-tui/session-controller
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { join } from 'node:path';
10
11
  import { installModelSelection, } from '@deepseek-ai/dsh-agent';
11
12
  import { createUserMessage, } from '@deepseek-ai/dsh-llm';
12
13
  import { isUserInvocable } from '@deepseek-ai/dsh-skill';
14
+ import { SessionAlreadyOwnedError } from '@deepseek-ai/dsh-session-persistence';
13
15
  import { readColdSessionLog } from '@deepseek-ai/dsh-session-query';
14
- import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
16
+ import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
15
17
  import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
16
18
  import { firstVisibleStreamTime } from "../views/stream-time.js";
17
19
  import { LiveAttemptTracker } from "./live-attempt-tracker.js";
18
20
  import { blocksText } from "./content-text.js";
19
21
  import { jobNoticeFor } from "./job-notice.js";
20
22
  import { descendantDepth, isSteerableSubagent } from "./subagent-roster.js";
23
+ import { readRecentRows, writeRecentRows } from "./session-library.js";
24
+ import { readJsonFile, writeJsonAtomic } from "./json-file.js";
21
25
  import { SubagentTracker } from "./subagent-tracker.js";
22
26
  import { commandPermission } from "../commands/permission.js";
23
- import { isBlankSession, toolPresentationForPreset, } from "./session-configuration.js";
27
+ import { isBlankSession, } from "./session-configuration.js";
24
28
  import { stripComposerImageMarkers } from "../input/image-paste.js";
25
29
  async function setupAgentContext(agentCtx, agent, selection) {
26
30
  installModelSelection(agentCtx, selection);
27
31
  const agentPresets = agentCtx.get('agentPresets');
28
32
  const sessionProjections = agentCtx.get('sessionProjections');
29
- const tools = agentCtx.get('tools');
30
- if (agentPresets === undefined || sessionProjections === undefined || tools === undefined) {
33
+ if (agentPresets === undefined || sessionProjections === undefined) {
31
34
  throw new Error('agent configuration services are unavailable');
32
35
  }
33
36
  const agentPreset = sessionProjections.stateOf(agent.session, 'agentPreset') ?? agentPresets.defaultId;
34
37
  const mounted = await agentPresets.mount(agentCtx, agentPreset);
35
- const disposeToolPresentation = tools.presentAs(toolPresentationForPreset(mounted.id));
36
38
  await agentCtx.plugin(commandPermission(agent));
37
- return { agentPreset: mounted.id, disposeToolPresentation };
39
+ return { agentPreset: mounted.id };
38
40
  }
39
41
  function parseControl(line) {
40
42
  const match = /^\/([a-z][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)(?:\s+(.*))?$/su.exec(line.trim());
@@ -393,14 +395,19 @@ export class SessionRuntime {
393
395
  #tracker;
394
396
  #streamAttempts = new LiveAttemptTracker();
395
397
  #recentCache = new Map();
398
+ #recentRowsPath;
399
+ #upgradeMarkerPath;
400
+ #recentRowsLoaded = false;
396
401
  #refreshInFlight = null;
397
402
  #refreshDirty = false;
398
403
  #inspectEpoch = 0;
399
404
  #activationEpoch = 0;
400
405
  #inspectedId;
401
- constructor(ctx, tui) {
406
+ constructor(ctx, tui, options = {}) {
402
407
  this.#ctx = ctx;
403
408
  this.#tui = tui;
409
+ this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
410
+ this.#upgradeMarkerPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'sessions-upgraded.json');
404
411
  this.#tracker = new SubagentTracker({
405
412
  rootId: () => this.#active?.handle.agent.id,
406
413
  depth: (session) => {
@@ -446,10 +453,23 @@ export class SessionRuntime {
446
453
  }
447
454
  const jobs = this.#ctx.get('jobs');
448
455
  if (jobs !== undefined) {
449
- this.#off.push(jobs.onJobDone((snapshot, owner) => {
450
- const notice = jobNoticeFor(snapshot, owner, this.#active?.handle.agent);
451
- if (notice !== undefined)
452
- tui.notice(notice);
456
+ // Settlements speak session ids, so the owning Agent is resolved here;
457
+ // a job whose owner is gone has no notice to show.
458
+ this.#off.push(jobs.events.subscribe({ owners: 'scope' }, (event) => {
459
+ if (event.type !== 'settled')
460
+ return;
461
+ const owner = event.job.owner === undefined ? undefined : this.#ctx.get('agents')?.get(event.job.owner);
462
+ const notice = jobNoticeFor(event.job, owner, this.#active?.handle.agent);
463
+ if (notice !== undefined) {
464
+ const job = event.job;
465
+ tui.notice(notice, {
466
+ process: true, processSource: `job:${job.id}`,
467
+ ...(/^(bash|pwsh)$/u.test(job.kind) && (job.status === 'completed' || job.status === 'failed' || job.status === 'killed')
468
+ ? { job: { id: job.id, label: job.label, status: job.status, startedAt: job.startedAt, ...(job.detail === undefined ? {} : { detail: job.detail }) } }
469
+ : {}),
470
+ ...(job.status === 'failed' || job.status === 'killed' ? { level: 'error' } : {}),
471
+ });
472
+ }
453
473
  }));
454
474
  }
455
475
  this.#off.push(ctx.on('agent/status', (payload) => {
@@ -482,6 +502,8 @@ export class SessionRuntime {
482
502
  if (this.#inspectedId === undefined) {
483
503
  tui.event(event, ctx.get('tuiToolPresentation')?.event(active.handle.agent, event));
484
504
  }
505
+ if (event.type === 'workspace/changes')
506
+ this.#serveWorkspaceSummary(ctx, tui, session, event);
485
507
  if (event.type === 'subagent/catalog')
486
508
  this.#tracker.observeCatalog(session, [event]);
487
509
  if (shouldRefreshSessionInfoAfter(event))
@@ -535,6 +557,13 @@ export class SessionRuntime {
535
557
  this.#pushSessionInfo();
536
558
  }));
537
559
  }
560
+ // One-time per session format, off the first render: stored logs an earlier
561
+ // release wrote get their current-format generation here rather than on the
562
+ // next listing or search that has to decode them.
563
+ ctx.inject(['sessionPersistence'], (persistenceCtx) => {
564
+ void this.#upgradeStoredSessions(persistenceCtx.sessionPersistence)
565
+ .catch((error) => { persistenceCtx.logger.warn(error); });
566
+ });
538
567
  }
539
568
  get agent() {
540
569
  return this.#active?.handle.agent;
@@ -560,7 +589,7 @@ export class SessionRuntime {
560
589
  if (defaults === undefined)
561
590
  throw new Error('agent default model is unavailable');
562
591
  await this.refreshRecent();
563
- await this.#activate(await this.#create(defaults));
592
+ await this.#activate(await this.#create(defaults), 'initial');
564
593
  }
565
594
  /** Submit one human composer value; active turns retain it as a later follow-up. */
566
595
  async send(input, agent = this.#requiredAgent()) {
@@ -621,9 +650,9 @@ export class SessionRuntime {
621
650
  this.#tui.restoreInput({ text: line, images });
622
651
  if (result.text !== undefined) {
623
652
  if (result.kind === 'error')
624
- this.#tui.notice(result.text, { level: 'error' });
653
+ this.#tui.notice(result.text, { level: 'error', focus: true });
625
654
  else
626
- this.#tui.commandOutput(parsed.name, result.text);
655
+ this.#tui.commandOutput(parsed.name, result.text, { focus: true });
627
656
  }
628
657
  return true;
629
658
  }
@@ -662,7 +691,7 @@ export class SessionRuntime {
662
691
  /** Start a new top-level session with the current model selection. */
663
692
  async newSession(agent) {
664
693
  this.assertActive(agent);
665
- await this.#activate(await this.#create(this.selection(agent)));
694
+ await this.#activate(await this.#create(this.selection(agent)), 'new');
666
695
  await this.refreshRecent();
667
696
  }
668
697
  /** Replace the active top-level session with one durable session. */
@@ -688,7 +717,6 @@ export class SessionRuntime {
688
717
  contextWindow: undefined,
689
718
  reasoningEffort: undefined,
690
719
  agentPreset: configuration.agentPreset,
691
- disposeToolPresentation: configuration.disposeToolPresentation,
692
720
  });
693
721
  await this.refreshRecent();
694
722
  }
@@ -817,7 +845,6 @@ export class SessionRuntime {
817
845
  contextWindow: undefined,
818
846
  reasoningEffort: undefined,
819
847
  agentPreset: configuration.agentPreset,
820
- disposeToolPresentation: configuration.disposeToolPresentation,
821
848
  });
822
849
  this.#tui.restoreInput({ text, images });
823
850
  this.#tui.notice(`Rewound to before turn ${selected.turn}. The original session remains available in /resume.`);
@@ -864,15 +891,7 @@ export class SessionRuntime {
864
891
  const active = this.#requiredActive();
865
892
  if (active.agentPreset === id)
866
893
  return active.agentPreset;
867
- const previousPreset = active.agentPreset;
868
894
  const preset = await this.#ctx.agentPresets.recompose(agent.ctx, id);
869
- try {
870
- this.#replaceToolPresentation(active, toolPresentationForPreset(preset.id));
871
- }
872
- catch (error) {
873
- await this.#ctx.agentPresets.recompose(agent.ctx, previousPreset);
874
- throw error;
875
- }
876
895
  active.agentPreset = preset.id;
877
896
  agent.session.append('agent-preset/selected', { agentPreset: preset.id });
878
897
  this.#pushTools();
@@ -892,6 +911,78 @@ export class SessionRuntime {
892
911
  this.#pushSessionInfo();
893
912
  return outcome;
894
913
  }
914
+ /**
915
+ * Resolve and attach the changed-file summary a `workspace/changes` event
916
+ * announces.
917
+ *
918
+ * The Harness appends the event and only then files the summary under its
919
+ * sequence, so a listener running inside that append always reads
920
+ * `undefined`. The lookup is therefore deferred by one microtask, which runs
921
+ * after the recorder's own synchronous store, and enriches the turn-only
922
+ * block the transcript already rendered. A session without the plugin, or a
923
+ * recorder that has already been disposed, simply keeps the fallback.
924
+ *
925
+ * @param ctx - host context carrying the optional service.
926
+ * @param tui - the mounted presentation service.
927
+ * @param session - the Session that appended the event.
928
+ * @param event - the `workspace/changes` event.
929
+ */
930
+ #serveWorkspaceSummary(ctx, tui, session, event) {
931
+ const service = ctx.get('workspaceChanges');
932
+ if (service === undefined)
933
+ return;
934
+ const { id } = session;
935
+ const { seq, data } = event;
936
+ queueMicrotask(() => {
937
+ const summary = service.summary(id, seq);
938
+ if (summary !== undefined)
939
+ tui.setWorkspaceSummary(data.turn, summary);
940
+ });
941
+ }
942
+ /**
943
+ * Publish a current-format generation for every stored session an earlier
944
+ * release wrote. A read open never upgrades a log, so without this pass
945
+ * every listing and search keeps decoding the historical corpus — the pass
946
+ * runs once per session format, in the background, and skips a session
947
+ * another live process owns.
948
+ */
949
+ async #upgradeStoredSessions(persistence) {
950
+ const markerPath = this.#upgradeMarkerPath;
951
+ if (markerPath === undefined)
952
+ return;
953
+ const marker = readJsonFile(markerPath);
954
+ if (typeof marker?.sessionFormat === 'number' && marker.sessionFormat >= SESSION_FORMAT_VERSION)
955
+ return;
956
+ const before = await persistence.list();
957
+ const revisions = new Map(before.map(snapshot => [String(snapshot.header.id), snapshot.revision]));
958
+ const active = this.#active?.handle.agent.id;
959
+ for (const snapshot of before) {
960
+ if (this.#disposed)
961
+ return;
962
+ if (String(snapshot.header.id) === active)
963
+ continue;
964
+ try {
965
+ const handle = await persistence.open(snapshot.header.id, 'write');
966
+ await handle.close();
967
+ }
968
+ catch (error) {
969
+ // Another process owns it, or the log is unreadable: leave it alone.
970
+ if (!(error instanceof SessionAlreadyOwnedError))
971
+ this.#ctx.logger.debug(error);
972
+ }
973
+ }
974
+ if (this.#disposed)
975
+ return;
976
+ let upgraded = 0;
977
+ for (const snapshot of await persistence.list()) {
978
+ if (revisions.get(String(snapshot.header.id)) !== snapshot.revision)
979
+ upgraded += 1;
980
+ }
981
+ writeJsonAtomic(markerPath, { sessionFormat: SESSION_FORMAT_VERSION });
982
+ if (upgraded > 0) {
983
+ this.#tui.notice(`Upgraded ${upgraded} stored session${upgraded === 1 ? '' : 's'} to the current format.`);
984
+ }
985
+ }
895
986
  async refreshRecent() {
896
987
  // Coalesce concurrent callers: the recent list is one snapshot, and a
897
988
  // burst of title/status events must not queue a read of every stored log.
@@ -921,24 +1012,40 @@ export class SessionRuntime {
921
1012
  }
922
1013
  const snapshots = (await persistence.list()).filter(snapshot => snapshot.header.origin !== 'subagent')
923
1014
  .sort((left, right) => right.header.createdAt - left.header.createdAt);
1015
+ // A stored row stays valid while its revision holds, so a cold process
1016
+ // renders labels for unchanged sessions without reading their logs; only a
1017
+ // new or changed session pays a read.
1018
+ if (!this.#recentRowsLoaded && this.#recentRowsPath !== undefined) {
1019
+ this.#recentRowsLoaded = true;
1020
+ for (const [id, memo] of readRecentRows(this.#recentRowsPath))
1021
+ this.#recentCache.set(id, memo);
1022
+ }
924
1023
  const rows = [];
1024
+ let memoChanged = false;
925
1025
  const liveIds = new Set(snapshots.map(snapshot => snapshot.header.id));
926
- for (const id of this.#recentCache.keys())
927
- if (!liveIds.has(id))
928
- this.#recentCache.delete(id);
1026
+ for (const id of this.#recentCache.keys()) {
1027
+ if (liveIds.has(id))
1028
+ continue;
1029
+ this.#recentCache.delete(id);
1030
+ memoChanged = true;
1031
+ }
929
1032
  for (const snapshot of snapshots) {
930
1033
  const header = snapshot.header;
931
1034
  const cached = this.#recentCache.get(header.id);
932
1035
  if (cached?.revision === snapshot.revision) {
933
- rows.push(cached.row);
1036
+ if (cached.row !== null)
1037
+ rows.push(cached.row);
934
1038
  continue;
935
1039
  }
936
1040
  try {
937
1041
  const log = await readColdSessionLog(persistence, header.id);
938
1042
  const status = recentSessionStatus(log.events);
939
1043
  const content = recentSessionContent(log.events);
940
- if (content === undefined)
1044
+ if (content === undefined) {
1045
+ this.#recentCache.set(header.id, { revision: snapshot.revision, row: null });
1046
+ memoChanged = true;
941
1047
  continue;
1048
+ }
942
1049
  const row = {
943
1050
  id: header.id,
944
1051
  ...content,
@@ -948,12 +1055,15 @@ export class SessionRuntime {
948
1055
  ...(status === undefined ? {} : { status }),
949
1056
  };
950
1057
  this.#recentCache.set(header.id, { revision: snapshot.revision, row });
1058
+ memoChanged = true;
951
1059
  rows.push(row);
952
1060
  }
953
1061
  catch {
954
1062
  rows.push({ id: header.id, title: '(unavailable session)', createdAt: header.createdAt });
955
1063
  }
956
1064
  }
1065
+ if (memoChanged && this.#recentRowsPath !== undefined)
1066
+ writeRecentRows(this.#recentRowsPath, this.#recentCache);
957
1067
  this.#recent = rows;
958
1068
  this.#pushSessionInfo();
959
1069
  }
@@ -1002,7 +1112,6 @@ export class SessionRuntime {
1002
1112
  contextWindow: undefined,
1003
1113
  reasoningEffort: undefined,
1004
1114
  agentPreset: configuration.agentPreset,
1005
- disposeToolPresentation: configuration.disposeToolPresentation,
1006
1115
  };
1007
1116
  }
1008
1117
  /**
@@ -1035,7 +1144,7 @@ export class SessionRuntime {
1035
1144
  return undefined;
1036
1145
  }
1037
1146
  }
1038
- async #activate(next) {
1147
+ async #activate(next, reason = 'open') {
1039
1148
  // Reject a late create before it publishes UI on a disposed tree.
1040
1149
  if (this.#disposed) {
1041
1150
  this.#releaseHandle(next.handle);
@@ -1046,7 +1155,7 @@ export class SessionRuntime {
1046
1155
  this.#active = next;
1047
1156
  void this.#claimWorkspace(next);
1048
1157
  try {
1049
- this.#presentAgent(next);
1158
+ this.#presentAgent(next, reason);
1050
1159
  const selected = this.selection(next.handle.agent);
1051
1160
  const info = await this.#resolveModelInfo(selected);
1052
1161
  if (this.#disposed || this.#activationEpoch !== epoch) {
@@ -1107,7 +1216,7 @@ export class SessionRuntime {
1107
1216
  this.#releaseHandle(previous.handle);
1108
1217
  }
1109
1218
  /** Show one active session on the terminal: transcript, tools, controls, model. */
1110
- #presentAgent(active) {
1219
+ #presentAgent(active, reason = 'open') {
1111
1220
  const agent = active.handle.agent;
1112
1221
  this.#inspectedId = undefined;
1113
1222
  this.#tui.setInspectedSubagent(undefined);
@@ -1116,7 +1225,7 @@ export class SessionRuntime {
1116
1225
  // Seed welcome metadata before replaceSession commits the startup header to
1117
1226
  // native scrollback; later updates cannot rewrite that frozen first frame.
1118
1227
  this.#pushSessionInfo();
1119
- this.#replaceTranscript(agent);
1228
+ this.#replaceTranscript(agent, reason);
1120
1229
  this.#pushTools();
1121
1230
  this.#pushCommands();
1122
1231
  const selected = this.selection(agent);
@@ -1238,27 +1347,10 @@ export class SessionRuntime {
1238
1347
  controls: this.#sessionControls(active, projection),
1239
1348
  });
1240
1349
  }
1241
- #replaceToolPresentation(active, mode) {
1242
- const previousMode = toolPresentationForPreset(active.agentPreset);
1243
- active.disposeToolPresentation();
1244
- try {
1245
- const tools = active.handle.agent.ctx.get('tools');
1246
- if (tools === undefined)
1247
- throw new Error('tool registry is unavailable');
1248
- active.disposeToolPresentation = tools.presentAs(mode);
1249
- }
1250
- catch (error) {
1251
- const tools = active.handle.agent.ctx.get('tools');
1252
- if (tools === undefined)
1253
- throw error;
1254
- active.disposeToolPresentation = tools.presentAs(previousMode);
1255
- throw error;
1256
- }
1257
- }
1258
- #replaceTranscript(agent) {
1350
+ #replaceTranscript(agent, reason = 'refresh') {
1259
1351
  const events = agent.session.snapshotEvents();
1260
1352
  this.#tracker.observeCatalog(agent.session, events);
1261
- this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status);
1353
+ this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
1262
1354
  this.#replayLivePrefix(agent.session.id);
1263
1355
  }
1264
1356
  #replaceVisibleTranscript() {
@@ -1313,7 +1405,7 @@ export class SessionRuntime {
1313
1405
  return;
1314
1406
  this.#inspectedId = id;
1315
1407
  const child = this.#ctx.get('agents')?.get(SessionId(id));
1316
- this.#tui.replaceSession(events, child === undefined ? undefined : this.#ctx.get('tuiToolPresentation')?.session(child, events), child?.status ?? 'idle');
1408
+ this.#tui.replaceSession(events, child === undefined ? undefined : this.#ctx.get('tuiToolPresentation')?.session(child, events), child?.status ?? 'idle', 'refresh');
1317
1409
  this.#replayLivePrefix(id);
1318
1410
  this.#tui.setInspectedSubagent(this.#inspectView(id, child?.status === 'running' ? 'running' : 'waiting'));
1319
1411
  this.#tui.setStatus(child?.status ?? 'idle');