@markusylisiurunen/tau 0.3.41 → 0.3.43

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 (104) hide show
  1. package/README.md +14 -6
  2. package/dist/core/agent/agent_runtime.js +32 -44
  3. package/dist/core/agent/agent_runtime.js.map +1 -1
  4. package/dist/core/auth/auth_manager.js +25 -9
  5. package/dist/core/auth/auth_manager.js.map +1 -1
  6. package/dist/core/auth/auth_storage.js +1 -0
  7. package/dist/core/auth/auth_storage.js.map +1 -1
  8. package/dist/core/auth/cli.js +49 -35
  9. package/dist/core/auth/cli.js.map +1 -1
  10. package/dist/core/auth/credential_store.js +54 -22
  11. package/dist/core/auth/credential_store.js.map +1 -1
  12. package/dist/core/auth/index.js +1 -1
  13. package/dist/core/auth/index.js.map +1 -1
  14. package/dist/core/auth/providers/openai_codex.js +71 -23
  15. package/dist/core/auth/providers/openai_codex.js.map +1 -1
  16. package/dist/core/cli.js +4 -2
  17. package/dist/core/cli.js.map +1 -1
  18. package/dist/core/config/builtin_themes.js +62 -155
  19. package/dist/core/config/builtin_themes.js.map +1 -1
  20. package/dist/core/index.js +1 -1
  21. package/dist/core/index.js.map +1 -1
  22. package/dist/core/models/tau_model_overrides.js +1 -66
  23. package/dist/core/models/tau_model_overrides.js.map +1 -1
  24. package/dist/core/session/direct_bash.js +1 -0
  25. package/dist/core/session/direct_bash.js.map +1 -1
  26. package/dist/core/session/runner.js +21 -14
  27. package/dist/core/session/runner.js.map +1 -1
  28. package/dist/core/static/code_mode/capabilities.mjs +25 -0
  29. package/dist/core/static/code_mode/history/documentation.md +2 -0
  30. package/dist/core/static/code_mode/history/sandbox_runner.mjs +4 -1
  31. package/dist/core/static/code_mode/nook/documentation.md +2 -0
  32. package/dist/core/static/code_mode/nook/sandbox_runner.mjs +4 -1
  33. package/dist/core/static/code_mode/web/documentation.md +2 -0
  34. package/dist/core/static/code_mode/web/sandbox_runner.mjs +4 -1
  35. package/dist/core/subagents/agent_supervisor.js +3 -3
  36. package/dist/core/subagents/agent_supervisor.js.map +1 -1
  37. package/dist/core/telegram/adapter.js +0 -23
  38. package/dist/core/telegram/adapter.js.map +1 -1
  39. package/dist/core/telegram/session_manager.js +34 -7
  40. package/dist/core/telegram/session_manager.js.map +1 -1
  41. package/dist/core/tools/bash.js +10 -2
  42. package/dist/core/tools/bash.js.map +1 -1
  43. package/dist/core/tools/code_mode.js +1 -0
  44. package/dist/core/tools/code_mode.js.map +1 -1
  45. package/dist/core/tools/presentation.js +1 -1
  46. package/dist/core/utils/auto_retry.js +1 -1
  47. package/dist/core/utils/auto_retry.js.map +1 -1
  48. package/dist/core/version.js +1 -1
  49. package/dist/host/local_session_host.js +319 -198
  50. package/dist/host/local_session_host.js.map +1 -1
  51. package/dist/host/session_protocol_handler.js +1 -1
  52. package/dist/host/session_protocol_handler.js.map +1 -1
  53. package/dist/main.js +16 -2
  54. package/dist/main.js.map +1 -1
  55. package/dist/protocol/index.d.ts +2 -2
  56. package/dist/protocol/index.js +1 -1
  57. package/dist/protocol/index.js.map +1 -1
  58. package/dist/protocol/session_protocol.d.ts +115 -40
  59. package/dist/protocol/session_protocol.js +377 -98
  60. package/dist/protocol/session_protocol.js.map +1 -1
  61. package/dist/sdk/index.d.ts +1 -2
  62. package/dist/sdk/index.js +0 -1
  63. package/dist/sdk/index.js.map +1 -1
  64. package/dist/store/session_snapshot_migrations.js +311 -4
  65. package/dist/store/session_snapshot_migrations.js.map +1 -1
  66. package/dist/tui/chat_controller/assistant_clipboard.js +12 -8
  67. package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -1
  68. package/dist/tui/chat_controller/diff_review_service.js +13 -9
  69. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  70. package/dist/tui/chat_controller/history_message_model.js +3 -3
  71. package/dist/tui/chat_controller/history_message_model.js.map +1 -1
  72. package/dist/tui/chat_controller/status_format.js +18 -0
  73. package/dist/tui/chat_controller/status_format.js.map +1 -1
  74. package/dist/tui/chat_view.js +39 -38
  75. package/dist/tui/chat_view.js.map +1 -1
  76. package/dist/tui/session_chat_app.js +1 -3
  77. package/dist/tui/session_chat_app.js.map +1 -1
  78. package/dist/tui/session_chat_controller.js +375 -241
  79. package/dist/tui/session_chat_controller.js.map +1 -1
  80. package/dist/tui/speech_playback.js +4 -4
  81. package/dist/tui/speech_playback.js.map +1 -1
  82. package/dist/tui/ui/assistant_message.js +1 -15
  83. package/dist/tui/ui/assistant_message.js.map +1 -1
  84. package/dist/tui/ui/chat_message_model.js +26 -7
  85. package/dist/tui/ui/chat_message_model.js.map +1 -1
  86. package/dist/tui/ui/custom_editor.js +1 -1
  87. package/dist/tui/ui/custom_editor.js.map +1 -1
  88. package/dist/tui/ui/footer.js +161 -47
  89. package/dist/tui/ui/footer.js.map +1 -1
  90. package/dist/tui/ui/theme/palette.js +10 -16
  91. package/dist/tui/ui/theme/palette.js.map +1 -1
  92. package/dist/tui/ui/theme/theme.js.map +1 -1
  93. package/dist/tui/ui/tool_card.js +12 -9
  94. package/dist/tui/ui/tool_card.js.map +1 -1
  95. package/dist/tui/ui/transcript_notice.js +44 -0
  96. package/dist/tui/ui/transcript_notice.js.map +1 -0
  97. package/dist/tui/ui/transcript_text.js +14 -0
  98. package/dist/tui/ui/transcript_text.js.map +1 -0
  99. package/package.json +10 -12
  100. package/dist/sdk/turn_outcome.d.ts +0 -2
  101. package/dist/sdk/turn_outcome.js +0 -4
  102. package/dist/sdk/turn_outcome.js.map +0 -1
  103. package/dist/tui/ui/system_message.js +0 -22
  104. package/dist/tui/ui/system_message.js.map +0 -1
@@ -12,7 +12,7 @@ import { REASONING_LEVELS } from "../core/types.js";
12
12
  import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "../core/utils/format.js";
13
13
  import { extractAssistantText } from "../core/utils/messages.js";
14
14
  import { collectSpeechToTextContext } from "../core/utils/speech_to_text_context.js";
15
- import { getAutoCompactionMetadataFromMessage, hasAutoCompactionContinuationMetadata, } from "../core/utils/user_metadata.js";
15
+ import { hasAutoCompactionContinuationMetadata } from "../core/utils/user_metadata.js";
16
16
  import { APP_VERSION } from "../core/version.js";
17
17
  import { applySessionProtocolDelta } from "../protocol/session_protocol.js";
18
18
  import { TauSessionProtocolResponseError } from "../transport/errors.js";
@@ -22,7 +22,7 @@ import { formatDiffReviewUserMessage } from "./chat_controller/diff_review_user_
22
22
  import { formatRewindCandidateAge, formatRewindCandidateLabel, } from "./chat_controller/history_labels.js";
23
23
  import { buildHistoryMessageModel, extractHistoryUserText, } from "./chat_controller/history_message_model.js";
24
24
  import { InterruptLifecycle } from "./chat_controller/interrupt_lifecycle.js";
25
- import { formatDurationMs, formatSessionCost } from "./chat_controller/status_format.js";
25
+ import { formatDurationMs, formatFooterTokenCount, formatSessionCost, } from "./chat_controller/status_format.js";
26
26
  import { copyTextToClipboard } from "./clipboard.js";
27
27
  import { DOUBLE_PRESS_WINDOW_MS } from "./constants.js";
28
28
  import { cleanupListenTempFile, createListenTempFilePath, getSpeechToTextApiKey, getSpeechToTextApiKeyErrorMessage, LISTEN_RECORDING_MAX_DURATION_MS, LISTEN_RECORDING_MIN_BYTES, readListenAudio, startListenAudioCapture, transcribeListenAudio, } from "./listen_capture.js";
@@ -46,15 +46,21 @@ export class SessionChatController {
46
46
  diffReviewService;
47
47
  snapshot;
48
48
  renderedMessageIds = [];
49
+ viewMessageIds = new Map();
50
+ usedViewMessageIds = new Set();
49
51
  hiddenHistoryEntryIds = new Set();
52
+ ephemeralTimelineItems = new Map();
53
+ renderSegment = 0;
50
54
  observedSessionRevision;
51
55
  eventUnsubscribe;
56
+ ephemeralUnsubscribe;
52
57
  pendingUserMessagesUnsubscribe;
53
58
  snapshotRecovery;
54
59
  snapshotRecoveryDeltas = [];
55
60
  hasPendingUserMessages = false;
56
61
  pendingIdleNotification = false;
57
62
  isStreaming = false;
63
+ assistantInterruptRequested = false;
58
64
  submittedTurnInProgress = false;
59
65
  manualCompactionInProgress = false;
60
66
  localDiffReviewInProgress = false;
@@ -66,11 +72,10 @@ export class SessionChatController {
66
72
  lastTurnDurationMs = 0;
67
73
  turnTimer;
68
74
  lastEmptySubmitAt;
69
- assistantMessages = [];
70
75
  listenRecording;
71
76
  listenTransition;
72
77
  isTranscribingListen = false;
73
- speechStatusHint;
78
+ speechActivityLabel;
74
79
  caffeinated;
75
80
  turnCaffeinate;
76
81
  disableCaffeinateForSession = false;
@@ -107,7 +112,7 @@ export class SessionChatController {
107
112
  persona: (id) => this.setPersona(id),
108
113
  prompt: (id) => this.insertPrompt(id),
109
114
  theme: (id) => this.switchTheme(id),
110
- unknown: () => this.view.addSystemMessage("unknown command. type /help.", "error"),
115
+ unknown: () => this.view.addTranscriptNotice("unknown command", "error", ["type /help for commands"]),
111
116
  };
112
117
  this.diffReviewService = new DiffReviewService({
113
118
  view: this.view,
@@ -130,11 +135,13 @@ export class SessionChatController {
130
135
  this.renderSnapshot(this.snapshot);
131
136
  this.hydrateRunningSnapshotState();
132
137
  this.eventUnsubscribe = this.session.onDelta((delta) => this.onSdkDelta(delta));
138
+ this.ephemeralUnsubscribe = this.session.onEphemeral((message) => this.onSdkEphemeral(message));
133
139
  this.pendingUserMessagesUnsubscribe = this.session.onPendingUserMessages((message) => this.onSdkPendingUserMessages(message));
134
140
  this.refreshStatus();
135
141
  }
136
142
  async dispose() {
137
143
  this.eventUnsubscribe?.();
144
+ this.ephemeralUnsubscribe?.();
138
145
  this.pendingUserMessagesUnsubscribe?.();
139
146
  if (this.listenTransition) {
140
147
  await this.listenTransition;
@@ -216,7 +223,7 @@ export class SessionChatController {
216
223
  this.lastEmptySubmitAt = undefined;
217
224
  if (this.isSessionOperationActive()) {
218
225
  if (this.isBlockingSessionOperationActive()) {
219
- this.view.addSystemMessage("wait for tau to become idle before submitting input", "warn");
226
+ this.view.showFooterNotice("wait for tau to become idle before submitting input", "default");
220
227
  return;
221
228
  }
222
229
  if (trimmed.startsWith("/") && this.isSingleLineInput(text)) {
@@ -226,7 +233,7 @@ export class SessionChatController {
226
233
  await this.commandRegistry.dispatch(parsed, this.commandHandlers);
227
234
  }
228
235
  else {
229
- this.view.addSystemMessage("wait for tau to become idle before running commands", "warn");
236
+ this.view.showFooterNotice("wait for tau to become idle before running commands", "default");
230
237
  }
231
238
  return;
232
239
  }
@@ -270,16 +277,19 @@ export class SessionChatController {
270
277
  await this.handleSubmit(text);
271
278
  }
272
279
  showHelp() {
273
- this.view.addSystemMessage(this.commandRegistry.buildHelpText({
274
- agentsFiles: this.getAgentsFilePaths(),
275
- skills: this.snapshot.catalog.skills,
276
- themes: this.themeIds,
277
- formatPath: (path) => formatPathForSessionDisplay(path, this.snapshot.executionEnvironment.home),
278
- }), "muted");
280
+ this.view.addMessage({
281
+ type: "transcript_text",
282
+ text: this.commandRegistry.buildHelpText({
283
+ agentsFiles: this.getAgentsFilePaths(),
284
+ skills: this.snapshot.catalog.skills,
285
+ themes: this.themeIds,
286
+ formatPath: (path) => formatPathForSessionDisplay(path, this.snapshot.executionEnvironment.home),
287
+ }),
288
+ });
279
289
  }
280
290
  switchTheme(themeId) {
281
291
  this.view.updateTheme(themeId);
282
- this.view.addSystemMessage(`theme set to ${themeId}`, "success");
292
+ this.view.showFooterNotice(`theme set to ${themeId}`, "default");
283
293
  }
284
294
  handleEditorChange(text) {
285
295
  const wasBash = this.isBashMode;
@@ -397,7 +407,7 @@ export class SessionChatController {
397
407
  });
398
408
  if (result.userHistoryEntryId) {
399
409
  this.hiddenHistoryEntryIds.add(result.userHistoryEntryId);
400
- this.renderedMessageIds.push(result.userHistoryEntryId);
410
+ this.renderedMessageIds.push(this.getViewMessageId(result.userHistoryEntryId));
401
411
  }
402
412
  this.view.updateLocalToolUi({
403
413
  toolCallId,
@@ -438,7 +448,7 @@ export class SessionChatController {
438
448
  return;
439
449
  }
440
450
  if (this.isBlockingSessionOperationActive()) {
441
- this.view.addSystemMessage("wait for tau to become idle before submitting input", "warn");
451
+ this.view.showFooterNotice("wait for tau to become idle before submitting input", "default");
442
452
  return;
443
453
  }
444
454
  this.submitSteeringText(trimmed);
@@ -446,14 +456,18 @@ export class SessionChatController {
446
456
  submitQueuedText(text) {
447
457
  void this.session.queue(text).catch((error) => {
448
458
  if (!this.isPendingMessageCancellation(error)) {
449
- this.view.addSystemMessage(`queueing failed: ${formatSessionError(error)}`, "error");
459
+ this.view.addTranscriptNotice("failed to queue message", "error", [
460
+ formatSessionError(error),
461
+ ]);
450
462
  }
451
463
  });
452
464
  }
453
465
  submitSteeringText(text) {
454
466
  void this.session.steer(text).catch((error) => {
455
467
  if (!this.isPendingMessageCancellation(error)) {
456
- this.view.addSystemMessage(`steering failed: ${formatSessionError(error)}`, "error");
468
+ this.view.addTranscriptNotice("failed to steer assistant", "error", [
469
+ formatSessionError(error),
470
+ ]);
457
471
  }
458
472
  });
459
473
  }
@@ -468,7 +482,9 @@ export class SessionChatController {
468
482
  this.view.requestRender();
469
483
  }
470
484
  catch (error) {
471
- this.view.addSystemMessage(`pending message cancellation failed: ${error.message}`, "error");
485
+ this.view.addTranscriptNotice("failed to cancel pending messages", "error", [
486
+ error.message,
487
+ ]);
472
488
  }
473
489
  }
474
490
  isPendingMessageCancellation(error) {
@@ -513,7 +529,9 @@ export class SessionChatController {
513
529
  return;
514
530
  }
515
531
  this.disableCaffeinateForSession = true;
516
- this.view.addSystemMessage(`failed to run caffeinate: ${error.message}`, "warn");
532
+ this.view.addTranscriptNotice("caffeinate unavailable", "default", [
533
+ error.message,
534
+ ]);
517
535
  }
518
536
  }
519
537
  async runSessionTurn(task) {
@@ -521,23 +539,20 @@ export class SessionChatController {
521
539
  return;
522
540
  }
523
541
  this.isStreaming = true;
542
+ this.assistantInterruptRequested = false;
524
543
  this.submittedTurnInProgress = true;
525
544
  this.startTurnTimer();
526
545
  this.startTurnCaffeinate();
527
546
  this.view.startWorkingIcon();
528
547
  this.refreshStatus();
529
548
  try {
530
- const result = await task();
531
- if (result.turn.status === "blocked") {
532
- this.view.addSystemMessage(`turn blocked: ${result.turn.message}`, "error");
533
- }
534
- else if (result.turn.status === "failed") {
535
- this.view.addSystemMessage(`turn failed: ${result.turn.errorMessage ?? "model provider returned an error"}`, "error");
536
- }
549
+ await task();
537
550
  await this.syncFromSessionSnapshot();
538
551
  }
539
552
  catch (error) {
540
- this.view.addSystemMessage(`session turn failed: ${formatSessionError(error)}`, "error");
553
+ this.view.addTranscriptNotice("failed to run assistant turn", "error", [
554
+ formatSessionError(error),
555
+ ]);
541
556
  this.stopVisibleSessionTurn();
542
557
  void this.stopTurnCaffeinate();
543
558
  await this.syncFromSessionSnapshot();
@@ -551,7 +566,7 @@ export class SessionChatController {
551
566
  }
552
567
  async interrupt() {
553
568
  if (this.interruptLifecycle.interruptActiveTask()) {
554
- this.view.addSystemMessage("interrupted", "error");
569
+ this.view.showFooterNotice("interrupted", "default");
555
570
  return;
556
571
  }
557
572
  if (this.listenRecording) {
@@ -562,23 +577,29 @@ export class SessionChatController {
562
577
  if (!this.speakTask.abortController.signal.aborted) {
563
578
  this.speakTask.abortController.abort();
564
579
  }
565
- this.view.addSystemMessage("interrupted", "error");
580
+ this.view.showFooterNotice("interrupted", "default");
566
581
  return;
567
582
  }
568
- if (!this.isStreaming) {
583
+ if (!this.isStreaming || this.assistantInterruptRequested) {
569
584
  return;
570
585
  }
586
+ this.assistantInterruptRequested = true;
571
587
  try {
572
588
  const result = await this.session.interrupt();
573
- this.view.addSystemMessage(result.interrupted ? "interrupted" : "interrupt requested", "error");
589
+ if (!result.interrupted) {
590
+ this.assistantInterruptRequested = false;
591
+ }
574
592
  }
575
593
  catch (error) {
576
- this.view.addSystemMessage(`interrupt failed: ${error.message}`, "error");
594
+ this.assistantInterruptRequested = false;
595
+ this.view.addTranscriptNotice("failed to interrupt assistant", "error", [
596
+ error.message,
597
+ ]);
577
598
  }
578
599
  }
579
600
  async toggleListenCapture() {
580
601
  if (this.listenTransition) {
581
- this.view.addSystemMessage("speech recording state change already in progress", "warn");
602
+ this.view.showFooterNotice("speech recording state change already in progress", "default");
582
603
  return;
583
604
  }
584
605
  if (this.listenRecording) {
@@ -589,15 +610,15 @@ export class SessionChatController {
589
610
  }
590
611
  async startListenCaptureFromCommand() {
591
612
  if (this.listenTransition) {
592
- this.view.addSystemMessage("speech recording state change already in progress", "warn");
613
+ this.view.showFooterNotice("speech recording state change already in progress", "default");
593
614
  return;
594
615
  }
595
616
  if (this.listenRecording) {
596
- this.view.addSystemMessage("speech recording already in progress", "warn");
617
+ this.view.showFooterNotice("speech recording already in progress", "default");
597
618
  return;
598
619
  }
599
620
  if (this.isTranscribingListen) {
600
- this.view.addSystemMessage("speech transcription already in progress", "warn");
621
+ this.view.showFooterNotice("speech transcription already in progress", "default");
601
622
  return;
602
623
  }
603
624
  await this.runListenTransition(() => this.startListenCapture());
@@ -619,12 +640,14 @@ export class SessionChatController {
619
640
  }
620
641
  async startListenCapture() {
621
642
  if (this.deps.env.platform() !== "darwin") {
622
- this.view.addSystemMessage("/listen is currently supported only on macOS.", "warn");
643
+ this.view.showFooterNotice("/listen is currently supported only on macOS.", "default");
623
644
  return;
624
645
  }
625
646
  const apiKey = getSpeechToTextApiKey(this.config, this.deps);
626
647
  if (!apiKey) {
627
- this.view.addSystemMessage(getSpeechToTextApiKeyErrorMessage(this.config, "use /listen"), "error");
648
+ this.view.addTranscriptNotice("speech-to-text is not configured", "error", [
649
+ getSpeechToTextApiKeyErrorMessage(this.config, "use /listen"),
650
+ ]);
628
651
  return;
629
652
  }
630
653
  let audioPath;
@@ -656,7 +679,7 @@ export class SessionChatController {
656
679
  if (audioPath) {
657
680
  await cleanupListenTempFile(audioPath);
658
681
  }
659
- this.view.addSystemMessage(`failed to start recording: ${err.message}`, "error");
682
+ this.view.addTranscriptNotice("failed to start recording", "error", [err.message]);
660
683
  }
661
684
  }
662
685
  async stopListenCapture() {
@@ -673,15 +696,16 @@ export class SessionChatController {
673
696
  await recording.completion;
674
697
  }
675
698
  catch (err) {
676
- this.view.addSystemMessage(`recording failed: ${err.message}`, "error");
699
+ this.view.addTranscriptNotice("failed to record audio", "error", [err.message]);
677
700
  await cleanupListenTempFile(recording.audioPath);
678
701
  return;
679
702
  }
680
703
  this.isTranscribingListen = true;
704
+ this.refreshStatus();
681
705
  try {
682
706
  const audio = await readListenAudio(recording.audioPath);
683
707
  if (audio.byteLength < LISTEN_RECORDING_MIN_BYTES) {
684
- this.view.addSystemMessage("recording too short, try again", "warn");
708
+ this.view.showFooterNotice("recording too short, try again", "default");
685
709
  return;
686
710
  }
687
711
  const transcript = await transcribeListenAudio({
@@ -697,10 +721,13 @@ export class SessionChatController {
697
721
  this.view.insertEditorTextAtCursor(text);
698
722
  }
699
723
  catch (err) {
700
- this.view.addSystemMessage(`speech transcription failed: ${err.message}`, "error");
724
+ this.view.addTranscriptNotice("failed to transcribe speech", "error", [
725
+ err.message,
726
+ ]);
701
727
  }
702
728
  finally {
703
729
  this.isTranscribingListen = false;
730
+ this.refreshStatus();
704
731
  await cleanupListenTempFile(recording.audioPath);
705
732
  }
706
733
  }
@@ -736,7 +763,7 @@ export class SessionChatController {
736
763
  : result.closeSignal
737
764
  ? `ffmpeg terminated by signal ${result.closeSignal}`
738
765
  : "ffmpeg exited";
739
- this.view.addSystemMessage(`recording stopped unexpectedly (${detail})`, "error");
766
+ this.view.addTranscriptNotice("recording stopped unexpectedly", "error", [detail]);
740
767
  await cleanupListenTempFile(recording.audioPath);
741
768
  }
742
769
  catch (err) {
@@ -748,10 +775,12 @@ export class SessionChatController {
748
775
  this.refreshStatus();
749
776
  const error = err;
750
777
  if (error.code === "ENOENT") {
751
- this.view.addSystemMessage("ffmpeg not found. install it with: brew install ffmpeg", "error");
778
+ this.view.addTranscriptNotice("ffmpeg not found", "error", [
779
+ "install it with: brew install ffmpeg",
780
+ ]);
752
781
  }
753
782
  else {
754
- this.view.addSystemMessage(`recording failed: ${error.message}`, "error");
783
+ this.view.addTranscriptNotice("failed to record audio", "error", [error.message]);
755
784
  }
756
785
  await cleanupListenTempFile(recording.audioPath);
757
786
  }
@@ -764,11 +793,11 @@ export class SessionChatController {
764
793
  }
765
794
  async createNewSession() {
766
795
  if (this.isSessionOperationActive()) {
767
- this.view.addSystemMessage("cannot create a new session while another session operation is running", "warn");
796
+ this.view.showFooterNotice("cannot create a new session while another session operation is running", "default");
768
797
  return;
769
798
  }
770
799
  if (!this.createSession) {
771
- this.view.addSystemMessage("new session creation is unavailable", "error");
800
+ this.view.addTranscriptNotice("cannot create a new session", "error");
772
801
  return;
773
802
  }
774
803
  const createInput = {
@@ -788,12 +817,14 @@ export class SessionChatController {
788
817
  this.refreshStatus();
789
818
  let nextSession;
790
819
  let nextEventUnsubscribe;
820
+ let nextEphemeralUnsubscribe;
791
821
  let nextPendingUserMessagesUnsubscribe;
792
822
  let installed = false;
793
823
  try {
794
824
  nextSession = await this.createSession(createInput);
795
825
  const nextSnapshot = await nextSession.snapshot();
796
826
  const pendingDeltas = [];
827
+ const pendingEphemeralMessages = [];
797
828
  const pendingUserMessages = [];
798
829
  let forwardEvents = false;
799
830
  nextEventUnsubscribe = nextSession.onDelta((delta) => {
@@ -804,6 +835,14 @@ export class SessionChatController {
804
835
  pendingDeltas.push(delta);
805
836
  }
806
837
  });
838
+ nextEphemeralUnsubscribe = nextSession.onEphemeral((message) => {
839
+ if (forwardEvents) {
840
+ this.onSdkEphemeral(message);
841
+ }
842
+ else {
843
+ pendingEphemeralMessages.push(message);
844
+ }
845
+ });
807
846
  nextPendingUserMessagesUnsubscribe = nextSession.onPendingUserMessages((message) => {
808
847
  if (forwardEvents) {
809
848
  this.onSdkPendingUserMessages(message);
@@ -814,21 +853,25 @@ export class SessionChatController {
814
853
  });
815
854
  const previousSession = this.session;
816
855
  this.eventUnsubscribe?.();
856
+ this.ephemeralUnsubscribe?.();
817
857
  this.pendingUserMessagesUnsubscribe?.();
818
858
  this.session = nextSession;
819
859
  this.snapshot = nextSnapshot;
820
860
  this.observedSessionRevision = nextSnapshot.revision;
821
861
  this.eventUnsubscribe = nextEventUnsubscribe;
862
+ this.ephemeralUnsubscribe = nextEphemeralUnsubscribe;
822
863
  this.pendingUserMessagesUnsubscribe = nextPendingUserMessagesUnsubscribe;
823
864
  installed = true;
824
865
  this.view.resetToolUiSession();
825
- this.assistantMessages = [];
826
866
  this.startLocalUiSession();
827
867
  this.addSessionIdentityMessage();
828
868
  this.renderSnapshot(this.snapshot);
829
869
  for (const delta of pendingDeltas) {
830
870
  this.onSdkDelta(delta);
831
871
  }
872
+ for (const message of pendingEphemeralMessages) {
873
+ this.onSdkEphemeral(message);
874
+ }
832
875
  for (const message of pendingUserMessages) {
833
876
  this.onSdkPendingUserMessages(message);
834
877
  }
@@ -837,16 +880,21 @@ export class SessionChatController {
837
880
  await previousSession.unobserve();
838
881
  }
839
882
  catch (detachError) {
840
- this.view.addSystemMessage(`old session unobserve failed: ${detachError.message}`, "warn");
883
+ this.view.addTranscriptNotice("failed to detach previous session", "error", [
884
+ detachError.message,
885
+ ]);
841
886
  }
842
887
  }
843
888
  catch (error) {
844
889
  if (!installed && nextSession) {
845
890
  nextEventUnsubscribe?.();
891
+ nextEphemeralUnsubscribe?.();
846
892
  nextPendingUserMessagesUnsubscribe?.();
847
893
  await nextSession.unobserve().catch(() => undefined);
848
894
  }
849
- this.view.addSystemMessage(`new session failed: ${error.message}`, "error");
895
+ this.view.addTranscriptNotice("failed to create session", "error", [
896
+ error.message,
897
+ ]);
850
898
  }
851
899
  finally {
852
900
  this.sessionReplacementInProgress = false;
@@ -879,6 +927,23 @@ export class SessionChatController {
879
927
  this.view.setPendingUserMessages(message.state.messages);
880
928
  this.sendPendingIdleNotification();
881
929
  }
930
+ onSdkEphemeral(message) {
931
+ const event = message.event;
932
+ switch (event.type) {
933
+ case "feedback.notice":
934
+ this.view.showFooterNotice(event.title, event.tone, event.durationMs);
935
+ return;
936
+ case "timeline.item":
937
+ if (event.epoch !== this.snapshot.timeline.epoch) {
938
+ return;
939
+ }
940
+ this.ephemeralTimelineItems.set(event.item.id, event.item);
941
+ this.syncRenderedHistory(this.snapshot);
942
+ return;
943
+ case "ephemeral-agent.thread-update":
944
+ return;
945
+ }
946
+ }
882
947
  onSdkDelta(delta) {
883
948
  if (this.snapshotRecovery) {
884
949
  this.snapshotRecoveryDeltas.push(delta);
@@ -891,10 +956,7 @@ export class SessionChatController {
891
956
  void this.recoverFromRevisionGap();
892
957
  }
893
958
  tryApplySdkDelta(delta) {
894
- if (delta.delta.type === "snapshot.reset" && delta.toRevision < this.snapshot.revision) {
895
- return true;
896
- }
897
- if (delta.delta.type === "snapshot.patch" && delta.toRevision <= this.snapshot.revision) {
959
+ if (delta.toRevision <= this.snapshot.revision) {
898
960
  return true;
899
961
  }
900
962
  if (delta.delta.type === "snapshot.patch" && delta.fromRevision !== this.snapshot.revision) {
@@ -911,16 +973,28 @@ export class SessionChatController {
911
973
  return true;
912
974
  }
913
975
  const nextSnapshot = applySessionProtocolDelta(this.snapshot, delta);
914
- if (this.shouldRenderAutoCompactedReset(delta)) {
915
- this.renderAutoCompactedReset(nextSnapshot);
976
+ if (delta.cause.type === "compaction" && delta.delta.type === "snapshot.reset") {
977
+ this.renderCompactedReset(nextSnapshot, delta.cause);
916
978
  }
917
979
  else {
980
+ if (delta.cause.type === "rewind") {
981
+ for (const [id, item] of this.ephemeralTimelineItems) {
982
+ if (item.sequence > delta.cause.cutoffSequence) {
983
+ this.ephemeralTimelineItems.delete(id);
984
+ }
985
+ }
986
+ }
987
+ else if (nextSnapshot.timeline.epoch !== this.snapshot.timeline.epoch) {
988
+ this.ephemeralTimelineItems.clear();
989
+ }
918
990
  this.syncRenderedHistory(nextSnapshot);
919
991
  }
920
992
  this.refreshStatus();
921
993
  }
922
994
  catch (error) {
923
- this.view.addSystemMessage(`session delta failed: ${error.message}`, "warn");
995
+ this.view.addTranscriptNotice("failed to update session", "error", [
996
+ error.message,
997
+ ]);
924
998
  return false;
925
999
  }
926
1000
  return true;
@@ -939,6 +1013,7 @@ export class SessionChatController {
939
1013
  const message = nextSnapshot.messages.find((entry) => entry.id === change.messageId);
940
1014
  if (!message ||
941
1015
  this.hiddenHistoryEntryIds.has(message.id) ||
1016
+ !this.isMessageProjected(message.id) ||
942
1017
  !isMessageInTimeline(nextSnapshot, message.id)) {
943
1018
  return true;
944
1019
  }
@@ -946,16 +1021,17 @@ export class SessionChatController {
946
1021
  if (!model) {
947
1022
  return true;
948
1023
  }
949
- if (this.renderedMessageIds.includes(message.id)) {
1024
+ const viewMessageId = this.getViewMessageId(message.id);
1025
+ if (this.renderedMessageIds.includes(viewMessageId)) {
950
1026
  if (model.type === "assistant" || model.type === "assistant_partial") {
951
- this.view.updateAssistantMessage(message.id, model);
1027
+ this.view.updateAssistantMessage(viewMessageId, model);
952
1028
  }
953
1029
  else {
954
- this.view.updateMessage(message.id, model);
1030
+ this.view.updateMessage(viewMessageId, model);
955
1031
  }
956
1032
  }
957
1033
  else {
958
- this.renderedMessageIds.push(this.view.addMessage(model, message.id));
1034
+ this.renderedMessageIds.push(this.view.addMessage(model, viewMessageId));
959
1035
  }
960
1036
  return true;
961
1037
  }
@@ -988,26 +1064,45 @@ export class SessionChatController {
988
1064
  if (facetChanges.some(({ facet }) => facet.kind !== "tau.tool-ui-events" || facet.subject.type !== "tool")) {
989
1065
  return false;
990
1066
  }
991
- this.snapshot = applySessionProtocolDelta(this.snapshot, delta);
992
- this.observedSessionRevision = Math.max(this.observedSessionRevision, this.snapshot.revision);
993
- this.view.reconcileToolUiSession(getToolUiModelsInModelOrder(this.snapshot));
1067
+ const nextSnapshot = applySessionProtocolDelta(this.snapshot, delta);
1068
+ this.syncRenderedHistory(nextSnapshot);
994
1069
  this.refreshStatus();
995
1070
  return true;
996
1071
  }
1072
+ getViewMessageId(protocolId) {
1073
+ const existingId = this.viewMessageIds.get(protocolId);
1074
+ if (existingId)
1075
+ return existingId;
1076
+ let viewId = protocolId;
1077
+ if (this.usedViewMessageIds.has(viewId)) {
1078
+ const baseId = `timeline-segment:${this.renderSegment}:${protocolId}`;
1079
+ viewId = baseId;
1080
+ for (let suffix = 2; this.usedViewMessageIds.has(viewId); suffix += 1) {
1081
+ viewId = `${baseId}:${suffix}`;
1082
+ }
1083
+ }
1084
+ this.viewMessageIds.set(protocolId, viewId);
1085
+ this.usedViewMessageIds.add(viewId);
1086
+ return viewId;
1087
+ }
997
1088
  renderSnapshot(snapshot) {
998
1089
  this.snapshot = snapshot;
999
1090
  this.observedSessionRevision = Math.max(this.observedSessionRevision, snapshot.revision);
1000
- this.assistantMessages = [];
1001
- for (const item of getRenderableTimelineItems(snapshot)) {
1091
+ for (const item of getRenderableTimelineItems(snapshot, this.ephemeralTimelineItems.values())) {
1092
+ const viewMessageId = this.getViewMessageId(item.id);
1002
1093
  if (item.type === "notice") {
1003
1094
  this.renderedMessageIds.push(this.view.addMessage({
1004
- type: "system",
1005
- text: item.text,
1006
- kind: item.severity === "info" ? "muted" : item.severity,
1007
- }, item.id));
1095
+ type: "transcript_notice",
1096
+ title: item.title,
1097
+ ...(item.content ? { content: item.content } : {}),
1098
+ tone: item.severity === "error" ? "error" : "default",
1099
+ }, viewMessageId));
1100
+ }
1101
+ else if (item.type === "tool") {
1102
+ this.renderedMessageIds.push(this.view.addMessage({ type: "tool", tool: item.model }, viewMessageId));
1008
1103
  }
1009
1104
  else {
1010
- this.renderProtocolMessage(item.message);
1105
+ this.renderProtocolMessage(item.message, viewMessageId);
1011
1106
  }
1012
1107
  }
1013
1108
  this.syncSnapshotToolAndAgentUi(snapshot);
@@ -1020,21 +1115,18 @@ export class SessionChatController {
1020
1115
  this.startTurnTimer();
1021
1116
  this.view.startWorkingIcon();
1022
1117
  }
1023
- renderProtocolMessage(message) {
1118
+ renderProtocolMessage(message, viewMessageId) {
1024
1119
  const model = this.buildProtocolMessageModel(message);
1025
1120
  if (!model) {
1026
1121
  return;
1027
1122
  }
1028
- if (isAssistantMessage(message.message)) {
1029
- this.assistantMessages.push(message.message);
1030
- }
1031
- this.renderedMessageIds.push(this.view.addMessage(model, message.id));
1123
+ this.renderedMessageIds.push(this.view.addMessage(model, viewMessageId));
1032
1124
  }
1033
1125
  syncRenderedHistory(snapshot) {
1034
1126
  this.snapshot = snapshot;
1035
1127
  this.observedSessionRevision = Math.max(this.observedSessionRevision, snapshot.revision);
1036
- const items = getRenderableTimelineItems(snapshot);
1037
- const snapshotIds = new Set(items.map((item) => item.id));
1128
+ const items = getRenderableTimelineItems(snapshot, this.ephemeralTimelineItems.values());
1129
+ const snapshotIds = new Set(items.map((item) => this.getViewMessageId(item.id)));
1038
1130
  const staleIds = this.renderedMessageIds.filter((id) => !snapshotIds.has(id));
1039
1131
  if (staleIds.length > 0) {
1040
1132
  this.view.removeMessages(staleIds);
@@ -1042,35 +1134,38 @@ export class SessionChatController {
1042
1134
  const renderedIds = new Set(this.renderedMessageIds);
1043
1135
  this.renderedMessageIds.splice(0, this.renderedMessageIds.length, ...this.renderedMessageIds.filter((id) => snapshotIds.has(id)));
1044
1136
  for (const item of items) {
1137
+ const viewMessageId = this.getViewMessageId(item.id);
1045
1138
  if (item.type === "message" && this.hiddenHistoryEntryIds.has(item.id)) {
1046
- if (!renderedIds.has(item.id)) {
1047
- this.renderedMessageIds.push(item.id);
1139
+ if (!renderedIds.has(viewMessageId)) {
1140
+ this.renderedMessageIds.push(viewMessageId);
1048
1141
  }
1049
1142
  continue;
1050
1143
  }
1051
1144
  const model = item.type === "notice"
1052
1145
  ? {
1053
- type: "system",
1054
- text: item.text,
1055
- kind: item.severity === "info" ? "muted" : item.severity,
1146
+ type: "transcript_notice",
1147
+ title: item.title,
1148
+ ...(item.content ? { content: item.content } : {}),
1149
+ tone: item.severity === "error" ? "error" : "default",
1056
1150
  }
1057
- : this.buildProtocolMessageModel(item.message);
1151
+ : item.type === "tool"
1152
+ ? { type: "tool", tool: item.model }
1153
+ : this.buildProtocolMessageModel(item.message);
1058
1154
  if (!model) {
1059
1155
  continue;
1060
1156
  }
1061
- if (renderedIds.has(item.id)) {
1157
+ if (renderedIds.has(viewMessageId)) {
1062
1158
  if (model.type === "assistant") {
1063
- this.view.updateAssistantMessage(item.id, model);
1159
+ this.view.updateAssistantMessage(viewMessageId, model);
1064
1160
  }
1065
1161
  else {
1066
- this.view.updateMessage(item.id, model);
1162
+ this.view.updateMessage(viewMessageId, model);
1067
1163
  }
1068
1164
  }
1069
1165
  else {
1070
- this.renderedMessageIds.push(this.view.addMessage(model, item.id));
1166
+ this.renderedMessageIds.push(this.view.addMessage(model, viewMessageId));
1071
1167
  }
1072
1168
  }
1073
- this.assistantMessages = this.collectAssistantMessages(snapshot);
1074
1169
  this.updateStreamingStateFromSnapshot(snapshot);
1075
1170
  this.syncSnapshotToolAndAgentUi(snapshot);
1076
1171
  }
@@ -1105,30 +1200,34 @@ export class SessionChatController {
1105
1200
  }
1106
1201
  this.finishObservedSessionTurn();
1107
1202
  }
1203
+ isMessageProjected(messageId) {
1204
+ return isMessageInTimeline(this.snapshot, messageId);
1205
+ }
1108
1206
  syncSnapshotToolAndAgentUi(snapshot) {
1109
- this.view.reconcileToolUiSession(getToolUiModelsInModelOrder(snapshot));
1110
1207
  this.view.reconcileSubagentUiSession(Object.values(snapshot.agents).map((agent) => ({
1111
1208
  state: structuredClone(agent),
1112
1209
  activity: getSubagentActivity(snapshot, agent.id),
1113
1210
  })));
1114
1211
  }
1115
- collectAssistantMessages(snapshot) {
1116
- const messages = [];
1117
- for (const entry of snapshot.messages) {
1118
- if (isAssistantMessage(entry.message)) {
1119
- messages.push(entry.message);
1120
- }
1121
- }
1122
- return messages;
1123
- }
1124
- async syncFromSessionSnapshot() {
1212
+ async syncFromSessionSnapshot(ephemeralTimelineItemIdsToDiscard) {
1125
1213
  try {
1126
- this.syncRenderedHistory(await this.session.snapshot());
1214
+ const snapshot = await this.session.snapshot();
1215
+ for (const id of ephemeralTimelineItemIdsToDiscard ?? []) {
1216
+ this.ephemeralTimelineItems.delete(id);
1217
+ }
1218
+ if (snapshot.timeline.epoch > this.snapshot.timeline.epoch) {
1219
+ this.renderCompactedSnapshot(snapshot);
1220
+ }
1221
+ else {
1222
+ this.syncRenderedHistory(snapshot);
1223
+ }
1127
1224
  this.refreshStatus();
1128
1225
  return true;
1129
1226
  }
1130
1227
  catch (error) {
1131
- this.view.addSystemMessage(`snapshot refresh failed: ${error.message}`, "warn");
1228
+ this.view.addTranscriptNotice("failed to refresh session", "error", [
1229
+ error.message,
1230
+ ]);
1132
1231
  return false;
1133
1232
  }
1134
1233
  }
@@ -1152,6 +1251,7 @@ export class SessionChatController {
1152
1251
  return false;
1153
1252
  }
1154
1253
  this.isStreaming = false;
1254
+ this.assistantInterruptRequested = false;
1155
1255
  this.view.stopWorkingIcon();
1156
1256
  this.stopTurnTimer();
1157
1257
  this.refreshStatus();
@@ -1162,6 +1262,7 @@ export class SessionChatController {
1162
1262
  return;
1163
1263
  }
1164
1264
  this.isStreaming = true;
1265
+ this.assistantInterruptRequested = false;
1165
1266
  this.startTurnTimer();
1166
1267
  this.startTurnCaffeinate();
1167
1268
  this.view.startWorkingIcon();
@@ -1179,7 +1280,7 @@ export class SessionChatController {
1179
1280
  if (this.snapshotRecovery) {
1180
1281
  return;
1181
1282
  }
1182
- this.snapshotRecovery = this.syncFromSessionSnapshot()
1283
+ this.snapshotRecovery = this.syncFromSessionSnapshotAfterRevisionGap()
1183
1284
  .then((synced) => {
1184
1285
  if (synced) {
1185
1286
  return this.replaySnapshotRecoveryDeltas();
@@ -1190,6 +1291,9 @@ export class SessionChatController {
1190
1291
  });
1191
1292
  await this.snapshotRecovery;
1192
1293
  }
1294
+ syncFromSessionSnapshotAfterRevisionGap() {
1295
+ return this.syncFromSessionSnapshot(new Set(this.ephemeralTimelineItems.keys()));
1296
+ }
1193
1297
  async replaySnapshotRecoveryDeltas() {
1194
1298
  while (this.snapshotRecoveryDeltas.length > 0) {
1195
1299
  const deltas = this.snapshotRecoveryDeltas.splice(0);
@@ -1198,7 +1302,7 @@ export class SessionChatController {
1198
1302
  if (this.tryApplySdkDelta(delta)) {
1199
1303
  continue;
1200
1304
  }
1201
- if (!(await this.syncFromSessionSnapshot())) {
1305
+ if (!(await this.syncFromSessionSnapshotAfterRevisionGap())) {
1202
1306
  this.snapshotRecoveryDeltas.unshift(delta, ...deltas.slice(index + 1));
1203
1307
  return;
1204
1308
  }
@@ -1206,44 +1310,46 @@ export class SessionChatController {
1206
1310
  }
1207
1311
  }
1208
1312
  startLocalUiSession() {
1313
+ this.renderSegment += 1;
1314
+ this.viewMessageIds.clear();
1315
+ this.ephemeralTimelineItems.clear();
1209
1316
  this.hiddenHistoryEntryIds.clear();
1210
1317
  this.renderedMessageIds.splice(0);
1211
1318
  this.view.addMessage({ type: "session_divider", label: "new session" });
1212
1319
  }
1320
+ startCompactedUiSession() {
1321
+ this.renderSegment += 1;
1322
+ this.viewMessageIds.clear();
1323
+ this.ephemeralTimelineItems.clear();
1324
+ this.hiddenHistoryEntryIds.clear();
1325
+ this.renderedMessageIds.splice(0);
1326
+ this.view.addMessage({ type: "session_divider", label: "compacted context" });
1327
+ }
1213
1328
  addSessionIdentityMessage() {
1214
- this.view.addSystemMessage(this.formatSessionIdentityText(), "muted");
1329
+ this.view.addTranscriptNotice("session id", "default", [this.snapshot.sessionId]);
1215
1330
  }
1216
1331
  renderCompactedSnapshot(snapshot) {
1217
- this.startLocalUiSession();
1332
+ this.view.resetToolUiSessionPreservingSubagents();
1333
+ this.startCompactedUiSession();
1218
1334
  this.renderSnapshot(snapshot);
1219
1335
  }
1220
- shouldRenderAutoCompactedReset(delta) {
1221
- return (delta.reason === "maintenance" &&
1222
- delta.delta.type === "snapshot.reset" &&
1223
- this.hasRunningAutoCompactionOperation(this.snapshot));
1224
- }
1225
- renderAutoCompactedReset(snapshot) {
1336
+ renderCompactedReset(snapshot, cause) {
1226
1337
  this.view.resetToolUiSessionPreservingSubagents();
1227
- this.startLocalUiSession();
1338
+ this.startCompactedUiSession();
1228
1339
  this.renderSnapshot(snapshot);
1229
- const metadata = this.getAutoCompactionMetadata(snapshot);
1230
- if (metadata) {
1231
- this.view.addMessage({
1232
- type: "system",
1233
- text: formatAutoCompactionRetainedText(metadata),
1234
- kind: "muted",
1235
- });
1340
+ if (cause.kind === "auto") {
1341
+ this.view.addTranscriptNotice(formatAutoCompactionRetainedText(cause), "default");
1236
1342
  }
1237
1343
  }
1238
1344
  toggleThinkingVisibility() {
1239
1345
  this.showThinking = !this.showThinking;
1240
1346
  this.view.setThinkingVisibility(this.showThinking);
1241
- this.view.addSystemMessage(this.showThinking ? "thoughts visible" : "thoughts hidden", "success");
1347
+ this.view.showFooterNotice(this.showThinking ? "thoughts visible" : "thoughts hidden", "default");
1242
1348
  }
1243
1349
  async cyclePersonality() {
1244
1350
  const personas = this.snapshot.catalog.personas;
1245
1351
  if (personas.length === 0) {
1246
- this.view.addSystemMessage("no personas available.", "warn");
1352
+ this.view.showFooterNotice("no personas available.", "default");
1247
1353
  return;
1248
1354
  }
1249
1355
  const currentIndex = personas.findIndex((persona) => persona.id.toLowerCase() === this.snapshot.settings.personaId.toLowerCase());
@@ -1265,7 +1371,9 @@ export class SessionChatController {
1265
1371
  this.refreshStatus();
1266
1372
  }
1267
1373
  catch (error) {
1268
- this.view.addSystemMessage(`reasoning change failed: ${error.message}`, "error");
1374
+ this.view.addTranscriptNotice("failed to change reasoning", "error", [
1375
+ error.message,
1376
+ ]);
1269
1377
  }
1270
1378
  }
1271
1379
  getAllowedReasoningLevels(persona) {
@@ -1283,37 +1391,39 @@ export class SessionChatController {
1283
1391
  async setPersona(rawId) {
1284
1392
  const id = rawId.trim();
1285
1393
  if (!id) {
1286
- this.view.addSystemMessage("missing persona id", "error");
1394
+ this.view.addTranscriptNotice("missing persona id", "error");
1287
1395
  return;
1288
1396
  }
1289
1397
  const persona = this.snapshot.catalog.personas.find((candidate) => candidate.id.toLowerCase() === id.toLowerCase());
1290
1398
  if (!persona) {
1291
- this.view.addSystemMessage(`unknown persona '${id}'.`, "error");
1399
+ this.view.addTranscriptNotice("unknown persona", "error", [id]);
1292
1400
  return;
1293
1401
  }
1294
1402
  if (this.isSessionOperationActive()) {
1295
- this.view.addSystemMessage("cannot switch persona while a session turn is running", "warn");
1403
+ this.view.showFooterNotice("cannot switch persona while a session turn is running", "default");
1296
1404
  return;
1297
1405
  }
1298
1406
  try {
1299
1407
  this.snapshot = await this.session.setPersona(persona.id);
1300
1408
  this.syncRenderedHistory(this.snapshot);
1301
1409
  this.refreshStatus();
1302
- this.view.addSystemMessage(`switched to ${persona.label} (${this.snapshot.bootstrap.model.id})`, "success");
1410
+ this.view.showFooterNotice(`switched to ${persona.label} (${this.snapshot.bootstrap.model.id})`, "default");
1303
1411
  }
1304
1412
  catch (error) {
1305
- this.view.addSystemMessage(`persona switch failed: ${error.message}`, "error");
1413
+ this.view.addTranscriptNotice("failed to switch persona", "error", [
1414
+ error.message,
1415
+ ]);
1306
1416
  }
1307
1417
  }
1308
1418
  async insertPrompt(rawId) {
1309
1419
  const id = rawId.trim();
1310
1420
  if (!id) {
1311
- this.view.addSystemMessage("missing prompt id", "error");
1421
+ this.view.addTranscriptNotice("missing prompt id", "error");
1312
1422
  return;
1313
1423
  }
1314
1424
  const prompt = this.snapshot.catalog.prompts.find((candidate) => candidate.id.toLowerCase() === id.toLowerCase());
1315
1425
  if (!prompt) {
1316
- this.view.addSystemMessage(`unknown prompt '${id}'.`, "error");
1426
+ this.view.addTranscriptNotice("unknown prompt", "error", [id]);
1317
1427
  return;
1318
1428
  }
1319
1429
  try {
@@ -1321,12 +1431,12 @@ export class SessionChatController {
1321
1431
  this.view.setEditorText(resolved.text);
1322
1432
  }
1323
1433
  catch (error) {
1324
- this.view.addSystemMessage(`prompt load failed: ${error.message}`, "error");
1434
+ this.view.addTranscriptNotice("failed to load prompt", "error", [error.message]);
1325
1435
  }
1326
1436
  }
1327
1437
  async reloadContent() {
1328
1438
  if (this.isSessionOperationActive()) {
1329
- this.view.addSystemMessage("cannot reload while a session turn is running", "warn");
1439
+ this.view.showFooterNotice("cannot reload while a session turn is running", "default");
1330
1440
  return;
1331
1441
  }
1332
1442
  try {
@@ -1335,7 +1445,7 @@ export class SessionChatController {
1335
1445
  this.syncRenderedHistory(this.snapshot);
1336
1446
  this.refreshStatus();
1337
1447
  for (const warning of result.warnings) {
1338
- this.view.addSystemMessage(warning, "warn");
1448
+ this.view.addTranscriptNotice("configuration warning", "default", [warning]);
1339
1449
  }
1340
1450
  const summary = [
1341
1451
  `${result.counts.personas} personas`,
@@ -1343,35 +1453,33 @@ export class SessionChatController {
1343
1453
  `${result.counts.skills} skills`,
1344
1454
  ...formatAgentsMdReloadSummary(this.getAgentsFilePaths().length),
1345
1455
  ].join(", ");
1346
- this.view.addSystemMessage(`reloaded: ${summary}.`, "success");
1456
+ this.view.showFooterNotice(`reloaded: ${summary}.`, "default");
1347
1457
  }
1348
1458
  catch (error) {
1349
- this.view.addSystemMessage(`reload failed: ${error.message}`, "error");
1459
+ this.view.addTranscriptNotice("failed to reload session content", "error", [
1460
+ error.message,
1461
+ ]);
1350
1462
  }
1351
1463
  }
1352
1464
  async compactSession(mode, guidanceText) {
1353
1465
  if (this.isSessionOperationActive()) {
1354
- this.view.addSystemMessage("cannot compact while a session turn is running", "warn");
1466
+ this.view.showFooterNotice("cannot compact while a session turn is running", "default");
1355
1467
  return;
1356
1468
  }
1357
1469
  const guidance = guidanceText?.trim() ?? "";
1358
1470
  this.manualCompactionInProgress = true;
1359
1471
  this.refreshStatus();
1360
- this.view.addSystemMessage("summarizing session...", "success");
1361
1472
  try {
1362
1473
  const result = await this.session.compact(mode, {
1363
1474
  ...(guidance ? { guidance } : {}),
1364
1475
  });
1365
- this.renderCompactedSnapshot(result.snapshot);
1366
- const message = mode === "summary-and-last" && result.includedLastAssistant
1367
- ? "session compacted. previous context and last assistant message have been included."
1368
- : "session compacted. previous context has been summarized.";
1369
- this.view.addSystemMessage(message, "success");
1476
+ if (result.snapshot.revision > this.snapshot.revision) {
1477
+ this.renderCompactedSnapshot(result.snapshot);
1478
+ }
1370
1479
  }
1371
1480
  catch (error) {
1372
1481
  const message = error.message || "compaction failed";
1373
- const kind = message === "no conversation to compact." ? "warn" : "error";
1374
- this.view.addSystemMessage(kind === "warn" ? message : `compact failed: ${message}`, kind);
1482
+ this.view.addTranscriptNotice("failed to compact session", "error", [message]);
1375
1483
  }
1376
1484
  finally {
1377
1485
  this.manualCompactionInProgress = false;
@@ -1381,7 +1489,7 @@ export class SessionChatController {
1381
1489
  async handleGoalAction(action) {
1382
1490
  if (action.type === "show") {
1383
1491
  const goal = this.snapshot.goal;
1384
- this.view.addSystemMessage(goal ? `goal ${goal.status}: ${goal.objective}` : "no session goal", goal ? "success" : "muted");
1492
+ this.view.addTranscriptNotice(goal ? `goal ${goal.status}` : "no session goal", "default", goal ? [goal.objective] : undefined);
1385
1493
  return;
1386
1494
  }
1387
1495
  if (action.type === "start") {
@@ -1395,41 +1503,43 @@ export class SessionChatController {
1395
1503
  try {
1396
1504
  this.snapshot = await this.session.clearGoal();
1397
1505
  this.refreshStatus();
1398
- this.view.addSystemMessage("session goal cleared", "success");
1506
+ this.view.showFooterNotice("session goal cleared", "default");
1399
1507
  }
1400
1508
  catch (error) {
1401
- this.view.addSystemMessage(`goal clear failed: ${error.message}`, "error");
1509
+ this.view.addTranscriptNotice("failed to clear goal", "error", [error.message]);
1402
1510
  }
1403
1511
  }
1404
1512
  async speakLastAssistantMessage() {
1405
1513
  if (this.speakTask) {
1406
- this.view.addSystemMessage("speech playback already in progress", "warn");
1514
+ this.view.showFooterNotice("speech playback already in progress", "default");
1407
1515
  return;
1408
1516
  }
1409
1517
  if (this.isSessionOperationActive()) {
1410
- this.view.addSystemMessage("wait for the assistant to finish before speaking", "warn");
1518
+ this.view.showFooterNotice("wait for the assistant to finish before speaking", "default");
1411
1519
  return;
1412
1520
  }
1413
1521
  if (this.deps.env.platform() !== "darwin") {
1414
- this.view.addSystemMessage("/speak is currently supported only on macOS.", "warn");
1522
+ this.view.showFooterNotice("/speak is currently supported only on macOS.", "default");
1415
1523
  return;
1416
1524
  }
1417
1525
  const lastAssistant = this.getLastAssistantMessage();
1418
1526
  if (!lastAssistant) {
1419
- this.view.addSystemMessage("no assistant message to speak yet.", "warn");
1527
+ this.view.showFooterNotice("no assistant message to speak yet.", "default");
1420
1528
  return;
1421
1529
  }
1422
1530
  const sourceText = extractAssistantText(lastAssistant).trim();
1423
1531
  if (!sourceText) {
1424
- this.view.addSystemMessage("last assistant message was empty.", "warn");
1532
+ this.view.showFooterNotice("last assistant message was empty.", "default");
1425
1533
  return;
1426
1534
  }
1427
1535
  const apiKey = getGoogleApiKey(this.config, this.deps.env.env());
1428
1536
  if (!apiKey) {
1429
- this.view.addSystemMessage("set GEMINI_API_KEY or apiKeys.google to use /speak", "error");
1537
+ this.view.addTranscriptNotice("speech synthesis is not configured", "error", [
1538
+ "set GEMINI_API_KEY or apiKeys.google to use /speak",
1539
+ ]);
1430
1540
  return;
1431
1541
  }
1432
- this.speechStatusHint = "rewriting for speech...";
1542
+ this.speechActivityLabel = "rewriting for speech";
1433
1543
  this.refreshStatus();
1434
1544
  this.view.startWorkingIcon();
1435
1545
  const abortController = new AbortController();
@@ -1447,13 +1557,15 @@ export class SessionChatController {
1447
1557
  if (abortController.signal.aborted) {
1448
1558
  return;
1449
1559
  }
1450
- this.view.addSystemMessage(`speech synthesis failed: ${err.message}`, "error");
1560
+ this.view.addTranscriptNotice("failed to synthesize speech", "error", [
1561
+ err.message,
1562
+ ]);
1451
1563
  }
1452
1564
  finally {
1453
1565
  if (this.speakTask === task) {
1454
1566
  this.speakTask = undefined;
1455
1567
  }
1456
- this.speechStatusHint = undefined;
1568
+ this.speechActivityLabel = undefined;
1457
1569
  this.view.stopWorkingIcon();
1458
1570
  this.refreshStatus();
1459
1571
  this.view.requestRender();
@@ -1465,22 +1577,25 @@ export class SessionChatController {
1465
1577
  apiKey: args.apiKey,
1466
1578
  sourceText: args.sourceText,
1467
1579
  signal: args.signal,
1468
- onStatusHint: (hint) => {
1469
- this.speechStatusHint = hint;
1580
+ onActivityLabel: (hint) => {
1581
+ this.speechActivityLabel = hint;
1470
1582
  this.refreshStatus();
1471
1583
  },
1472
1584
  });
1473
1585
  }
1474
1586
  refreshStatus() {
1587
+ const activityLabel = this.getActivityLabel();
1475
1588
  this.view.updateStatus({
1476
- footer: {
1477
- cwdLabel: this.getFooterCwdLabel(),
1478
- contextUsage: this.getContextUsageString(),
1479
- sessionCost: this.getSessionCostString(),
1480
- duration: this.getTurnDurationString(),
1481
- statusHint: this.diffReviewService.getStatusHint(this.getSessionOperationStatusHint() ?? this.speechStatusHint),
1482
- pursuingGoal: this.snapshot.goal?.status === "active",
1483
- },
1589
+ footer: activityLabel
1590
+ ? { type: "activity", label: activityLabel }
1591
+ : {
1592
+ type: "regular",
1593
+ cwdLabel: this.getFooterCwdLabel(),
1594
+ contextUsage: this.getContextUsageString(),
1595
+ sessionCost: this.getSessionCostString(),
1596
+ duration: this.getTurnDurationString(),
1597
+ pursuingGoal: this.snapshot.goal?.status === "active",
1598
+ },
1484
1599
  editor: {
1485
1600
  mode: this.getInputMode(),
1486
1601
  personaName: this.getCurrentPersonaSnapshot()?.label ?? this.snapshot.settings.personaId,
@@ -1501,17 +1616,24 @@ export class SessionChatController {
1501
1616
  return "bash";
1502
1617
  return "normal";
1503
1618
  }
1504
- getSessionOperationStatusHint() {
1505
- if (this.manualCompactionInProgress) {
1506
- return "compacting context...";
1619
+ getActivityLabel() {
1620
+ if (this.manualCompactionInProgress || this.hasRunningCompactionOperation(this.snapshot)) {
1621
+ return "compacting context";
1622
+ }
1623
+ if (this.isTranscribingListen) {
1624
+ return "transcribing voice input";
1507
1625
  }
1508
- const hasRunningAutoCompaction = this.hasRunningAutoCompactionOperation(this.snapshot);
1509
- return hasRunningAutoCompaction ? "compacting context..." : undefined;
1626
+ return this.diffReviewService.getActivityLabel(this.speechActivityLabel);
1510
1627
  }
1511
- hasRunningAutoCompactionOperation(snapshot) {
1512
- return snapshot.timeline.some((item) => item.type === "operation" &&
1513
- item.operation.kind === "auto-compaction" &&
1514
- item.operation.status === "running");
1628
+ hasRunningCompactionOperation(snapshot) {
1629
+ return snapshot.timeline.items.some((item) => {
1630
+ if (item.type !== "operation") {
1631
+ return false;
1632
+ }
1633
+ const operation = snapshot.operations[item.operationId];
1634
+ return (operation?.status === "running" &&
1635
+ (operation.kind === "auto-compaction" || operation.kind === "manual-compaction"));
1636
+ });
1515
1637
  }
1516
1638
  getContextUsageString() {
1517
1639
  const last = this.getLastAssistantMessage();
@@ -1519,7 +1641,7 @@ export class SessionChatController {
1519
1641
  ? this.getContextWindowForLastTurn(last)
1520
1642
  : this.getBootstrapContextWindow();
1521
1643
  const { input, read, write, output } = this.getSessionUsageTotals();
1522
- const stats = `↑${formatTokenWindow(input)} ↓${formatTokenWindow(output)} r${formatTokenWindow(read)} w${formatTokenWindow(write)}`;
1644
+ const stats = `↑${formatFooterTokenCount(input)} ↓${formatFooterTokenCount(output)} r${formatFooterTokenCount(read)} w${formatFooterTokenCount(write)}`;
1523
1645
  const contextWindowUsageTokens = getAssistantContextWindowUsage(last);
1524
1646
  const percent = windowTokens > 0 ? (contextWindowUsageTokens / windowTokens) * 100 : 0;
1525
1647
  const percentStr = `${formatAdaptiveNumber(percent, 1, 3)}%`;
@@ -1554,11 +1676,8 @@ export class SessionChatController {
1554
1676
  }
1555
1677
  }
1556
1678
  getLastAssistantMessage() {
1557
- if (this.assistantMessages.length > 0) {
1558
- return this.assistantMessages[this.assistantMessages.length - 1];
1559
- }
1560
- for (let i = this.snapshot.messages.length - 1; i >= 0; i -= 1) {
1561
- const message = this.snapshot.messages[i]?.message;
1679
+ for (let index = this.snapshot.messages.length - 1; index >= 0; index -= 1) {
1680
+ const message = this.snapshot.messages[index]?.message;
1562
1681
  if (isAssistantMessage(message)) {
1563
1682
  return message;
1564
1683
  }
@@ -1616,18 +1735,6 @@ export class SessionChatController {
1616
1735
  }
1617
1736
  return [...files];
1618
1737
  }
1619
- getAutoCompactionMetadata(snapshot) {
1620
- for (const entry of snapshot.messages) {
1621
- if (!isCoreMessage(entry.message)) {
1622
- continue;
1623
- }
1624
- const metadata = getAutoCompactionMetadataFromMessage(entry.message);
1625
- if (metadata) {
1626
- return metadata;
1627
- }
1628
- }
1629
- return undefined;
1630
- }
1631
1738
  async copyLastAssistantText() {
1632
1739
  await copyAssistantTextToClipboard({
1633
1740
  view: this.view,
@@ -1652,11 +1759,11 @@ export class SessionChatController {
1652
1759
  }
1653
1760
  async startDiffReview(argsText) {
1654
1761
  if (this.diffReviewService.isActive()) {
1655
- this.view.addSystemMessage("diff review is already active.", "warn");
1762
+ this.view.showFooterNotice("diff review is already active.", "default");
1656
1763
  return;
1657
1764
  }
1658
1765
  if (!this.isDiffReviewIdle()) {
1659
- this.view.addSystemMessage("wait for tau to become idle before starting /diff.", "warn");
1766
+ this.view.showFooterNotice("wait for tau to become idle before starting /diff.", "default");
1660
1767
  return;
1661
1768
  }
1662
1769
  const session = this.session;
@@ -1769,6 +1876,7 @@ export class SessionChatController {
1769
1876
  const now = Date.now();
1770
1877
  const candidates = this.snapshot.messages.flatMap((entry) => {
1771
1878
  if (!entry.modelVisible ||
1879
+ !this.isMessageProjected(entry.id) ||
1772
1880
  entry.message.role !== "user" ||
1773
1881
  hasAutoCompactionContinuationMetadata(entry.message)) {
1774
1882
  return [];
@@ -1787,7 +1895,7 @@ export class SessionChatController {
1787
1895
  ];
1788
1896
  });
1789
1897
  if (candidates.length === 0) {
1790
- this.view.addSystemMessage("no user messages available to rewind.", "warn");
1898
+ this.view.showFooterNotice("no user messages available to rewind.", "default");
1791
1899
  return;
1792
1900
  }
1793
1901
  this.view.showRewindPicker({
@@ -1796,7 +1904,7 @@ export class SessionChatController {
1796
1904
  const selected = candidates.find((candidate) => candidate.id === id);
1797
1905
  if (!selected) {
1798
1906
  this.view.hideRewindPicker();
1799
- this.view.addSystemMessage("rewind selection failed.", "error");
1907
+ this.view.addTranscriptNotice("failed to select rewind point", "error");
1800
1908
  return;
1801
1909
  }
1802
1910
  void this.applyRewindSelection(selected.id);
@@ -1817,39 +1925,43 @@ export class SessionChatController {
1817
1925
  this.refreshStatus();
1818
1926
  }
1819
1927
  catch {
1820
- this.view.addSystemMessage("rewind failed.", "error");
1928
+ this.view.addTranscriptNotice("failed to rewind session", "error");
1821
1929
  }
1822
1930
  }
1823
1931
  interruptSelectedSubagent() {
1824
1932
  const selectedId = this.view.getSelectedSubagentId();
1825
1933
  if (!selectedId) {
1826
- this.view.addSystemMessage("no active subagent selected", "warn");
1934
+ this.view.showFooterNotice("no active subagent selected", "default");
1827
1935
  return;
1828
1936
  }
1829
1937
  void this.session
1830
1938
  .interruptSubagent(selectedId)
1831
1939
  .then((result) => {
1832
1940
  if (!result.found) {
1833
- this.view.addSystemMessage(`unknown subagent id: ${selectedId}`, "warn");
1941
+ this.view.showFooterNotice(`unknown subagent id: ${selectedId}`, "default");
1834
1942
  }
1835
1943
  })
1836
1944
  .catch((err) => {
1837
- this.view.addSystemMessage(`failed to interrupt subagent: ${err.message}`, "error");
1945
+ this.view.addTranscriptNotice("failed to interrupt subagent", "error", [
1946
+ err.message,
1947
+ ]);
1838
1948
  });
1839
1949
  }
1840
1950
  async stashEditorToClipboard() {
1841
1951
  const text = this.view.getExpandedEditorText();
1842
1952
  if (!text.trim()) {
1843
- this.view.addSystemMessage("no input to stash yet", "warn");
1953
+ this.view.showFooterNotice("no input to stash yet", "default");
1844
1954
  return;
1845
1955
  }
1846
1956
  try {
1847
1957
  await copyTextToClipboard(text);
1848
1958
  this.view.setEditorText("");
1849
- this.view.addSystemMessage("stashed input to clipboard", "success");
1959
+ this.view.showFooterNotice("stashed input to clipboard", "default");
1850
1960
  }
1851
1961
  catch (err) {
1852
- this.view.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
1962
+ this.view.addTranscriptNotice("failed to copy to clipboard", "error", [
1963
+ err.message,
1964
+ ]);
1853
1965
  }
1854
1966
  }
1855
1967
  isSingleLineInput(text) {
@@ -1871,28 +1983,69 @@ function formatSessionError(error) {
1871
1983
  : undefined;
1872
1984
  return cause && cause !== message ? `${message}: ${cause}` : message;
1873
1985
  }
1874
- function getRenderableTimelineItems(snapshot) {
1986
+ function getRenderableTimelineItems(snapshot, ephemeralItems) {
1875
1987
  const messagesById = new Map(snapshot.messages.map((message) => [message.id, message]));
1876
- return snapshot.timeline.flatMap((item) => {
1988
+ const items = [...snapshot.timeline.items, ...ephemeralItems].sort((left, right) => left.sequence - right.sequence);
1989
+ return items.flatMap((item) => {
1877
1990
  if (item.type === "notice") {
1878
1991
  return [
1879
1992
  {
1880
1993
  type: "notice",
1881
1994
  id: item.id,
1882
1995
  severity: item.notice.severity,
1883
- text: item.notice.text,
1996
+ title: item.notice.presentation.title,
1997
+ ...(item.notice.presentation.content
1998
+ ? { content: item.notice.presentation.content }
1999
+ : {}),
1884
2000
  },
1885
2001
  ];
1886
2002
  }
1887
- if (item.type !== "message") {
1888
- return [];
2003
+ if (item.type === "message") {
2004
+ const message = messagesById.get(item.messageId);
2005
+ if (!message) {
2006
+ return [];
2007
+ }
2008
+ const outcomeNotice = getMessageOutcomeNotice(message);
2009
+ return [
2010
+ { type: "message", id: message.id, message },
2011
+ ...(outcomeNotice ? [outcomeNotice] : []),
2012
+ ];
2013
+ }
2014
+ if (item.type === "tool") {
2015
+ const tool = snapshot.tools[item.toolId];
2016
+ return tool ? [{ type: "tool", id: item.id, model: buildToolUiModel(snapshot, tool) }] : [];
1889
2017
  }
1890
- const message = messagesById.get(item.messageId);
1891
- return message ? [{ type: "message", id: message.id, message }] : [];
2018
+ return [];
1892
2019
  });
1893
2020
  }
2021
+ function getMessageOutcomeNotice(message) {
2022
+ if (!isAssistantMessage(message.message)) {
2023
+ return undefined;
2024
+ }
2025
+ if (message.message.stopReason === "error") {
2026
+ const afterToolExecution = message.message.content.some((content) => content.type === "toolCall");
2027
+ return {
2028
+ type: "notice",
2029
+ id: `presentation:failure:${message.id}`,
2030
+ severity: "error",
2031
+ title: afterToolExecution
2032
+ ? "model request failed after tool execution"
2033
+ : "model request failed",
2034
+ content: [message.message.errorMessage ?? "the model provider returned an unknown error"],
2035
+ };
2036
+ }
2037
+ if (message.state === "interrupted") {
2038
+ return {
2039
+ type: "notice",
2040
+ id: `presentation:interruption:${message.id}`,
2041
+ severity: "info",
2042
+ title: "assistant turn interrupted",
2043
+ };
2044
+ }
2045
+ return undefined;
2046
+ }
1894
2047
  function isMessageInTimeline(snapshot, messageId) {
1895
- return snapshot.timeline.some((item) => item.type === "message" && item.messageId === messageId);
2048
+ return snapshot.timeline.items.some((item) => item.type === "message" && item.messageId === messageId);
1896
2049
  }
1897
2050
  function assistantPartialFromProtocolMessage(message) {
1898
2051
  if (message.message.role !== "assistant") {
@@ -1985,21 +2138,15 @@ const RECOVERED_TOOL_ACTION_LABELS = {
1985
2138
  blocked: "blocked",
1986
2139
  cancelled: "cancelled",
1987
2140
  };
1988
- function getToolUiModelsInModelOrder(snapshot) {
1989
- const facetsByToolId = new Map();
1990
- for (const facet of Object.values(snapshot.facets)) {
1991
- if (facet.kind === "tau.tool-ui-events" && facet.subject.type === "tool") {
1992
- facetsByToolId.set(facet.subject.id, facet);
1993
- }
1994
- }
1995
- return getToolIdsInModelOrder(snapshot).map((toolId) => {
1996
- const tool = snapshot.tools[toolId];
1997
- return {
1998
- toolCallId: tool.toolCallId,
1999
- status: tool.status,
2000
- presentation: getToolPresentation(snapshot, tool, facetsByToolId.get(toolId)),
2001
- };
2002
- });
2141
+ function buildToolUiModel(snapshot, tool) {
2142
+ const facet = Object.values(snapshot.facets).find((candidate) => candidate.kind === "tau.tool-ui-events" &&
2143
+ candidate.subject.type === "tool" &&
2144
+ candidate.subject.id === tool.id);
2145
+ return {
2146
+ toolCallId: tool.toolCallId,
2147
+ status: tool.status,
2148
+ presentation: getToolPresentation(snapshot, tool, facet),
2149
+ };
2003
2150
  }
2004
2151
  function getToolPresentation(snapshot, tool, facet) {
2005
2152
  if (facet?.kind !== "tau.tool-ui-events" || facet.version !== TOOL_UI_FACET_VERSION) {
@@ -2047,19 +2194,6 @@ function getRecoveredToolResult(snapshot, tool) {
2047
2194
  }
2048
2195
  return tool.error?.trim() || tool.summary?.trim() || undefined;
2049
2196
  }
2050
- function getToolIdsInModelOrder(snapshot) {
2051
- const messageOrder = new Map(snapshot.messages.map((message, index) => [message.id, index]));
2052
- return Object.values(snapshot.tools)
2053
- .sort((left, right) => {
2054
- const leftPosition = left.status === "streaming" ? left.origin : left.call;
2055
- const rightPosition = right.status === "streaming" ? right.origin : right.call;
2056
- const leftMessageIndex = messageOrder.get(leftPosition.messageId) ?? Number.MAX_SAFE_INTEGER;
2057
- const rightMessageIndex = messageOrder.get(rightPosition.messageId) ?? Number.MAX_SAFE_INTEGER;
2058
- return (leftMessageIndex - rightMessageIndex ||
2059
- leftPosition.contentIndex - rightPosition.contentIndex);
2060
- })
2061
- .map((tool) => tool.id);
2062
- }
2063
2197
  function getSubagentActivity(snapshot, agentId) {
2064
2198
  const facet = Object.values(snapshot.facets).find((candidate) => candidate.kind === SUBAGENT_ACTIVITY_FACET_KIND &&
2065
2199
  candidate.subject.type === "agent" &&