@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.
- package/README.md +14 -6
- package/dist/core/agent/agent_runtime.js +32 -44
- package/dist/core/agent/agent_runtime.js.map +1 -1
- package/dist/core/auth/auth_manager.js +25 -9
- package/dist/core/auth/auth_manager.js.map +1 -1
- package/dist/core/auth/auth_storage.js +1 -0
- package/dist/core/auth/auth_storage.js.map +1 -1
- package/dist/core/auth/cli.js +49 -35
- package/dist/core/auth/cli.js.map +1 -1
- package/dist/core/auth/credential_store.js +54 -22
- package/dist/core/auth/credential_store.js.map +1 -1
- package/dist/core/auth/index.js +1 -1
- package/dist/core/auth/index.js.map +1 -1
- package/dist/core/auth/providers/openai_codex.js +71 -23
- package/dist/core/auth/providers/openai_codex.js.map +1 -1
- package/dist/core/cli.js +4 -2
- package/dist/core/cli.js.map +1 -1
- package/dist/core/config/builtin_themes.js +62 -155
- package/dist/core/config/builtin_themes.js.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/models/tau_model_overrides.js +1 -66
- package/dist/core/models/tau_model_overrides.js.map +1 -1
- package/dist/core/session/direct_bash.js +1 -0
- package/dist/core/session/direct_bash.js.map +1 -1
- package/dist/core/session/runner.js +21 -14
- package/dist/core/session/runner.js.map +1 -1
- package/dist/core/static/code_mode/capabilities.mjs +25 -0
- package/dist/core/static/code_mode/history/documentation.md +2 -0
- package/dist/core/static/code_mode/history/sandbox_runner.mjs +4 -1
- package/dist/core/static/code_mode/nook/documentation.md +2 -0
- package/dist/core/static/code_mode/nook/sandbox_runner.mjs +4 -1
- package/dist/core/static/code_mode/web/documentation.md +2 -0
- package/dist/core/static/code_mode/web/sandbox_runner.mjs +4 -1
- package/dist/core/subagents/agent_supervisor.js +3 -3
- package/dist/core/subagents/agent_supervisor.js.map +1 -1
- package/dist/core/telegram/adapter.js +0 -23
- package/dist/core/telegram/adapter.js.map +1 -1
- package/dist/core/telegram/session_manager.js +34 -7
- package/dist/core/telegram/session_manager.js.map +1 -1
- package/dist/core/tools/bash.js +10 -2
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/code_mode.js +1 -0
- package/dist/core/tools/code_mode.js.map +1 -1
- package/dist/core/tools/presentation.js +1 -1
- package/dist/core/utils/auto_retry.js +1 -1
- package/dist/core/utils/auto_retry.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/host/local_session_host.js +319 -198
- package/dist/host/local_session_host.js.map +1 -1
- package/dist/host/session_protocol_handler.js +1 -1
- package/dist/host/session_protocol_handler.js.map +1 -1
- package/dist/main.js +16 -2
- package/dist/main.js.map +1 -1
- package/dist/protocol/index.d.ts +2 -2
- package/dist/protocol/index.js +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/session_protocol.d.ts +115 -40
- package/dist/protocol/session_protocol.js +377 -98
- package/dist/protocol/session_protocol.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -2
- package/dist/sdk/index.js +0 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/store/session_snapshot_migrations.js +311 -4
- package/dist/store/session_snapshot_migrations.js.map +1 -1
- package/dist/tui/chat_controller/assistant_clipboard.js +12 -8
- package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -1
- package/dist/tui/chat_controller/diff_review_service.js +13 -9
- package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
- package/dist/tui/chat_controller/history_message_model.js +3 -3
- package/dist/tui/chat_controller/history_message_model.js.map +1 -1
- package/dist/tui/chat_controller/status_format.js +18 -0
- package/dist/tui/chat_controller/status_format.js.map +1 -1
- package/dist/tui/chat_view.js +39 -38
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/session_chat_app.js +1 -3
- package/dist/tui/session_chat_app.js.map +1 -1
- package/dist/tui/session_chat_controller.js +375 -241
- package/dist/tui/session_chat_controller.js.map +1 -1
- package/dist/tui/speech_playback.js +4 -4
- package/dist/tui/speech_playback.js.map +1 -1
- package/dist/tui/ui/assistant_message.js +1 -15
- package/dist/tui/ui/assistant_message.js.map +1 -1
- package/dist/tui/ui/chat_message_model.js +26 -7
- package/dist/tui/ui/chat_message_model.js.map +1 -1
- package/dist/tui/ui/custom_editor.js +1 -1
- package/dist/tui/ui/custom_editor.js.map +1 -1
- package/dist/tui/ui/footer.js +161 -47
- package/dist/tui/ui/footer.js.map +1 -1
- package/dist/tui/ui/theme/palette.js +10 -16
- package/dist/tui/ui/theme/palette.js.map +1 -1
- package/dist/tui/ui/theme/theme.js.map +1 -1
- package/dist/tui/ui/tool_card.js +12 -9
- package/dist/tui/ui/tool_card.js.map +1 -1
- package/dist/tui/ui/transcript_notice.js +44 -0
- package/dist/tui/ui/transcript_notice.js.map +1 -0
- package/dist/tui/ui/transcript_text.js +14 -0
- package/dist/tui/ui/transcript_text.js.map +1 -0
- package/package.json +10 -12
- package/dist/sdk/turn_outcome.d.ts +0 -2
- package/dist/sdk/turn_outcome.js +0 -4
- package/dist/sdk/turn_outcome.js.map +0 -1
- package/dist/tui/ui/system_message.js +0 -22
- 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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
589
|
+
if (!result.interrupted) {
|
|
590
|
+
this.assistantInterruptRequested = false;
|
|
591
|
+
}
|
|
574
592
|
}
|
|
575
593
|
catch (error) {
|
|
576
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
617
|
+
this.view.showFooterNotice("speech recording already in progress", "default");
|
|
597
618
|
return;
|
|
598
619
|
}
|
|
599
620
|
if (this.isTranscribingListen) {
|
|
600
|
-
this.view.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
778
|
+
this.view.addTranscriptNotice("ffmpeg not found", "error", [
|
|
779
|
+
"install it with: brew install ffmpeg",
|
|
780
|
+
]);
|
|
752
781
|
}
|
|
753
782
|
else {
|
|
754
|
-
this.view.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
915
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
1027
|
+
this.view.updateAssistantMessage(viewMessageId, model);
|
|
952
1028
|
}
|
|
953
1029
|
else {
|
|
954
|
-
this.view.updateMessage(
|
|
1030
|
+
this.view.updateMessage(viewMessageId, model);
|
|
955
1031
|
}
|
|
956
1032
|
}
|
|
957
1033
|
else {
|
|
958
|
-
this.renderedMessageIds.push(this.view.addMessage(model,
|
|
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
|
-
|
|
992
|
-
this.
|
|
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.
|
|
1001
|
-
|
|
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: "
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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(
|
|
1047
|
-
this.renderedMessageIds.push(
|
|
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: "
|
|
1054
|
-
|
|
1055
|
-
|
|
1146
|
+
type: "transcript_notice",
|
|
1147
|
+
title: item.title,
|
|
1148
|
+
...(item.content ? { content: item.content } : {}),
|
|
1149
|
+
tone: item.severity === "error" ? "error" : "default",
|
|
1056
1150
|
}
|
|
1057
|
-
:
|
|
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(
|
|
1157
|
+
if (renderedIds.has(viewMessageId)) {
|
|
1062
1158
|
if (model.type === "assistant") {
|
|
1063
|
-
this.view.updateAssistantMessage(
|
|
1159
|
+
this.view.updateAssistantMessage(viewMessageId, model);
|
|
1064
1160
|
}
|
|
1065
1161
|
else {
|
|
1066
|
-
this.view.updateMessage(
|
|
1162
|
+
this.view.updateMessage(viewMessageId, model);
|
|
1067
1163
|
}
|
|
1068
1164
|
}
|
|
1069
1165
|
else {
|
|
1070
|
-
this.renderedMessageIds.push(this.view.addMessage(model,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1329
|
+
this.view.addTranscriptNotice("session id", "default", [this.snapshot.sessionId]);
|
|
1215
1330
|
}
|
|
1216
1331
|
renderCompactedSnapshot(snapshot) {
|
|
1217
|
-
this.
|
|
1332
|
+
this.view.resetToolUiSessionPreservingSubagents();
|
|
1333
|
+
this.startCompactedUiSession();
|
|
1218
1334
|
this.renderSnapshot(snapshot);
|
|
1219
1335
|
}
|
|
1220
|
-
|
|
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.
|
|
1338
|
+
this.startCompactedUiSession();
|
|
1228
1339
|
this.renderSnapshot(snapshot);
|
|
1229
|
-
|
|
1230
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1399
|
+
this.view.addTranscriptNotice("unknown persona", "error", [id]);
|
|
1292
1400
|
return;
|
|
1293
1401
|
}
|
|
1294
1402
|
if (this.isSessionOperationActive()) {
|
|
1295
|
-
this.view.
|
|
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.
|
|
1410
|
+
this.view.showFooterNotice(`switched to ${persona.label} (${this.snapshot.bootstrap.model.id})`, "default");
|
|
1303
1411
|
}
|
|
1304
1412
|
catch (error) {
|
|
1305
|
-
this.view.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1456
|
+
this.view.showFooterNotice(`reloaded: ${summary}.`, "default");
|
|
1347
1457
|
}
|
|
1348
1458
|
catch (error) {
|
|
1349
|
-
this.view.
|
|
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.
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1506
|
+
this.view.showFooterNotice("session goal cleared", "default");
|
|
1399
1507
|
}
|
|
1400
1508
|
catch (error) {
|
|
1401
|
-
this.view.
|
|
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.
|
|
1514
|
+
this.view.showFooterNotice("speech playback already in progress", "default");
|
|
1407
1515
|
return;
|
|
1408
1516
|
}
|
|
1409
1517
|
if (this.isSessionOperationActive()) {
|
|
1410
|
-
this.view.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1469
|
-
this.
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1509
|
-
return hasRunningAutoCompaction ? "compacting context..." : undefined;
|
|
1626
|
+
return this.diffReviewService.getActivityLabel(this.speechActivityLabel);
|
|
1510
1627
|
}
|
|
1511
|
-
|
|
1512
|
-
return snapshot.timeline.some((item) =>
|
|
1513
|
-
item.
|
|
1514
|
-
|
|
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 = `↑${
|
|
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
|
-
|
|
1558
|
-
|
|
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.
|
|
1762
|
+
this.view.showFooterNotice("diff review is already active.", "default");
|
|
1656
1763
|
return;
|
|
1657
1764
|
}
|
|
1658
1765
|
if (!this.isDiffReviewIdle()) {
|
|
1659
|
-
this.view.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1941
|
+
this.view.showFooterNotice(`unknown subagent id: ${selectedId}`, "default");
|
|
1834
1942
|
}
|
|
1835
1943
|
})
|
|
1836
1944
|
.catch((err) => {
|
|
1837
|
-
this.view.
|
|
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.
|
|
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.
|
|
1959
|
+
this.view.showFooterNotice("stashed input to clipboard", "default");
|
|
1850
1960
|
}
|
|
1851
1961
|
catch (err) {
|
|
1852
|
-
this.view.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1888
|
-
|
|
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
|
-
|
|
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
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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" &&
|