@markusylisiurunen/tau 0.2.128 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -75
- package/dist/core/async/cli.js +12 -2
- package/dist/core/async/cli.js.map +1 -1
- package/dist/core/async/http_protocol.js +7 -1
- package/dist/core/async/http_protocol.js.map +1 -1
- package/dist/core/async/http_server.js +9 -1
- package/dist/core/async/http_server.js.map +1 -1
- package/dist/core/async/index.js.map +1 -1
- package/dist/core/async/session_manager.js +94 -51
- package/dist/core/async/session_manager.js.map +1 -1
- package/dist/core/auth/auth_manager.js +0 -92
- package/dist/core/auth/auth_manager.js.map +1 -1
- package/dist/core/auth/credential_store.js +247 -0
- package/dist/core/auth/credential_store.js.map +1 -0
- package/dist/core/auth/index.js +0 -1
- package/dist/core/auth/index.js.map +1 -1
- package/dist/core/cli.js +12 -15
- package/dist/core/cli.js.map +1 -1
- package/dist/core/commands/registry.js +11 -55
- package/dist/core/commands/registry.js.map +1 -1
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/runtime.js +0 -1
- package/dist/core/config/runtime.js.map +1 -1
- package/dist/core/config/schema.js +102 -17
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/debug.js +1 -20
- package/dist/core/debug.js.map +1 -1
- package/dist/core/diff_review/{session.js → bridge.js} +57 -114
- package/dist/core/diff_review/bridge.js.map +1 -0
- package/dist/core/diff_review/index.js +2 -3
- package/dist/core/diff_review/index.js.map +1 -1
- package/dist/core/diff_review/review_thread.js +48 -23
- package/dist/core/diff_review/review_thread.js.map +1 -1
- package/dist/core/diff_review/snapshot.js +40 -11
- package/dist/core/diff_review/snapshot.js.map +1 -1
- package/dist/core/events/parser.js +43 -12
- package/dist/core/events/parser.js.map +1 -1
- package/dist/core/index.js +1 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/models/catalog.js +3 -3
- package/dist/core/models/catalog.js.map +1 -1
- package/dist/core/modes/index.js +1 -2
- package/dist/core/modes/index.js.map +1 -1
- package/dist/core/modes/rpc_server.js +15 -325
- package/dist/core/modes/rpc_server.js.map +1 -1
- package/dist/core/modes/websocket_server.js +181 -0
- package/dist/core/modes/websocket_server.js.map +1 -0
- package/dist/core/personas.js +11 -1
- package/dist/core/personas.js.map +1 -1
- package/dist/core/runtime/chat_runtime.js +19 -12
- package/dist/core/runtime/chat_runtime.js.map +1 -1
- package/dist/core/runtime/conversation_turn_runtime.js +16 -1
- package/dist/core/runtime/conversation_turn_runtime.js.map +1 -1
- package/dist/core/runtime/runtime_bootstrap.js +326 -0
- package/dist/core/runtime/runtime_bootstrap.js.map +1 -1
- package/dist/core/runtime/steering.js +6 -0
- package/dist/core/runtime/steering.js.map +1 -0
- package/dist/core/session/compaction.js +225 -22
- package/dist/core/session/compaction.js.map +1 -1
- package/dist/core/session/core_session.js +9 -0
- package/dist/core/session/core_session.js.map +1 -1
- package/dist/core/session/direct_bash.js +34 -0
- package/dist/core/session/direct_bash.js.map +1 -0
- package/dist/core/session/pruning.js +595 -0
- package/dist/core/session/pruning.js.map +1 -0
- package/dist/core/session/runner.js +47 -8
- package/dist/core/session/runner.js.map +1 -1
- package/dist/core/session/session_engine.js +128 -55
- package/dist/core/session/session_engine.js.map +1 -1
- package/dist/core/static/prompts/diff-review-wrapper.md +1 -1
- package/dist/core/subagents/subagent_engine.js +19 -27
- package/dist/core/subagents/subagent_engine.js.map +1 -1
- package/dist/core/tools/catalog.js.map +1 -1
- package/dist/core/tools/diff_review.js +12 -12
- package/dist/core/tools/diff_review.js.map +1 -1
- package/dist/core/tools/execution_backend.js +9 -2
- package/dist/core/tools/execution_backend.js.map +1 -1
- package/dist/core/tools/registry.js.map +1 -1
- package/dist/core/usage/logs.js.map +1 -1
- package/dist/core/utils/compact.js +3 -1
- package/dist/core/utils/compact.js.map +1 -1
- package/dist/core/utils/context.js +1 -1
- package/dist/core/utils/context.js.map +1 -1
- package/dist/core/utils/context_builder.js +1 -18
- package/dist/core/utils/context_builder.js.map +1 -1
- package/dist/core/utils/gemini_speech.js +1 -1
- package/dist/core/utils/gemini_speech.js.map +1 -1
- package/dist/core/utils/model_stream.js +105 -14
- package/dist/core/utils/model_stream.js.map +1 -1
- package/dist/core/utils/project_files.js +11 -16
- package/dist/core/utils/project_files.js.map +1 -1
- package/dist/core/utils/spawn_capture.js +2 -2
- package/dist/core/utils/spawn_capture.js.map +1 -1
- package/dist/core/utils/user_metadata.js +35 -2
- package/dist/core/utils/user_metadata.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/diff_tool/app/dist/assets/{index-jn1I_CPZ.js → index-ivRn2dP2.js} +34 -34
- package/dist/diff_tool/app/dist/index.html +1 -1
- package/dist/execution/cloudflare_sandbox_execution_environment.js +400 -0
- package/dist/execution/cloudflare_sandbox_execution_environment.js.map +1 -0
- package/dist/execution/execution_environment.js +24 -0
- package/dist/execution/execution_environment.js.map +1 -0
- package/dist/execution/fly_sprite_execution_environment.js +559 -0
- package/dist/execution/fly_sprite_execution_environment.js.map +1 -0
- package/dist/execution/local_execution_environment.js +93 -0
- package/dist/execution/local_execution_environment.js.map +1 -0
- package/dist/execution/runtime_config_snapshot.js +209 -0
- package/dist/execution/runtime_config_snapshot.js.map +1 -0
- package/dist/execution/sandbox_tool_helpers.js +55 -0
- package/dist/execution/sandbox_tool_helpers.js.map +1 -0
- package/dist/execution/tool_backend_execution_environment.js +55 -0
- package/dist/execution/tool_backend_execution_environment.js.map +1 -0
- package/dist/host/hosted_ephemeral_agent_session.js +286 -0
- package/dist/host/hosted_ephemeral_agent_session.js.map +1 -0
- package/dist/host/index.js +3 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/local_session_host.js +1464 -0
- package/dist/host/local_session_host.js.map +1 -0
- package/dist/host/session_host.js +2 -0
- package/dist/host/session_host.js.map +1 -0
- package/dist/host/session_protocol_handler.js +873 -0
- package/dist/host/session_protocol_handler.js.map +1 -0
- package/dist/main.js +537 -77
- package/dist/main.js.map +1 -1
- package/dist/protocol/index.d.ts +2 -0
- package/dist/protocol/index.js +2 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/session_protocol.d.ts +691 -0
- package/dist/protocol/session_protocol.js +2117 -0
- package/dist/protocol/session_protocol.js.map +1 -0
- package/dist/sdk/client.d.ts +2 -1
- package/dist/sdk/client.js +136 -392
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/index.d.ts +7 -4
- package/dist/sdk/index.js +4 -2
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/session.d.ts +5 -0
- package/dist/sdk/session.js +406 -0
- package/dist/sdk/session.js.map +1 -0
- package/dist/sdk/types.d.ts +97 -78
- package/dist/store/file_session_store.js +157 -0
- package/dist/store/file_session_store.js.map +1 -0
- package/dist/store/memory_session_store.js +24 -0
- package/dist/store/memory_session_store.js.map +1 -0
- package/dist/store/session_store.js +32 -0
- package/dist/store/session_store.js.map +1 -0
- package/dist/transport/errors.d.ts +29 -0
- package/dist/{sdk → transport}/errors.js +5 -5
- package/dist/transport/errors.js.map +1 -0
- package/dist/transport/in_process_session_transport.js +102 -0
- package/dist/transport/in_process_session_transport.js.map +1 -0
- package/dist/transport/index.d.ts +7 -0
- package/dist/transport/index.js +4 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/pending_session_protocol_requests.js +67 -0
- package/dist/transport/pending_session_protocol_requests.js.map +1 -0
- package/dist/transport/session_protocol_transport_helpers.js +98 -0
- package/dist/transport/session_protocol_transport_helpers.js.map +1 -0
- package/dist/transport/session_transport.d.ts +11 -0
- package/dist/transport/session_transport.js +2 -0
- package/dist/transport/session_transport.js.map +1 -0
- package/dist/transport/stdio_session_transport.d.ts +38 -0
- package/dist/transport/stdio_session_transport.js +243 -0
- package/dist/transport/stdio_session_transport.js.map +1 -0
- package/dist/transport/websocket_session_transport.d.ts +52 -0
- package/dist/transport/websocket_session_transport.js +235 -0
- package/dist/transport/websocket_session_transport.js.map +1 -0
- package/dist/tui/chat_controller/assistant_clipboard.js +39 -0
- package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -0
- package/dist/tui/chat_controller/command_hints.js +43 -0
- package/dist/tui/chat_controller/command_hints.js.map +1 -0
- package/dist/tui/chat_controller/diff_review_service.js +9 -9
- package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
- package/dist/tui/chat_controller/diff_review_user_message.js +17 -0
- package/dist/tui/chat_controller/diff_review_user_message.js.map +1 -0
- package/dist/tui/chat_controller/history_labels.js +8 -0
- package/dist/tui/chat_controller/history_labels.js.map +1 -0
- package/dist/tui/chat_controller/history_message_model.js +36 -0
- package/dist/tui/chat_controller/history_message_model.js.map +1 -0
- package/dist/tui/chat_controller/queued_user_messages.js +18 -1
- package/dist/tui/chat_controller/queued_user_messages.js.map +1 -1
- package/dist/tui/chat_controller/status_format.js +22 -0
- package/dist/tui/chat_controller/status_format.js.map +1 -0
- package/dist/tui/chat_view.js +3 -1
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/index.js +1 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/listen_capture.js +87 -0
- package/dist/tui/listen_capture.js.map +1 -0
- package/dist/tui/memory_mode.js +23 -0
- package/dist/tui/memory_mode.js.map +1 -0
- package/dist/tui/session_chat_app.js +227 -0
- package/dist/tui/session_chat_app.js.map +1 -0
- package/dist/tui/session_chat_controller.js +2167 -0
- package/dist/tui/session_chat_controller.js.map +1 -0
- package/dist/tui/session_tool_execution_backend.js +170 -0
- package/dist/tui/session_tool_execution_backend.js.map +1 -0
- package/dist/tui/speech_playback.js +107 -0
- package/dist/tui/speech_playback.js.map +1 -0
- package/dist/tui/ui/assistant_message.js +27 -21
- package/dist/tui/ui/assistant_message.js.map +1 -1
- package/dist/tui/ui/components/editor.js +17 -13
- package/dist/tui/ui/components/editor.js.map +1 -1
- package/dist/tui/ui/custom_editor.js +25 -5
- package/dist/tui/ui/custom_editor.js.map +1 -1
- package/dist/tui/ui/queued_messages.js +1 -1
- package/dist/tui/ui/queued_messages.js.map +1 -1
- package/dist/tui/ui/slash_autocomplete.js +14 -36
- package/dist/tui/ui/slash_autocomplete.js.map +1 -1
- package/dist/tui/ui/tool_ui_registry.js +21 -0
- package/dist/tui/ui/tool_ui_registry.js.map +1 -1
- package/package.json +14 -11
- package/dist/core/auth/credential_resolver.js +0 -36
- package/dist/core/auth/credential_resolver.js.map +0 -1
- package/dist/core/config/bash_commands.js +0 -44
- package/dist/core/config/bash_commands.js.map +0 -1
- package/dist/core/diff_review/session.js.map +0 -1
- package/dist/core/modes/rpc_adapter.js +0 -17
- package/dist/core/modes/rpc_adapter.js.map +0 -1
- package/dist/core/modes/rpc_protocol.js +0 -500
- package/dist/core/modes/rpc_protocol.js.map +0 -1
- package/dist/core/session/checkpoint.js +0 -34
- package/dist/core/session/checkpoint.js.map +0 -1
- package/dist/sdk/errors.d.ts +0 -33
- package/dist/sdk/errors.js.map +0 -1
- package/dist/tui/app.js +0 -64
- package/dist/tui/app.js.map +0 -1
- package/dist/tui/chat_controller/session_maintenance_service.js +0 -634
- package/dist/tui/chat_controller/session_maintenance_service.js.map +0 -1
- package/dist/tui/chat_controller.js +0 -2532
- package/dist/tui/chat_controller.js.map +0 -1
|
@@ -1,2532 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { statSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { formatCodexAuthError } from "../core/auth/auth_messages.js";
|
|
8
|
-
import { getAuthPath } from "../core/auth/auth_paths.js";
|
|
9
|
-
import { AuthStorage } from "../core/auth/auth_storage.js";
|
|
10
|
-
import { createCredentialResolver, } from "../core/auth/credential_resolver.js";
|
|
11
|
-
import { createCommandRegistry, getRiskLevelDescription, } from "../core/commands/index.js";
|
|
12
|
-
import { createDefaultConfigDeps, getGoogleApiKey, getMistralApiKey, loadRuntimeConfig, } from "../core/config/index.js";
|
|
13
|
-
import { startDiffReviewSession as startCoreDiffReviewSession } from "../core/diff_review/index.js";
|
|
14
|
-
import { ChatRuntime } from "../core/runtime/chat_runtime.js";
|
|
15
|
-
import { createDefaultCoreDeps } from "../core/runtime/deps.js";
|
|
16
|
-
import { resolvePersonaSkillsForPromptContext, resolveProjectContextForPromptContext, resolveRuntimePromptBootstrap, } from "../core/runtime/runtime_bootstrap.js";
|
|
17
|
-
import { createCheckpoint } from "../core/session/checkpoint.js";
|
|
18
|
-
import { buildBashUiText, formatBashUserMessageText, getBashOutputPolicy, prepareBashOutput, } from "../core/tools/bash.js";
|
|
19
|
-
import { ToolCatalog } from "../core/tools/catalog.js";
|
|
20
|
-
import { createLocalToolExecutionBackend } from "../core/tools/execution_backend.js";
|
|
21
|
-
import { REASONING_LEVELS, } from "../core/types.js";
|
|
22
|
-
import { findAgentsFilesFromCwdToHome } from "../core/utils/agents_files.js";
|
|
23
|
-
import { formatCwdChangeNotice, formatProjectContextChangeNotice, formatRiskLevelChangeNotice, } from "../core/utils/context.js";
|
|
24
|
-
import { formatAdaptiveNumber, formatCwd, formatPathForDisplay, formatTokenWindow, } from "../core/utils/format.js";
|
|
25
|
-
import { streamGeminiSpeechAudio } from "../core/utils/gemini_speech.js";
|
|
26
|
-
import { extractAllFencedCodeBlocks, extractAssistantText } from "../core/utils/messages.js";
|
|
27
|
-
import { streamModel } from "../core/utils/model_stream.js";
|
|
28
|
-
import { listProjectFilesAsync } from "../core/utils/project_files.js";
|
|
29
|
-
import { transcribeAudio } from "../core/utils/speech_to_text.js";
|
|
30
|
-
import { getAutoCompactionMetadataFromMessage, hasAutoCompactionContinuationMetadata, stripTauUserMetadata, } from "../core/utils/user_metadata.js";
|
|
31
|
-
import { APP_VERSION } from "../core/version.js";
|
|
32
|
-
import { DiffReviewService, } from "./chat_controller/diff_review_service.js";
|
|
33
|
-
import { InterruptLifecycle } from "./chat_controller/interrupt_lifecycle.js";
|
|
34
|
-
import { QueuedUserMessages } from "./chat_controller/queued_user_messages.js";
|
|
35
|
-
import { SessionMaintenanceService, } from "./chat_controller/session_maintenance_service.js";
|
|
36
|
-
import { copyTextToClipboard } from "./clipboard.js";
|
|
37
|
-
import { DOUBLE_PRESS_WINDOW_MS } from "./constants.js";
|
|
38
|
-
import { getFileAutocompleteToken } from "./ui/slash_autocomplete.js";
|
|
39
|
-
const RELOAD_PLANS = {
|
|
40
|
-
"new-session": {
|
|
41
|
-
personas: true,
|
|
42
|
-
prompts: true,
|
|
43
|
-
skills: true,
|
|
44
|
-
themes: true,
|
|
45
|
-
bashCommands: true,
|
|
46
|
-
projectContext: true,
|
|
47
|
-
},
|
|
48
|
-
"reload-command": {
|
|
49
|
-
personas: false,
|
|
50
|
-
prompts: true,
|
|
51
|
-
skills: true,
|
|
52
|
-
themes: true,
|
|
53
|
-
bashCommands: true,
|
|
54
|
-
projectContext: true,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
const ALLOWED_RISK_LEVELS = ["read-only", "read-write"];
|
|
58
|
-
const LISTEN_TEMP_FILE_TEMPLATE = "/tmp/tau-listen.XXXXXX";
|
|
59
|
-
const SPEAK_TEMP_FILE_TEMPLATE = "/tmp/tau-speak.XXXXXX";
|
|
60
|
-
const LISTEN_RECORDING_MIN_BYTES = 1024;
|
|
61
|
-
const LISTEN_RECORDING_MAX_DURATION_MS = 5 * 60 * 1000;
|
|
62
|
-
const SPEAK_PLAYBACK_RATE = 1.4;
|
|
63
|
-
const CAFFEINATE_COMMAND = "/usr/bin/caffeinate";
|
|
64
|
-
const SmartPruneResponseSchema = z.object({
|
|
65
|
-
prune: z.array(z.string().trim().min(1)),
|
|
66
|
-
});
|
|
67
|
-
export class ChatController {
|
|
68
|
-
view;
|
|
69
|
-
personas;
|
|
70
|
-
currentPersona;
|
|
71
|
-
prompts;
|
|
72
|
-
skills;
|
|
73
|
-
themes;
|
|
74
|
-
bashCommands;
|
|
75
|
-
initialUserMessage;
|
|
76
|
-
config;
|
|
77
|
-
defaultDiffTool;
|
|
78
|
-
activeThemeId;
|
|
79
|
-
credentialResolver;
|
|
80
|
-
authPath;
|
|
81
|
-
caffeinated;
|
|
82
|
-
agentCwd;
|
|
83
|
-
includeAgentContext;
|
|
84
|
-
runtime;
|
|
85
|
-
engine;
|
|
86
|
-
commandRegistry;
|
|
87
|
-
commandHandlers;
|
|
88
|
-
toolBackend;
|
|
89
|
-
deps;
|
|
90
|
-
eventUnsubscribe;
|
|
91
|
-
isStreaming = false;
|
|
92
|
-
queuedMessageBuffer;
|
|
93
|
-
interruptLifecycle;
|
|
94
|
-
diffReviewService;
|
|
95
|
-
maintenanceService;
|
|
96
|
-
isBashMode = false;
|
|
97
|
-
isBashIncognito = false;
|
|
98
|
-
isMemoryMode = false;
|
|
99
|
-
showThinking = false;
|
|
100
|
-
compactToolUi = true;
|
|
101
|
-
commandHint;
|
|
102
|
-
speechStatusHint;
|
|
103
|
-
autoCompactionStatusHint;
|
|
104
|
-
riskLevel = "read-only";
|
|
105
|
-
projectContextBlock;
|
|
106
|
-
projectFiles = [];
|
|
107
|
-
projectFilesCwd;
|
|
108
|
-
isRefreshingProjectFiles = false;
|
|
109
|
-
isInFileAutocomplete = false;
|
|
110
|
-
agentsFiles;
|
|
111
|
-
agentsConfigErrors;
|
|
112
|
-
pendingRiskLevelChange;
|
|
113
|
-
pendingCwdChange;
|
|
114
|
-
pendingProjectContextChange;
|
|
115
|
-
expandedFilesInCurrentPrompt = new Set();
|
|
116
|
-
expandedSkillsInCurrentPrompt = new Set();
|
|
117
|
-
assistantState;
|
|
118
|
-
currentTurnStartedAt;
|
|
119
|
-
lastTurnDurationMs = 0;
|
|
120
|
-
turnTimer;
|
|
121
|
-
lastEmptySubmitAt;
|
|
122
|
-
listenRecording;
|
|
123
|
-
isTranscribingListen = false;
|
|
124
|
-
listenTransition;
|
|
125
|
-
speakTask;
|
|
126
|
-
turnCaffeinate;
|
|
127
|
-
disableCaffeinateForSession = false;
|
|
128
|
-
constructor(options) {
|
|
129
|
-
this.view = options.view;
|
|
130
|
-
this.deps = options.deps ?? createDefaultCoreDeps();
|
|
131
|
-
const cwd = this.deps.env.cwd();
|
|
132
|
-
const home = this.deps.env.home();
|
|
133
|
-
this.personas = options.personas;
|
|
134
|
-
if (this.personas.length === 0) {
|
|
135
|
-
throw new Error("no personas available. add a custom persona in ~/.config/tau/personas or .tau/personas, or unset disableBuiltinPersonas.");
|
|
136
|
-
}
|
|
137
|
-
this.prompts = options.prompts ?? [];
|
|
138
|
-
this.skills = options.skills ?? [];
|
|
139
|
-
this.themes = options.themes ?? [];
|
|
140
|
-
this.bashCommands = options.bashCommands ?? [];
|
|
141
|
-
this.initialUserMessage = options.initialUserMessage;
|
|
142
|
-
this.config = options.config ?? {};
|
|
143
|
-
this.defaultDiffTool = options.defaultDiffTool;
|
|
144
|
-
this.caffeinated = options.caffeinated ?? false;
|
|
145
|
-
this.activeThemeId = this.config.defaultTheme;
|
|
146
|
-
this.authPath = getAuthPath(this.deps.env.home());
|
|
147
|
-
const authStorage = new AuthStorage(this.authPath);
|
|
148
|
-
this.credentialResolver = createCredentialResolver({
|
|
149
|
-
authStorage,
|
|
150
|
-
getConfig: () => this.config,
|
|
151
|
-
});
|
|
152
|
-
this.compactToolUi = true;
|
|
153
|
-
const queuedUserMessages = options.queuedUserMessages ?? [];
|
|
154
|
-
this.queuedMessageBuffer = new QueuedUserMessages(queuedUserMessages);
|
|
155
|
-
this.interruptLifecycle = new InterruptLifecycle();
|
|
156
|
-
this.includeAgentContext = !options.noAgentContextFiles;
|
|
157
|
-
const initialPersona = (options.initialPersonaId &&
|
|
158
|
-
this.personas.find((p) => p.id.toLowerCase() === options.initialPersonaId.toLowerCase())) ||
|
|
159
|
-
this.personas[0];
|
|
160
|
-
this.currentPersona = this.createSessionPersona(initialPersona);
|
|
161
|
-
this.clampPersonaReasoning(this.currentPersona);
|
|
162
|
-
if (options.initialReasoningOverride !== undefined) {
|
|
163
|
-
const allowed = this.getAllowedReasoningLevels(this.currentPersona);
|
|
164
|
-
if (allowed.includes(options.initialReasoningOverride)) {
|
|
165
|
-
this.currentPersona.settings.reasoning = options.initialReasoningOverride;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (options.initialRiskLevel) {
|
|
169
|
-
this.riskLevel = options.initialRiskLevel;
|
|
170
|
-
}
|
|
171
|
-
const startupBootstrap = resolveRuntimePromptBootstrap({
|
|
172
|
-
persona: this.currentPersona,
|
|
173
|
-
discoveredSkills: this.skills,
|
|
174
|
-
cwd,
|
|
175
|
-
home,
|
|
176
|
-
includeAgentContext: this.includeAgentContext,
|
|
177
|
-
readFile: this.deps.fs.readFile,
|
|
178
|
-
});
|
|
179
|
-
this.agentCwd = startupBootstrap.promptContext.cwd;
|
|
180
|
-
this.agentsFiles = startupBootstrap.agentsFiles;
|
|
181
|
-
this.agentsConfigErrors = startupBootstrap.warnings;
|
|
182
|
-
this.projectContextBlock = startupBootstrap.promptContext.projectContextBlock;
|
|
183
|
-
const skillsBlock = startupBootstrap.promptContext.skillsBlock;
|
|
184
|
-
this.projectFiles = [];
|
|
185
|
-
this.refreshProjectFilesInBackground();
|
|
186
|
-
this.toolBackend =
|
|
187
|
-
options.toolBackend ??
|
|
188
|
-
createLocalToolExecutionBackend({
|
|
189
|
-
spawn: this.deps.spawn,
|
|
190
|
-
env: this.deps.env,
|
|
191
|
-
});
|
|
192
|
-
const toolRegistry = ToolCatalog.createRegistry(this.toolBackend, {
|
|
193
|
-
diffReview: {
|
|
194
|
-
getDiffToolConfig: () => this.resolveDiffToolConfig(),
|
|
195
|
-
startSession: (args) => this.startDiffReviewSession(args),
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
this.runtime = ChatRuntime.create({
|
|
199
|
-
persona: this.currentPersona,
|
|
200
|
-
riskLevel: this.riskLevel,
|
|
201
|
-
toolRegistry,
|
|
202
|
-
promptContext: {
|
|
203
|
-
...startupBootstrap.promptContext,
|
|
204
|
-
skillsBlock,
|
|
205
|
-
},
|
|
206
|
-
environment: {
|
|
207
|
-
now: () => this.deps.clock.now(),
|
|
208
|
-
platform: () => this.deps.env.platform(),
|
|
209
|
-
nodeVersion: () => this.deps.env.nodeVersion(),
|
|
210
|
-
},
|
|
211
|
-
config: this.config,
|
|
212
|
-
deps: this.deps,
|
|
213
|
-
});
|
|
214
|
-
this.engine = this.runtime.session;
|
|
215
|
-
this.eventUnsubscribe = this.engine.onEvent((event) => this.onEvent(event));
|
|
216
|
-
this.diffReviewService = new DiffReviewService({
|
|
217
|
-
view: this.view,
|
|
218
|
-
interruptLifecycle: this.interruptLifecycle,
|
|
219
|
-
refreshStatus: () => this.refreshStatus(),
|
|
220
|
-
startTurnTimer: () => this.startTurnTimer(),
|
|
221
|
-
stopTurnTimer: () => this.stopTurnTimer(),
|
|
222
|
-
getDiffToolConfig: () => this.resolveDiffToolConfig(),
|
|
223
|
-
startSession: (args) => this.startDiffReviewSession(args),
|
|
224
|
-
onReviewReturned: (review) => this.handleReturnedDiffReview(review),
|
|
225
|
-
});
|
|
226
|
-
this.maintenanceService = new SessionMaintenanceService({
|
|
227
|
-
engine: this.engine,
|
|
228
|
-
view: this.view,
|
|
229
|
-
runStreamingTask: (task, onSettled) => this.runMaintenanceStreamingTask(task, onSettled),
|
|
230
|
-
applyCompactedHistoryUi: (compactionMessage) => this.applyCompactedHistoryUi(compactionMessage),
|
|
231
|
-
requestSmartPruneSelection: (prompt, signal) => this.requestSmartPruneSelection(prompt, signal),
|
|
232
|
-
});
|
|
233
|
-
this.commandRegistry = createCommandRegistry();
|
|
234
|
-
this.commandHandlers = {
|
|
235
|
-
help: () => this.showHelp(),
|
|
236
|
-
copyText: () => this.copyLastAssistantText(),
|
|
237
|
-
copyCode: () => this.copyLastAssistantCode(),
|
|
238
|
-
checkpoint: () => this.checkpointSession(),
|
|
239
|
-
newSession: () => this.clearSession(),
|
|
240
|
-
rewind: () => this.startRewindFlow(),
|
|
241
|
-
cd: (path) => this.changeDirectory(path),
|
|
242
|
-
diff: (argsText) => this.startDiffReview(argsText),
|
|
243
|
-
compactSummaryOnly: (extra) => this.compactSessionSummaryOnly(extra),
|
|
244
|
-
compactSummaryAndLast: (extra) => this.compactSessionSummaryAndLast(extra),
|
|
245
|
-
pruneEarliest: (extra) => this.pruneToolResults("earliest", extra),
|
|
246
|
-
pruneLargest: (extra) => this.pruneToolResults("largest", extra),
|
|
247
|
-
pruneSmart: (extra) => this.pruneToolResultsSmart(extra),
|
|
248
|
-
reload: () => this.reloadContent(),
|
|
249
|
-
listen: () => this.startListenCaptureFromCommand(),
|
|
250
|
-
speak: () => this.speakLastAssistantMessage(),
|
|
251
|
-
risk: (level) => this.setRiskLevel(level),
|
|
252
|
-
persona: (id) => this.switchPersona(id),
|
|
253
|
-
prompt: (id) => this.insertPrompt(id),
|
|
254
|
-
theme: (id) => this.switchTheme(id),
|
|
255
|
-
bash: (id) => this.runSavedBashCommand(id),
|
|
256
|
-
unknown: () => this.view.addSystemMessage("unknown command. type /help.", "error"),
|
|
257
|
-
};
|
|
258
|
-
this.view.setThinkingVisibility(this.showThinking);
|
|
259
|
-
this.view.setCompactToolUi(this.compactToolUi);
|
|
260
|
-
this.view.addMessage({
|
|
261
|
-
type: "app_intro",
|
|
262
|
-
title: this.buildStartupIntroTitle(),
|
|
263
|
-
body: this.buildStartupIntroBody(),
|
|
264
|
-
});
|
|
265
|
-
if (this.agentsConfigErrors.length > 0) {
|
|
266
|
-
this.view.addSystemMessage(["config warnings", ...this.agentsConfigErrors.map((e) => `- ${e}`)].join("\n"), "warn");
|
|
267
|
-
}
|
|
268
|
-
this.hydrateCheckpoint(options.initialHistory);
|
|
269
|
-
this.refreshStatus();
|
|
270
|
-
}
|
|
271
|
-
getCommandRegistry() {
|
|
272
|
-
return this.commandRegistry;
|
|
273
|
-
}
|
|
274
|
-
getAutocompleteSources() {
|
|
275
|
-
return {
|
|
276
|
-
personas: () => this.personas.map((p) => ({ id: p.id, label: p.label })),
|
|
277
|
-
prompts: () => this.prompts.map((t) => ({ id: t.id, label: t.label })),
|
|
278
|
-
themes: () => this.themes.map((theme) => ({ id: theme.id })),
|
|
279
|
-
bashCommands: () => this.bashCommands.map((b) => ({
|
|
280
|
-
id: b.id,
|
|
281
|
-
description: b.description,
|
|
282
|
-
})),
|
|
283
|
-
projectFiles: () => this.projectFiles,
|
|
284
|
-
skills: () => this.skills.map((skill) => skill.name),
|
|
285
|
-
subagents: () => this.getVisibleSubagentsForPersona(this.currentPersona),
|
|
286
|
-
riskLevels: () => ALLOWED_RISK_LEVELS,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
getInputHandlers() {
|
|
290
|
-
return {
|
|
291
|
-
onCtrlT: () => this.toggleThinkingVisibility(),
|
|
292
|
-
onCtrlO: () => this.toggleCompactToolUi(),
|
|
293
|
-
onShiftTab: () => this.cycleReasoningLevel(),
|
|
294
|
-
onCtrlR: () => this.cycleRiskLevel(),
|
|
295
|
-
onCtrlP: () => this.cyclePersonality(),
|
|
296
|
-
onCtrlS: () => void this.stashEditorToClipboard(),
|
|
297
|
-
onCtrlY: () => void this.toggleListenCapture(),
|
|
298
|
-
onEscape: () => this.onInterrupt(),
|
|
299
|
-
onCtrlF: () => {
|
|
300
|
-
this.expandFileMentions().catch((err) => {
|
|
301
|
-
this.view.addSystemMessage(`mention expansion failed: ${err.message}`, "error");
|
|
302
|
-
});
|
|
303
|
-
},
|
|
304
|
-
onAltUp: () => this.popQueuedUserMessageIntoEditor(),
|
|
305
|
-
onAltDown: () => this.cycleSubagentSelection(),
|
|
306
|
-
onCtrlG: () => this.terminateSelectedSubagent(),
|
|
307
|
-
beforeSubmit: (text) => this.beforeSubmit(text),
|
|
308
|
-
onChange: (text) => this.handleEditorChange(text),
|
|
309
|
-
onSubmit: (text) => void this.onUserInput(text),
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
async start() {
|
|
313
|
-
if (this.initialUserMessage) {
|
|
314
|
-
await this.sendInitialUserMessage(this.initialUserMessage);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
async dispose() {
|
|
318
|
-
this.eventUnsubscribe?.();
|
|
319
|
-
this.engine.dispose();
|
|
320
|
-
if (this.listenTransition) {
|
|
321
|
-
await this.listenTransition;
|
|
322
|
-
}
|
|
323
|
-
if (this.speakTask) {
|
|
324
|
-
this.speakTask.abortController.abort();
|
|
325
|
-
await this.speakTask.completion;
|
|
326
|
-
}
|
|
327
|
-
await this.cancelDiffReview();
|
|
328
|
-
await this.cancelListenCapture();
|
|
329
|
-
await this.stopTurnCaffeinate();
|
|
330
|
-
}
|
|
331
|
-
// Mode Adapter ---------------------------------------------------------------------------------
|
|
332
|
-
async onUserInput(text) {
|
|
333
|
-
await this.handleSubmit(text);
|
|
334
|
-
}
|
|
335
|
-
onInterrupt() {
|
|
336
|
-
if (this.listenRecording) {
|
|
337
|
-
void this.runListenTransition(() => this.stopListenCapture());
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (this.speakTask) {
|
|
341
|
-
if (!this.speakTask.abortController.signal.aborted) {
|
|
342
|
-
this.speakTask.abortController.abort();
|
|
343
|
-
this.view.addSystemMessage("interrupted", "error");
|
|
344
|
-
}
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
this.interruptActiveTask();
|
|
348
|
-
}
|
|
349
|
-
onEvent(event) {
|
|
350
|
-
switch (event.type) {
|
|
351
|
-
case "assistant_start":
|
|
352
|
-
this.assistantState = {
|
|
353
|
-
historyEntryId: event.historyEntryId,
|
|
354
|
-
inserted: false,
|
|
355
|
-
model: { type: "assistant_partial", text: "", thinking: "" },
|
|
356
|
-
};
|
|
357
|
-
return;
|
|
358
|
-
case "assistant_partial": {
|
|
359
|
-
const state = this.ensureAssistantState(event.historyEntryId);
|
|
360
|
-
const { snapshot } = event;
|
|
361
|
-
const model = {
|
|
362
|
-
type: "assistant_partial",
|
|
363
|
-
text: snapshot.hasTextStarted ? snapshot.text : "",
|
|
364
|
-
thinking: snapshot.thinking,
|
|
365
|
-
};
|
|
366
|
-
state.model = model;
|
|
367
|
-
const shouldInsert = snapshot.hasTextStarted || (this.showThinking && snapshot.hasAnyThinking);
|
|
368
|
-
if (shouldInsert && !state.inserted) {
|
|
369
|
-
this.ensureAssistantInserted(state);
|
|
370
|
-
}
|
|
371
|
-
if (state.inserted) {
|
|
372
|
-
this.view.updateAssistantMessage(state.historyEntryId, model);
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
case "assistant_final": {
|
|
377
|
-
const state = this.ensureAssistantState(event.historyEntryId);
|
|
378
|
-
if (this.shouldSuppressAbortedAssistantMessage(event.message)) {
|
|
379
|
-
this.refreshStatus();
|
|
380
|
-
this.assistantState = undefined;
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const model = { type: "assistant", message: event.message };
|
|
384
|
-
state.model = model;
|
|
385
|
-
if (!state.inserted) {
|
|
386
|
-
this.ensureAssistantInserted(state);
|
|
387
|
-
}
|
|
388
|
-
if (state.inserted) {
|
|
389
|
-
this.view.updateAssistantMessage(state.historyEntryId, model);
|
|
390
|
-
}
|
|
391
|
-
this.refreshStatus();
|
|
392
|
-
this.assistantState = undefined;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
case "tool_ui":
|
|
396
|
-
this.view.handleToolUiEvent(event.uiEvent);
|
|
397
|
-
if (event.uiEvent.type === "diff_review_started") {
|
|
398
|
-
this.view.sendTerminalNotification("tau is waiting for diff review");
|
|
399
|
-
}
|
|
400
|
-
this.refreshStatus();
|
|
401
|
-
return;
|
|
402
|
-
case "subagent_ui":
|
|
403
|
-
this.view.handleSubagentEvent(event.event);
|
|
404
|
-
this.refreshStatus();
|
|
405
|
-
return;
|
|
406
|
-
case "notice": {
|
|
407
|
-
const kind = event.severity === "error" ? "error" : event.severity === "warn" ? "warn" : "success";
|
|
408
|
-
this.view.addSystemMessage(event.text, kind);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
case "compaction_start":
|
|
412
|
-
this.autoCompactionStatusHint = "compacting context...";
|
|
413
|
-
this.refreshStatus();
|
|
414
|
-
return;
|
|
415
|
-
case "compaction_end":
|
|
416
|
-
this.autoCompactionStatusHint = undefined;
|
|
417
|
-
this.refreshStatus();
|
|
418
|
-
switch (event.outcome) {
|
|
419
|
-
case "compacted":
|
|
420
|
-
this.applyAutoCompactedHistoryUi(event.result);
|
|
421
|
-
return;
|
|
422
|
-
case "failed":
|
|
423
|
-
this.view.addSystemMessage(`auto-compaction failed: ${event.errorMessage}`, "error");
|
|
424
|
-
return;
|
|
425
|
-
case "skipped":
|
|
426
|
-
case "aborted":
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
return;
|
|
430
|
-
case "tool_result":
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
shouldSuppressAbortedAssistantMessage(message) {
|
|
435
|
-
if (message.stopReason !== "aborted") {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
for (const content of message.content) {
|
|
439
|
-
if (content.type === "text" && content.text.trim().length > 0) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
if (this.showThinking && content.type === "thinking" && content.thinking.trim().length > 0) {
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
ensureAssistantState(historyEntryId) {
|
|
449
|
-
if (this.assistantState?.historyEntryId === historyEntryId) {
|
|
450
|
-
return this.assistantState;
|
|
451
|
-
}
|
|
452
|
-
const state = {
|
|
453
|
-
historyEntryId,
|
|
454
|
-
inserted: false,
|
|
455
|
-
model: { type: "assistant_partial", text: "", thinking: "" },
|
|
456
|
-
};
|
|
457
|
-
this.assistantState = state;
|
|
458
|
-
return state;
|
|
459
|
-
}
|
|
460
|
-
ensureAssistantInserted(state) {
|
|
461
|
-
if (state.inserted)
|
|
462
|
-
return;
|
|
463
|
-
state.inserted = true;
|
|
464
|
-
this.view.addMessage(state.model, state.historyEntryId);
|
|
465
|
-
}
|
|
466
|
-
hydrateCheckpoint(history) {
|
|
467
|
-
if (!history || history.length === 0) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
let hidingAutoCompactionTail = false;
|
|
471
|
-
for (const message of history) {
|
|
472
|
-
const historyEntryId = this.engine.addMessage(message);
|
|
473
|
-
const autoCompaction = getAutoCompactionMetadataFromMessage(message);
|
|
474
|
-
if (autoCompaction) {
|
|
475
|
-
this.renderHistoryMessage(message, historyEntryId);
|
|
476
|
-
this.view.addMessage({
|
|
477
|
-
type: "system",
|
|
478
|
-
text: this.formatAutoCompactionRetainedText(autoCompaction),
|
|
479
|
-
kind: "muted",
|
|
480
|
-
});
|
|
481
|
-
hidingAutoCompactionTail = true;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (hasAutoCompactionContinuationMetadata(message)) {
|
|
485
|
-
hidingAutoCompactionTail = false;
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (hidingAutoCompactionTail) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
this.renderHistoryMessage(message, historyEntryId);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
renderHistoryMessage(message, historyEntryId) {
|
|
495
|
-
if (hasAutoCompactionContinuationMetadata(message)) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (message.role === "user") {
|
|
499
|
-
const text = this.extractUserText(message);
|
|
500
|
-
if (text) {
|
|
501
|
-
this.view.addMessage({ type: "user", text }, historyEntryId);
|
|
502
|
-
}
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (message.role === "assistant") {
|
|
506
|
-
this.view.addMessage({ type: "assistant", message: message }, historyEntryId);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
if (message.role === "toolResult") {
|
|
510
|
-
const toolResult = message;
|
|
511
|
-
this.view.addMessage({ type: "system", text: this.formatToolResultNotice(toolResult), kind: "muted" }, historyEntryId);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
extractUserText(message) {
|
|
515
|
-
if (typeof message.content === "string") {
|
|
516
|
-
return stripTauUserMetadata(message.content).trim();
|
|
517
|
-
}
|
|
518
|
-
const parts = [];
|
|
519
|
-
for (const block of message.content) {
|
|
520
|
-
if (block.type === "text") {
|
|
521
|
-
parts.push(block.text);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
return stripTauUserMetadata(parts.join("\n")).trim();
|
|
525
|
-
}
|
|
526
|
-
formatToolResultNotice(toolResult) {
|
|
527
|
-
const status = toolResult.isError ? "error" : "ok";
|
|
528
|
-
const icon = toolResult.isError ? "✗" : "✓";
|
|
529
|
-
return `${icon} ${toolResult.toolName} (${status})`;
|
|
530
|
-
}
|
|
531
|
-
// UI Updates ------------------------------------------------------------------------------------
|
|
532
|
-
refreshStatus() {
|
|
533
|
-
const reasoningLabel = this.currentPersona.settings.reasoning ?? "none";
|
|
534
|
-
const contextUsage = this.getContextUsageString();
|
|
535
|
-
const sessionCost = this.getSessionCostString();
|
|
536
|
-
const cwd = formatCwd(this.deps.env.cwd());
|
|
537
|
-
const duration = this.getTurnDurationString();
|
|
538
|
-
const personaName = this.currentPersona.label || this.currentPersona.id;
|
|
539
|
-
this.view.updateStatus({
|
|
540
|
-
footer: {
|
|
541
|
-
contextUsage,
|
|
542
|
-
sessionCost,
|
|
543
|
-
duration,
|
|
544
|
-
riskLevel: this.riskLevel,
|
|
545
|
-
commandHint: this.getActiveCommandHint(),
|
|
546
|
-
},
|
|
547
|
-
editor: {
|
|
548
|
-
mode: this.getInputMode(),
|
|
549
|
-
cwdLabel: cwd,
|
|
550
|
-
personaName,
|
|
551
|
-
reasoningLabel,
|
|
552
|
-
reasoning: this.currentPersona.settings.reasoning,
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
getInputMode() {
|
|
557
|
-
if (this.listenRecording)
|
|
558
|
-
return "recording";
|
|
559
|
-
if (this.isBashIncognito)
|
|
560
|
-
return "bash_incognito";
|
|
561
|
-
if (this.isBashMode)
|
|
562
|
-
return "bash";
|
|
563
|
-
if (this.isMemoryMode)
|
|
564
|
-
return "memory";
|
|
565
|
-
return "normal";
|
|
566
|
-
}
|
|
567
|
-
getActiveCommandHint() {
|
|
568
|
-
return this.diffReviewService.getCommandHint(this.autoCompactionStatusHint ?? this.speechStatusHint ?? this.commandHint);
|
|
569
|
-
}
|
|
570
|
-
// Context & Cost Tracking -----------------------------------------------------------------------
|
|
571
|
-
getContextUsageString() {
|
|
572
|
-
const last = this.getLastAssistantMessage();
|
|
573
|
-
const windowTokens = last
|
|
574
|
-
? this.getContextWindowForLastTurn(last)
|
|
575
|
-
: this.currentPersona.model.contextWindow;
|
|
576
|
-
const { input, read, write, output } = this.getSessionTotals();
|
|
577
|
-
const stats = `↑${formatTokenWindow(input)} ↓${formatTokenWindow(output)} (r${formatTokenWindow(read)} w${formatTokenWindow(write)})`;
|
|
578
|
-
const contextWindowUsageTokens = last
|
|
579
|
-
? (last.usage?.input ?? 0) +
|
|
580
|
-
(last.usage?.cacheRead ?? 0) +
|
|
581
|
-
(last.usage?.cacheWrite ?? 0) +
|
|
582
|
-
(last.usage?.output ?? 0)
|
|
583
|
-
: 0;
|
|
584
|
-
const percent = windowTokens > 0 ? (contextWindowUsageTokens / windowTokens) * 100 : 0;
|
|
585
|
-
const percentStr = `${formatAdaptiveNumber(percent, 1, 3)}%`;
|
|
586
|
-
return `${stats} · ${percentStr}/${formatTokenWindow(windowTokens)}`;
|
|
587
|
-
}
|
|
588
|
-
getSessionCostString() {
|
|
589
|
-
let total = 0;
|
|
590
|
-
for (const m of this.engine.history) {
|
|
591
|
-
if (m.role === "assistant") {
|
|
592
|
-
total += m.usage?.cost?.total ?? 0;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return `$${formatAdaptiveNumber(total + this.view.getToolUiCostTotal(), 2, 5)}`;
|
|
596
|
-
}
|
|
597
|
-
getTurnDurationString() {
|
|
598
|
-
const now = Date.now();
|
|
599
|
-
const elapsed = this.currentTurnStartedAt !== undefined
|
|
600
|
-
? Math.max(0, now - this.currentTurnStartedAt)
|
|
601
|
-
: Math.max(0, this.lastTurnDurationMs);
|
|
602
|
-
return this.formatDurationMs(elapsed);
|
|
603
|
-
}
|
|
604
|
-
formatDurationMs(ms) {
|
|
605
|
-
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
606
|
-
const seconds = totalSeconds % 60;
|
|
607
|
-
const minutesTotal = Math.floor(totalSeconds / 60);
|
|
608
|
-
const minutes = minutesTotal % 60;
|
|
609
|
-
const hours = Math.floor(minutesTotal / 60);
|
|
610
|
-
if (hours > 0) {
|
|
611
|
-
return `${hours}h ${minutes}m ${seconds}s`;
|
|
612
|
-
}
|
|
613
|
-
if (minutes > 0) {
|
|
614
|
-
return `${minutes}m ${seconds}s`;
|
|
615
|
-
}
|
|
616
|
-
return `${seconds}s`;
|
|
617
|
-
}
|
|
618
|
-
startTurnTimer() {
|
|
619
|
-
this.currentTurnStartedAt = Date.now();
|
|
620
|
-
this.lastTurnDurationMs = 0;
|
|
621
|
-
if (this.turnTimer) {
|
|
622
|
-
clearInterval(this.turnTimer);
|
|
623
|
-
}
|
|
624
|
-
this.turnTimer = setInterval(() => this.refreshStatus(), 1000);
|
|
625
|
-
}
|
|
626
|
-
stopTurnTimer() {
|
|
627
|
-
if (this.currentTurnStartedAt !== undefined) {
|
|
628
|
-
this.lastTurnDurationMs = Math.max(0, Date.now() - this.currentTurnStartedAt);
|
|
629
|
-
}
|
|
630
|
-
this.currentTurnStartedAt = undefined;
|
|
631
|
-
if (this.turnTimer) {
|
|
632
|
-
clearInterval(this.turnTimer);
|
|
633
|
-
this.turnTimer = undefined;
|
|
634
|
-
}
|
|
635
|
-
this.refreshStatus();
|
|
636
|
-
}
|
|
637
|
-
getSessionTotals() {
|
|
638
|
-
let input = 0;
|
|
639
|
-
let read = 0;
|
|
640
|
-
let write = 0;
|
|
641
|
-
let output = 0;
|
|
642
|
-
for (const m of this.engine.history) {
|
|
643
|
-
if (m.role === "assistant") {
|
|
644
|
-
const usage = m.usage;
|
|
645
|
-
input += usage?.input ?? 0;
|
|
646
|
-
read += usage?.cacheRead ?? 0;
|
|
647
|
-
write += usage?.cacheWrite ?? 0;
|
|
648
|
-
output += usage?.output ?? 0;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
return { input, read, write, output };
|
|
652
|
-
}
|
|
653
|
-
getLastAssistantMessage() {
|
|
654
|
-
const history = this.engine.history;
|
|
655
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
656
|
-
const m = history[i];
|
|
657
|
-
if (m?.role === "assistant")
|
|
658
|
-
return m;
|
|
659
|
-
}
|
|
660
|
-
return undefined;
|
|
661
|
-
}
|
|
662
|
-
getContextWindowForLastTurn(last) {
|
|
663
|
-
const exactPersona = this.personas.find((p) => p.model.provider === last.provider && p.model.id === last.model);
|
|
664
|
-
return exactPersona?.model.contextWindow ?? this.currentPersona.model.contextWindow;
|
|
665
|
-
}
|
|
666
|
-
// Reasoning Level Management --------------------------------------------------------------------
|
|
667
|
-
cycleReasoningLevel() {
|
|
668
|
-
const allowed = this.getAllowedReasoningLevels(this.currentPersona);
|
|
669
|
-
const current = (this.currentPersona.settings.reasoning ?? allowed[0]);
|
|
670
|
-
const index = allowed.indexOf(current);
|
|
671
|
-
const next = allowed[(index + 1) % allowed.length];
|
|
672
|
-
this.currentPersona.settings.reasoning = next;
|
|
673
|
-
this.refreshStatus();
|
|
674
|
-
}
|
|
675
|
-
isReasoningEffort(value) {
|
|
676
|
-
return typeof value === "string" && REASONING_LEVELS.includes(value);
|
|
677
|
-
}
|
|
678
|
-
getAllowedReasoningLevels(persona) {
|
|
679
|
-
if (!persona.model.reasoning) {
|
|
680
|
-
return ["none"];
|
|
681
|
-
}
|
|
682
|
-
const raw = persona.allowedReasoningLevels;
|
|
683
|
-
if (!raw || raw.length === 0) {
|
|
684
|
-
return REASONING_LEVELS;
|
|
685
|
-
}
|
|
686
|
-
const normalized = raw.filter((level) => this.isReasoningEffort(level));
|
|
687
|
-
const unique = [...new Set(normalized)];
|
|
688
|
-
return unique.length ? unique : REASONING_LEVELS;
|
|
689
|
-
}
|
|
690
|
-
createSessionPersona(persona) {
|
|
691
|
-
return {
|
|
692
|
-
...persona,
|
|
693
|
-
settings: { ...persona.settings },
|
|
694
|
-
allowedReasoningLevels: persona.allowedReasoningLevels
|
|
695
|
-
? [...persona.allowedReasoningLevels]
|
|
696
|
-
: undefined,
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
clampPersonaReasoning(persona) {
|
|
700
|
-
const allowed = this.getAllowedReasoningLevels(persona);
|
|
701
|
-
const desired = persona.settings.reasoning;
|
|
702
|
-
if (!desired || !allowed.includes(desired)) {
|
|
703
|
-
persona.settings.reasoning = allowed[0];
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
// Risk Level Management -------------------------------------------------------------------------
|
|
707
|
-
cycleRiskLevel() {
|
|
708
|
-
const allowed = ALLOWED_RISK_LEVELS;
|
|
709
|
-
const index = allowed.indexOf(this.riskLevel);
|
|
710
|
-
const next = allowed[(index + 1) % allowed.length] ?? "read-only";
|
|
711
|
-
this.setRiskLevel(next);
|
|
712
|
-
}
|
|
713
|
-
cyclePersonality() {
|
|
714
|
-
const index = this.personas.findIndex((persona) => persona.id.toLowerCase() === this.currentPersona.id.toLowerCase());
|
|
715
|
-
const next = this.personas[(index + 1) % this.personas.length] ?? this.personas[0];
|
|
716
|
-
this.switchPersona(next.id);
|
|
717
|
-
}
|
|
718
|
-
getVisibleSubagentsForPersona(persona) {
|
|
719
|
-
if (!persona.subagents)
|
|
720
|
-
return [];
|
|
721
|
-
return Object.keys(persona.subagents);
|
|
722
|
-
}
|
|
723
|
-
getSkillsIndexBlockForPersona(persona) {
|
|
724
|
-
const resolved = resolvePersonaSkillsForPromptContext({
|
|
725
|
-
persona,
|
|
726
|
-
discoveredSkills: this.skills,
|
|
727
|
-
});
|
|
728
|
-
return { skillsBlock: resolved.skillsBlock, unknown: resolved.unknown };
|
|
729
|
-
}
|
|
730
|
-
// User Actions ----------------------------------------------------------------------------------
|
|
731
|
-
toggleThinkingVisibility() {
|
|
732
|
-
this.showThinking = !this.showThinking;
|
|
733
|
-
this.view.setThinkingVisibility(this.showThinking);
|
|
734
|
-
if (this.showThinking && this.assistantState) {
|
|
735
|
-
const { model } = this.assistantState;
|
|
736
|
-
if (!this.assistantState.inserted &&
|
|
737
|
-
model.type === "assistant_partial" &&
|
|
738
|
-
model.text.length === 0 &&
|
|
739
|
-
model.thinking?.trim().length) {
|
|
740
|
-
this.ensureAssistantInserted(this.assistantState);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const message = this.showThinking ? "thoughts visible" : "thoughts hidden";
|
|
744
|
-
this.view.addSystemMessage(message, "success");
|
|
745
|
-
}
|
|
746
|
-
toggleCompactToolUi() {
|
|
747
|
-
this.compactToolUi = !this.compactToolUi;
|
|
748
|
-
this.view.setCompactToolUi(this.compactToolUi);
|
|
749
|
-
const message = this.compactToolUi ? "compact tool ui enabled" : "compact tool ui disabled";
|
|
750
|
-
this.view.addSystemMessage(message, "success");
|
|
751
|
-
}
|
|
752
|
-
changeDirectory(rawPath) {
|
|
753
|
-
const normalized = this.normalizeCdInput(rawPath);
|
|
754
|
-
if (!normalized) {
|
|
755
|
-
this.view.addSystemMessage("missing path for /cd", "warn");
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
const currentCwd = this.deps.env.cwd();
|
|
759
|
-
const home = this.deps.env.home();
|
|
760
|
-
const resolved = this.resolveCdPath(normalized, currentCwd, home);
|
|
761
|
-
let stats;
|
|
762
|
-
try {
|
|
763
|
-
stats = statSync(resolved);
|
|
764
|
-
}
|
|
765
|
-
catch {
|
|
766
|
-
this.view.addSystemMessage(`directory not found: ${normalized}`, "error");
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (!stats.isDirectory()) {
|
|
770
|
-
this.view.addSystemMessage(`not a directory: ${normalized}`, "error");
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
try {
|
|
774
|
-
process.chdir(resolved);
|
|
775
|
-
}
|
|
776
|
-
catch (err) {
|
|
777
|
-
this.view.addSystemMessage(`failed to change directory: ${err.message}`, "error");
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
const nextCwd = this.deps.env.cwd();
|
|
781
|
-
const previousAgentCwd = this.agentCwd;
|
|
782
|
-
const previousProjectContextBlock = this.projectContextBlock;
|
|
783
|
-
this.agentCwd = nextCwd;
|
|
784
|
-
this.refreshProjectContext(nextCwd);
|
|
785
|
-
this.updatePendingProjectContextChange(previousProjectContextBlock, this.projectContextBlock);
|
|
786
|
-
this.projectFiles = [];
|
|
787
|
-
this.refreshProjectFilesInBackground();
|
|
788
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
789
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
790
|
-
this.rebuildSubagentPrompts();
|
|
791
|
-
this.refreshStatus();
|
|
792
|
-
const from = this.pendingCwdChange?.from ?? previousAgentCwd;
|
|
793
|
-
if (from === this.agentCwd) {
|
|
794
|
-
this.pendingCwdChange = undefined;
|
|
795
|
-
}
|
|
796
|
-
else {
|
|
797
|
-
this.pendingCwdChange = { from, to: this.agentCwd };
|
|
798
|
-
}
|
|
799
|
-
this.view.addSystemMessage(`working directory set to ${formatCwd(nextCwd)}`, "success");
|
|
800
|
-
}
|
|
801
|
-
normalizeCdInput(rawPath) {
|
|
802
|
-
const trimmed = rawPath.trim();
|
|
803
|
-
if (!trimmed)
|
|
804
|
-
return "";
|
|
805
|
-
const quote = trimmed[0];
|
|
806
|
-
if ((quote === '"' || quote === "'") && trimmed.endsWith(quote) && trimmed.length > 1) {
|
|
807
|
-
return trimmed.slice(1, -1);
|
|
808
|
-
}
|
|
809
|
-
return trimmed;
|
|
810
|
-
}
|
|
811
|
-
resolveCdPath(input, cwd, home) {
|
|
812
|
-
if (input === "~")
|
|
813
|
-
return home;
|
|
814
|
-
if (input.startsWith("~/"))
|
|
815
|
-
return join(home, input.slice(2));
|
|
816
|
-
return resolve(cwd, input);
|
|
817
|
-
}
|
|
818
|
-
refreshProjectContext(cwd) {
|
|
819
|
-
const projectContext = resolveProjectContextForPromptContext({
|
|
820
|
-
cwd,
|
|
821
|
-
home: this.deps.env.home(),
|
|
822
|
-
includeAgentContext: this.includeAgentContext,
|
|
823
|
-
readFile: this.deps.fs.readFile,
|
|
824
|
-
});
|
|
825
|
-
this.agentsFiles = projectContext.agentsFiles;
|
|
826
|
-
this.agentsConfigErrors = projectContext.warnings;
|
|
827
|
-
this.projectContextBlock = projectContext.projectContextBlock;
|
|
828
|
-
}
|
|
829
|
-
updatePendingProjectContextChange(previous, next) {
|
|
830
|
-
if (previous === next) {
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
const from = this.pendingProjectContextChange?.from ?? previous;
|
|
834
|
-
if (from === next) {
|
|
835
|
-
this.pendingProjectContextChange = undefined;
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
this.pendingProjectContextChange = { from, to: next };
|
|
839
|
-
}
|
|
840
|
-
createAbortBusyTask() {
|
|
841
|
-
return this.interruptLifecycle.createAbortBusyTask();
|
|
842
|
-
}
|
|
843
|
-
beginBusyTask(task) {
|
|
844
|
-
this.interruptLifecycle.beginBusyTask(task);
|
|
845
|
-
}
|
|
846
|
-
endBusyTask(task) {
|
|
847
|
-
this.interruptLifecycle.endBusyTask(task);
|
|
848
|
-
}
|
|
849
|
-
interruptActiveTask() {
|
|
850
|
-
if (!this.interruptLifecycle.interruptActiveTask()) {
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
this.view.addSystemMessage("interrupted", "error");
|
|
854
|
-
}
|
|
855
|
-
async runMaintenanceStreamingTask(task, onSettled) {
|
|
856
|
-
this.isStreaming = true;
|
|
857
|
-
this.view.startWorkingIcon();
|
|
858
|
-
const { busyTask, signal } = this.createAbortBusyTask();
|
|
859
|
-
this.beginBusyTask(busyTask);
|
|
860
|
-
let outcome;
|
|
861
|
-
try {
|
|
862
|
-
const value = await task(signal);
|
|
863
|
-
outcome = { aborted: signal.aborted, value };
|
|
864
|
-
}
|
|
865
|
-
catch (error) {
|
|
866
|
-
outcome = { aborted: signal.aborted, error };
|
|
867
|
-
}
|
|
868
|
-
try {
|
|
869
|
-
if (onSettled) {
|
|
870
|
-
await onSettled(outcome);
|
|
871
|
-
}
|
|
872
|
-
return outcome;
|
|
873
|
-
}
|
|
874
|
-
finally {
|
|
875
|
-
this.endBusyTask(busyTask);
|
|
876
|
-
this.view.stopWorkingIcon();
|
|
877
|
-
this.isStreaming = false;
|
|
878
|
-
this.view.requestRender();
|
|
879
|
-
if (signal.aborted) {
|
|
880
|
-
this.dequeueQueuedUserMessagesIntoEditor();
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
void this.drainQueuedUserMessages();
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// Input Handling --------------------------------------------------------------------------------
|
|
888
|
-
beforeSubmit(text) {
|
|
889
|
-
if (this.listenRecording)
|
|
890
|
-
return false;
|
|
891
|
-
if (!this.isStreaming)
|
|
892
|
-
return true;
|
|
893
|
-
const trimmed = text.trimStart();
|
|
894
|
-
if (trimmed.startsWith("!")) {
|
|
895
|
-
return false;
|
|
896
|
-
}
|
|
897
|
-
if (trimmed.startsWith("/") && this.isSingleLineInput(text)) {
|
|
898
|
-
const parsed = this.commandRegistry.parse(trimmed);
|
|
899
|
-
if (parsed.type === "unknown") {
|
|
900
|
-
return true;
|
|
901
|
-
}
|
|
902
|
-
return this.commandRegistry.allowsDuringStreaming(parsed);
|
|
903
|
-
}
|
|
904
|
-
return true;
|
|
905
|
-
}
|
|
906
|
-
isSingleLineInput(text) {
|
|
907
|
-
return !/[\r\n]/.test(text);
|
|
908
|
-
}
|
|
909
|
-
handleEditorChange(text) {
|
|
910
|
-
const wasBash = this.isBashMode;
|
|
911
|
-
const wasBashIncognito = this.isBashIncognito;
|
|
912
|
-
const wasMemory = this.isMemoryMode;
|
|
913
|
-
const wasInFileAutocomplete = this.isInFileAutocomplete;
|
|
914
|
-
const previousCommandHint = this.commandHint;
|
|
915
|
-
if (text.trim().length > 0) {
|
|
916
|
-
this.lastEmptySubmitAt = undefined;
|
|
917
|
-
}
|
|
918
|
-
const trimmed = text.trimStart();
|
|
919
|
-
const isSingleLine = this.isSingleLineInput(text);
|
|
920
|
-
const isIncognito = trimmed.startsWith("!!");
|
|
921
|
-
this.isBashIncognito = isIncognito;
|
|
922
|
-
this.isBashMode = trimmed.startsWith("!") && !isIncognito;
|
|
923
|
-
this.isMemoryMode = isSingleLine && trimmed.startsWith("#");
|
|
924
|
-
const beforeCursor = this.getEditorTextBeforeCursor();
|
|
925
|
-
this.isInFileAutocomplete = Boolean(getFileAutocompleteToken(beforeCursor));
|
|
926
|
-
if (!wasInFileAutocomplete && this.isInFileAutocomplete) {
|
|
927
|
-
this.refreshProjectFilesInBackground();
|
|
928
|
-
}
|
|
929
|
-
this.commandHint = this.getCommandHintForInput(text);
|
|
930
|
-
const commandHintChanged = this.commandHint !== previousCommandHint;
|
|
931
|
-
if (wasBash !== this.isBashMode ||
|
|
932
|
-
wasBashIncognito !== this.isBashIncognito ||
|
|
933
|
-
wasMemory !== this.isMemoryMode ||
|
|
934
|
-
commandHintChanged) {
|
|
935
|
-
this.refreshStatus();
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
getCommandHintForInput(text) {
|
|
939
|
-
const trimmed = text.trimStart();
|
|
940
|
-
if (!trimmed.startsWith("/") || !this.isSingleLineInput(text)) {
|
|
941
|
-
return undefined;
|
|
942
|
-
}
|
|
943
|
-
const parsed = this.commandRegistry.parse(trimmed);
|
|
944
|
-
if (parsed.type === "unknown") {
|
|
945
|
-
return undefined;
|
|
946
|
-
}
|
|
947
|
-
return this.getCommandHint(parsed);
|
|
948
|
-
}
|
|
949
|
-
getCommandHint(command) {
|
|
950
|
-
switch (command.type) {
|
|
951
|
-
case "help":
|
|
952
|
-
return "show available commands";
|
|
953
|
-
case "copyText":
|
|
954
|
-
return "copy last assistant message";
|
|
955
|
-
case "copyCode":
|
|
956
|
-
return "copy last assistant code blocks";
|
|
957
|
-
case "checkpoint":
|
|
958
|
-
return "save a checkpoint file";
|
|
959
|
-
case "new":
|
|
960
|
-
return "clear the session and start fresh";
|
|
961
|
-
case "rewind":
|
|
962
|
-
return "rewind context to a selected prior user message";
|
|
963
|
-
case "cd":
|
|
964
|
-
return "change directory: /cd <path>";
|
|
965
|
-
case "diff":
|
|
966
|
-
return "open the external diff review tool: /diff [git diff args...]";
|
|
967
|
-
case "compactSummaryOnly":
|
|
968
|
-
return "summarize session and start new, optional prompt";
|
|
969
|
-
case "compactSummaryAndLast":
|
|
970
|
-
return "summarize session and keep last turn, optional prompt";
|
|
971
|
-
case "pruneEarliest":
|
|
972
|
-
return "prune earliest tool results and compact edit calls, optional fraction 0-1";
|
|
973
|
-
case "pruneLargest":
|
|
974
|
-
return "prune largest tool results and compact edit calls, optional fraction 0-1";
|
|
975
|
-
case "pruneSmart":
|
|
976
|
-
return "prune smart-selected tool results and compact edit calls, optional fraction and guidance";
|
|
977
|
-
case "reload":
|
|
978
|
-
return "reload prompts, skills, themes, bash commands, and AGENTS.md";
|
|
979
|
-
case "listen":
|
|
980
|
-
return "start microphone recording and transcribe to editor (macOS only)";
|
|
981
|
-
case "speak":
|
|
982
|
-
return "speak the last assistant message aloud (macOS only)";
|
|
983
|
-
case "risk":
|
|
984
|
-
return "set risk level: /risk:read-only or /risk:read-write";
|
|
985
|
-
case "bash":
|
|
986
|
-
return "run saved bash command: /bash:<id>";
|
|
987
|
-
case "persona":
|
|
988
|
-
return "switch persona: /persona:<id>";
|
|
989
|
-
case "prompt":
|
|
990
|
-
return "insert prompt template: /prompt:<id>";
|
|
991
|
-
case "theme":
|
|
992
|
-
return "switch theme: /theme:<id>";
|
|
993
|
-
case "unknown":
|
|
994
|
-
return undefined;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
getEditorTextBeforeCursor() {
|
|
998
|
-
const { line, col } = this.view.getEditorCursor();
|
|
999
|
-
const lines = this.view.getEditorLines();
|
|
1000
|
-
const current = lines[line] ?? "";
|
|
1001
|
-
return current.slice(0, col);
|
|
1002
|
-
}
|
|
1003
|
-
refreshProjectFilesInBackground() {
|
|
1004
|
-
const cwd = this.deps.env.cwd();
|
|
1005
|
-
if (this.isRefreshingProjectFiles && this.projectFilesCwd === cwd)
|
|
1006
|
-
return;
|
|
1007
|
-
this.isRefreshingProjectFiles = true;
|
|
1008
|
-
this.projectFilesCwd = cwd;
|
|
1009
|
-
void listProjectFilesAsync(cwd)
|
|
1010
|
-
.then((files) => {
|
|
1011
|
-
if (this.projectFilesCwd !== cwd)
|
|
1012
|
-
return;
|
|
1013
|
-
this.projectFiles = files;
|
|
1014
|
-
this.view.requestRender();
|
|
1015
|
-
})
|
|
1016
|
-
.catch(() => {
|
|
1017
|
-
// Ignore refresh errors; autocomplete will keep using the existing cache.
|
|
1018
|
-
})
|
|
1019
|
-
.finally(() => {
|
|
1020
|
-
if (this.projectFilesCwd === cwd) {
|
|
1021
|
-
this.isRefreshingProjectFiles = false;
|
|
1022
|
-
}
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
queueUserMessage(text) {
|
|
1026
|
-
this.queuedMessageBuffer.enqueue(text, () => this.view.requestRender());
|
|
1027
|
-
}
|
|
1028
|
-
popQueuedUserMessageIntoEditor() {
|
|
1029
|
-
this.queuedMessageBuffer.popIntoEditor({
|
|
1030
|
-
getEditorText: () => this.view.getEditorText(),
|
|
1031
|
-
setEditorText: (text) => this.view.setEditorText(text),
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
dequeueQueuedUserMessagesIntoEditor() {
|
|
1035
|
-
this.queuedMessageBuffer.dequeueIntoEditor({
|
|
1036
|
-
getEditorText: () => this.view.getEditorText(),
|
|
1037
|
-
setEditorText: (text) => this.view.setEditorText(text),
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
cycleSubagentSelection() {
|
|
1041
|
-
this.view.cycleSubagentSelection(1);
|
|
1042
|
-
this.view.requestRender();
|
|
1043
|
-
}
|
|
1044
|
-
terminateSelectedSubagent() {
|
|
1045
|
-
const selectedId = this.view.getSelectedSubagentId();
|
|
1046
|
-
if (!selectedId) {
|
|
1047
|
-
this.view.addSystemMessage("no active subagent selected", "warn");
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
void this.engine
|
|
1051
|
-
.terminateSubagent(selectedId)
|
|
1052
|
-
.then((found) => {
|
|
1053
|
-
if (!found) {
|
|
1054
|
-
this.view.addSystemMessage(`unknown subagent id: ${selectedId}`, "warn");
|
|
1055
|
-
}
|
|
1056
|
-
})
|
|
1057
|
-
.catch((err) => {
|
|
1058
|
-
this.view.addSystemMessage(`failed to terminate subagent: ${err.message}`, "error");
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
buildIdleNotificationTitle() {
|
|
1062
|
-
const baseTitle = "tau is waiting for your input";
|
|
1063
|
-
const lastAssistant = this.getLastAssistantMessage();
|
|
1064
|
-
if (!lastAssistant)
|
|
1065
|
-
return baseTitle;
|
|
1066
|
-
const rawText = extractAssistantText(lastAssistant).trimStart();
|
|
1067
|
-
if (!rawText)
|
|
1068
|
-
return baseTitle;
|
|
1069
|
-
const firstLine = rawText.split(/\r?\n/, 1)[0]?.trimEnd() ?? "";
|
|
1070
|
-
const headingMatch = firstLine.match(/^#{1,6}\s+(.+)$/);
|
|
1071
|
-
if (headingMatch) {
|
|
1072
|
-
const heading = headingMatch[1].trim();
|
|
1073
|
-
if (!heading)
|
|
1074
|
-
return baseTitle;
|
|
1075
|
-
const summary = heading.length > 60 ? `${heading.slice(0, 57)}...` : heading;
|
|
1076
|
-
return summary;
|
|
1077
|
-
}
|
|
1078
|
-
if (/^[A-Za-z0-9]/.test(rawText)) {
|
|
1079
|
-
const plainText = rawText.replace(/\s+/g, " ").trim();
|
|
1080
|
-
if (!plainText)
|
|
1081
|
-
return baseTitle;
|
|
1082
|
-
const summary = plainText.length > 60 ? `${plainText.slice(0, 57)}...` : plainText;
|
|
1083
|
-
return summary;
|
|
1084
|
-
}
|
|
1085
|
-
return baseTitle;
|
|
1086
|
-
}
|
|
1087
|
-
async drainQueuedUserMessages() {
|
|
1088
|
-
await this.queuedMessageBuffer.drain({
|
|
1089
|
-
isStreaming: () => this.isStreaming,
|
|
1090
|
-
onUserInput: (text) => this.onUserInput(text),
|
|
1091
|
-
requestRender: () => this.view.requestRender(),
|
|
1092
|
-
sendTerminalNotification: (title) => this.view.sendTerminalNotification(title),
|
|
1093
|
-
buildIdleNotificationTitle: () => this.buildIdleNotificationTitle(),
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
async handleSubmit(text) {
|
|
1097
|
-
const trimmed = text.trim();
|
|
1098
|
-
const isSingleLine = this.isSingleLineInput(text);
|
|
1099
|
-
if (this.diffReviewService.isActive()) {
|
|
1100
|
-
if (trimmed) {
|
|
1101
|
-
this.view.addSystemMessage("diff review is active. finish it in the diff tool or press esc to cancel.", "warn");
|
|
1102
|
-
}
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
if (!trimmed) {
|
|
1106
|
-
if (this.isStreaming)
|
|
1107
|
-
return;
|
|
1108
|
-
const now = Date.now();
|
|
1109
|
-
if (this.lastEmptySubmitAt !== undefined &&
|
|
1110
|
-
now - this.lastEmptySubmitAt <= DOUBLE_PRESS_WINDOW_MS) {
|
|
1111
|
-
this.lastEmptySubmitAt = undefined;
|
|
1112
|
-
await this.runAssistantTurn();
|
|
1113
|
-
}
|
|
1114
|
-
else {
|
|
1115
|
-
this.lastEmptySubmitAt = now;
|
|
1116
|
-
}
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
this.lastEmptySubmitAt = undefined;
|
|
1120
|
-
if (this.isStreaming) {
|
|
1121
|
-
if (trimmed.startsWith("/") && isSingleLine) {
|
|
1122
|
-
const parsed = this.commandRegistry.parse(trimmed);
|
|
1123
|
-
if (parsed.type !== "unknown") {
|
|
1124
|
-
if (this.commandRegistry.allowsDuringStreaming(parsed)) {
|
|
1125
|
-
await this.commandRegistry.dispatch(parsed, this.commandHandlers);
|
|
1126
|
-
}
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
if (trimmed.startsWith("!")) {
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
this.queueUserMessage(trimmed);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
if (trimmed.startsWith("/") && isSingleLine) {
|
|
1137
|
-
const parsed = this.commandRegistry.parse(trimmed);
|
|
1138
|
-
if (parsed.type !== "unknown") {
|
|
1139
|
-
await this.handleCommand(trimmed);
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
if (trimmed.startsWith("!!")) {
|
|
1144
|
-
const command = trimmed.slice(2).trim();
|
|
1145
|
-
if (command) {
|
|
1146
|
-
await this.runBashCommand(command, { addToContext: false, labelOverride: "incognito" });
|
|
1147
|
-
}
|
|
1148
|
-
return;
|
|
1149
|
-
}
|
|
1150
|
-
if (trimmed.startsWith("!")) {
|
|
1151
|
-
const command = trimmed.slice(1).trim();
|
|
1152
|
-
if (command)
|
|
1153
|
-
await this.runBashCommand(command);
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (isSingleLine && trimmed.startsWith("#")) {
|
|
1157
|
-
const request = trimmed.slice(1).trim();
|
|
1158
|
-
if (!request) {
|
|
1159
|
-
this.view.addSystemMessage("memory mode request was empty.", "warn");
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
const agentsFilePath = this.getMemoryModeFilePath();
|
|
1163
|
-
const textForModel = this.formatMemoryModeUserMessage(agentsFilePath, request);
|
|
1164
|
-
await this.sendUserMessage(request, { textForModel, kind: "memory" });
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
await this.sendUserMessage(trimmed);
|
|
1168
|
-
}
|
|
1169
|
-
async sendUserMessage(text, opts) {
|
|
1170
|
-
this.addUserMessageToMainSession(text, opts);
|
|
1171
|
-
await this.runAssistantTurn();
|
|
1172
|
-
}
|
|
1173
|
-
addUserMessageToMainSession(text, opts) {
|
|
1174
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1175
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1176
|
-
const rawTextForModel = opts?.textForModel ?? text;
|
|
1177
|
-
const textForModel = opts?.includePendingNotices === false
|
|
1178
|
-
? rawTextForModel
|
|
1179
|
-
: this.applyPendingUserNotices(rawTextForModel);
|
|
1180
|
-
const historyEntryId = this.engine.addUserText(textForModel, {
|
|
1181
|
-
historyEntryId: opts?.historyEntryId,
|
|
1182
|
-
});
|
|
1183
|
-
if (opts?.renderInView !== false) {
|
|
1184
|
-
this.view.addMessage({
|
|
1185
|
-
type: "user",
|
|
1186
|
-
text,
|
|
1187
|
-
...(opts?.kind ? { kind: opts.kind } : {}),
|
|
1188
|
-
}, historyEntryId);
|
|
1189
|
-
}
|
|
1190
|
-
return historyEntryId;
|
|
1191
|
-
}
|
|
1192
|
-
applyPendingUserNotices(textForModel) {
|
|
1193
|
-
const notices = [];
|
|
1194
|
-
if (this.pendingRiskLevelChange) {
|
|
1195
|
-
notices.push(formatRiskLevelChangeNotice(this.pendingRiskLevelChange));
|
|
1196
|
-
}
|
|
1197
|
-
if (this.pendingCwdChange) {
|
|
1198
|
-
notices.push(formatCwdChangeNotice(this.pendingCwdChange));
|
|
1199
|
-
}
|
|
1200
|
-
if (this.pendingProjectContextChange) {
|
|
1201
|
-
notices.push(formatProjectContextChangeNotice({
|
|
1202
|
-
projectContextBlock: this.pendingProjectContextChange.to,
|
|
1203
|
-
}));
|
|
1204
|
-
}
|
|
1205
|
-
this.pendingRiskLevelChange = undefined;
|
|
1206
|
-
this.pendingCwdChange = undefined;
|
|
1207
|
-
this.pendingProjectContextChange = undefined;
|
|
1208
|
-
if (notices.length === 0) {
|
|
1209
|
-
return textForModel;
|
|
1210
|
-
}
|
|
1211
|
-
return `${notices.join("\n")}\n\n${textForModel}`;
|
|
1212
|
-
}
|
|
1213
|
-
async sendInitialUserMessage(text) {
|
|
1214
|
-
const trimmed = text.trim();
|
|
1215
|
-
if (!trimmed || this.isStreaming)
|
|
1216
|
-
return;
|
|
1217
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1218
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1219
|
-
const historyEntryId = this.engine.addUserText(trimmed);
|
|
1220
|
-
this.view.addMessage({ type: "user", text: trimmed }, historyEntryId);
|
|
1221
|
-
await this.runAssistantTurn();
|
|
1222
|
-
}
|
|
1223
|
-
async toggleListenCapture() {
|
|
1224
|
-
if (this.listenTransition) {
|
|
1225
|
-
this.view.addSystemMessage("speech recording state change already in progress", "warn");
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
if (this.listenRecording) {
|
|
1229
|
-
await this.runListenTransition(() => this.stopListenCapture());
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
await this.startListenCaptureFromCommand();
|
|
1233
|
-
}
|
|
1234
|
-
async startListenCaptureFromCommand() {
|
|
1235
|
-
if (this.listenTransition) {
|
|
1236
|
-
this.view.addSystemMessage("speech recording state change already in progress", "warn");
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
if (this.listenRecording) {
|
|
1240
|
-
this.view.addSystemMessage("speech recording already in progress", "warn");
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (this.isTranscribingListen) {
|
|
1244
|
-
this.view.addSystemMessage("speech transcription already in progress", "warn");
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
await this.runListenTransition(() => this.startListenCapture());
|
|
1248
|
-
}
|
|
1249
|
-
async runListenTransition(task) {
|
|
1250
|
-
if (this.listenTransition) {
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
const transition = task();
|
|
1254
|
-
this.listenTransition = transition;
|
|
1255
|
-
try {
|
|
1256
|
-
await transition;
|
|
1257
|
-
}
|
|
1258
|
-
finally {
|
|
1259
|
-
if (this.listenTransition === transition) {
|
|
1260
|
-
this.listenTransition = undefined;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
async startListenCapture() {
|
|
1265
|
-
if (this.deps.env.platform() !== "darwin") {
|
|
1266
|
-
this.view.addSystemMessage("/listen is currently supported only on macOS.", "warn");
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
const apiKey = this.getSpeechToTextApiKey();
|
|
1270
|
-
if (!apiKey) {
|
|
1271
|
-
this.view.addSystemMessage(this.getSpeechToTextApiKeyErrorMessage("use /listen"), "error");
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
let audioPath;
|
|
1275
|
-
try {
|
|
1276
|
-
audioPath = await this.createTempFilePath(LISTEN_TEMP_FILE_TEMPLATE);
|
|
1277
|
-
const abortController = new AbortController();
|
|
1278
|
-
const completion = this.deps.spawn("ffmpeg", [
|
|
1279
|
-
"-hide_banner",
|
|
1280
|
-
"-loglevel",
|
|
1281
|
-
"error",
|
|
1282
|
-
"-nostdin",
|
|
1283
|
-
"-f",
|
|
1284
|
-
"avfoundation",
|
|
1285
|
-
"-i",
|
|
1286
|
-
":0",
|
|
1287
|
-
"-ac",
|
|
1288
|
-
"1",
|
|
1289
|
-
"-ar",
|
|
1290
|
-
"16000",
|
|
1291
|
-
"-c:a",
|
|
1292
|
-
"pcm_s16le",
|
|
1293
|
-
"-f",
|
|
1294
|
-
"wav",
|
|
1295
|
-
"-y",
|
|
1296
|
-
audioPath,
|
|
1297
|
-
], {
|
|
1298
|
-
detached: true,
|
|
1299
|
-
killProcessGroup: true,
|
|
1300
|
-
signal: abortController.signal,
|
|
1301
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
1302
|
-
});
|
|
1303
|
-
const recording = {
|
|
1304
|
-
audioPath,
|
|
1305
|
-
stopRequested: false,
|
|
1306
|
-
abortController,
|
|
1307
|
-
completion,
|
|
1308
|
-
};
|
|
1309
|
-
recording.maxDurationTimeout = setTimeout(() => {
|
|
1310
|
-
if (this.listenRecording !== recording || this.listenTransition)
|
|
1311
|
-
return;
|
|
1312
|
-
void this.runListenTransition(() => this.stopListenCapture());
|
|
1313
|
-
}, LISTEN_RECORDING_MAX_DURATION_MS);
|
|
1314
|
-
this.listenRecording = recording;
|
|
1315
|
-
this.view.setEditorInputEnabled(false);
|
|
1316
|
-
this.refreshStatus();
|
|
1317
|
-
void this.watchListenRecording(recording);
|
|
1318
|
-
}
|
|
1319
|
-
catch (err) {
|
|
1320
|
-
if (audioPath) {
|
|
1321
|
-
await this.cleanupTempFile(audioPath);
|
|
1322
|
-
}
|
|
1323
|
-
this.view.addSystemMessage(`failed to start recording: ${err.message}`, "error");
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
async stopListenCapture() {
|
|
1327
|
-
const recording = this.listenRecording;
|
|
1328
|
-
if (!recording)
|
|
1329
|
-
return;
|
|
1330
|
-
recording.stopRequested = true;
|
|
1331
|
-
this.clearListenRecordingMaxDurationTimeout(recording);
|
|
1332
|
-
this.listenRecording = undefined;
|
|
1333
|
-
this.view.setEditorInputEnabled(true);
|
|
1334
|
-
this.refreshStatus();
|
|
1335
|
-
recording.abortController.abort();
|
|
1336
|
-
try {
|
|
1337
|
-
await recording.completion;
|
|
1338
|
-
}
|
|
1339
|
-
catch (err) {
|
|
1340
|
-
this.view.addSystemMessage(`recording failed: ${err.message}`, "error");
|
|
1341
|
-
await this.cleanupTempFile(recording.audioPath);
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
this.isTranscribingListen = true;
|
|
1345
|
-
try {
|
|
1346
|
-
const audio = await readFile(recording.audioPath);
|
|
1347
|
-
if (audio.byteLength < LISTEN_RECORDING_MIN_BYTES) {
|
|
1348
|
-
this.view.addSystemMessage("recording too short, try again", "warn");
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
const transcript = await this.transcribeListenAudio(audio);
|
|
1352
|
-
const text = transcript.trim();
|
|
1353
|
-
if (!text) {
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
this.view.insertEditorTextAtCursor(text);
|
|
1357
|
-
}
|
|
1358
|
-
catch (err) {
|
|
1359
|
-
this.view.addSystemMessage(`speech transcription failed: ${err.message}`, "error");
|
|
1360
|
-
}
|
|
1361
|
-
finally {
|
|
1362
|
-
this.isTranscribingListen = false;
|
|
1363
|
-
await this.cleanupTempFile(recording.audioPath);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
async cancelListenCapture() {
|
|
1367
|
-
const recording = this.listenRecording;
|
|
1368
|
-
if (!recording)
|
|
1369
|
-
return;
|
|
1370
|
-
recording.stopRequested = true;
|
|
1371
|
-
this.clearListenRecordingMaxDurationTimeout(recording);
|
|
1372
|
-
this.listenRecording = undefined;
|
|
1373
|
-
this.view.setEditorInputEnabled(true);
|
|
1374
|
-
this.refreshStatus();
|
|
1375
|
-
recording.abortController.abort();
|
|
1376
|
-
try {
|
|
1377
|
-
await recording.completion;
|
|
1378
|
-
}
|
|
1379
|
-
catch {
|
|
1380
|
-
// ignore disposal errors
|
|
1381
|
-
}
|
|
1382
|
-
await this.cleanupTempFile(recording.audioPath);
|
|
1383
|
-
}
|
|
1384
|
-
async watchListenRecording(recording) {
|
|
1385
|
-
try {
|
|
1386
|
-
const result = await recording.completion;
|
|
1387
|
-
this.clearListenRecordingMaxDurationTimeout(recording);
|
|
1388
|
-
if (this.listenRecording !== recording || recording.stopRequested)
|
|
1389
|
-
return;
|
|
1390
|
-
this.listenRecording = undefined;
|
|
1391
|
-
this.view.setEditorInputEnabled(true);
|
|
1392
|
-
this.refreshStatus();
|
|
1393
|
-
const detail = result.exitCode !== null
|
|
1394
|
-
? `ffmpeg exited with code ${result.exitCode}`
|
|
1395
|
-
: result.closeSignal
|
|
1396
|
-
? `ffmpeg terminated by signal ${result.closeSignal}`
|
|
1397
|
-
: "ffmpeg exited";
|
|
1398
|
-
this.view.addSystemMessage(`recording stopped unexpectedly (${detail})`, "error");
|
|
1399
|
-
await this.cleanupTempFile(recording.audioPath);
|
|
1400
|
-
}
|
|
1401
|
-
catch (err) {
|
|
1402
|
-
this.clearListenRecordingMaxDurationTimeout(recording);
|
|
1403
|
-
if (this.listenRecording !== recording || recording.stopRequested)
|
|
1404
|
-
return;
|
|
1405
|
-
this.listenRecording = undefined;
|
|
1406
|
-
this.view.setEditorInputEnabled(true);
|
|
1407
|
-
this.refreshStatus();
|
|
1408
|
-
const error = err;
|
|
1409
|
-
if (error.code === "ENOENT") {
|
|
1410
|
-
this.view.addSystemMessage("ffmpeg not found. install it with: brew install ffmpeg", "error");
|
|
1411
|
-
}
|
|
1412
|
-
else {
|
|
1413
|
-
this.view.addSystemMessage(`recording failed: ${error.message}`, "error");
|
|
1414
|
-
}
|
|
1415
|
-
await this.cleanupTempFile(recording.audioPath);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
clearListenRecordingMaxDurationTimeout(recording) {
|
|
1419
|
-
if (!recording.maxDurationTimeout)
|
|
1420
|
-
return;
|
|
1421
|
-
clearTimeout(recording.maxDurationTimeout);
|
|
1422
|
-
recording.maxDurationTimeout = undefined;
|
|
1423
|
-
}
|
|
1424
|
-
async createTempFilePath(template) {
|
|
1425
|
-
const result = await this.deps.spawn("mktemp", [template]);
|
|
1426
|
-
if (result.exitCode !== 0) {
|
|
1427
|
-
const message = result.stderr.trim() || result.stdout.trim() || "mktemp failed";
|
|
1428
|
-
throw new Error(message);
|
|
1429
|
-
}
|
|
1430
|
-
const path = result.stdout.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
1431
|
-
if (!path) {
|
|
1432
|
-
throw new Error("mktemp returned an empty path");
|
|
1433
|
-
}
|
|
1434
|
-
return path;
|
|
1435
|
-
}
|
|
1436
|
-
getSpeechToTextProvider() {
|
|
1437
|
-
return this.config.speechToText?.provider ?? "mistral";
|
|
1438
|
-
}
|
|
1439
|
-
getSpeechToTextApiKey() {
|
|
1440
|
-
const provider = this.getSpeechToTextProvider();
|
|
1441
|
-
return provider === "gemini"
|
|
1442
|
-
? getGoogleApiKey(this.config, this.deps.env.env())
|
|
1443
|
-
: getMistralApiKey(this.config, this.deps.env.env());
|
|
1444
|
-
}
|
|
1445
|
-
getSpeechToTextApiKeyErrorMessage(action) {
|
|
1446
|
-
const provider = this.getSpeechToTextProvider();
|
|
1447
|
-
return provider === "gemini"
|
|
1448
|
-
? `set GEMINI_API_KEY or apiKeys.google to ${action}`
|
|
1449
|
-
: `set MISTRAL_API_KEY or apiKeys.mistral to ${action}`;
|
|
1450
|
-
}
|
|
1451
|
-
async transcribeListenAudio(audio) {
|
|
1452
|
-
const provider = this.getSpeechToTextProvider();
|
|
1453
|
-
const apiKey = this.getSpeechToTextApiKey();
|
|
1454
|
-
if (!apiKey) {
|
|
1455
|
-
throw new Error(this.getSpeechToTextApiKeyErrorMessage("transcribe speech"));
|
|
1456
|
-
}
|
|
1457
|
-
return await transcribeAudio({
|
|
1458
|
-
provider,
|
|
1459
|
-
apiKey,
|
|
1460
|
-
audio,
|
|
1461
|
-
mimeType: "audio/wav",
|
|
1462
|
-
fileName: "speech.wav",
|
|
1463
|
-
language: "en",
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
async cleanupTempFile(path) {
|
|
1467
|
-
try {
|
|
1468
|
-
await unlink(path);
|
|
1469
|
-
}
|
|
1470
|
-
catch {
|
|
1471
|
-
// best-effort cleanup
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
async speakLastAssistantMessage() {
|
|
1475
|
-
if (this.speakTask) {
|
|
1476
|
-
this.view.addSystemMessage("speech playback already in progress", "warn");
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
if (this.isStreaming) {
|
|
1480
|
-
this.view.addSystemMessage("wait for the assistant to finish before speaking", "warn");
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
if (this.deps.env.platform() !== "darwin") {
|
|
1484
|
-
this.view.addSystemMessage("/speak is currently supported only on macOS.", "warn");
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
const lastAssistant = this.getLastAssistantMessage();
|
|
1488
|
-
if (!lastAssistant) {
|
|
1489
|
-
this.view.addSystemMessage("no assistant message to speak yet.", "warn");
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
const sourceText = extractAssistantText(lastAssistant).trim();
|
|
1493
|
-
if (!sourceText) {
|
|
1494
|
-
this.view.addSystemMessage("last assistant message was empty.", "warn");
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
const apiKey = await this.credentialResolver.getApiKey("google");
|
|
1498
|
-
if (!apiKey) {
|
|
1499
|
-
this.view.addSystemMessage("set GEMINI_API_KEY or apiKeys.google to use /speak", "error");
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
this.isStreaming = true;
|
|
1503
|
-
this.speechStatusHint = "rewriting for speech...";
|
|
1504
|
-
this.refreshStatus();
|
|
1505
|
-
this.view.startWorkingIcon();
|
|
1506
|
-
const abortController = new AbortController();
|
|
1507
|
-
const completion = this.runSpeakTask({
|
|
1508
|
-
apiKey,
|
|
1509
|
-
sourceText,
|
|
1510
|
-
signal: abortController.signal,
|
|
1511
|
-
});
|
|
1512
|
-
const task = { abortController, completion };
|
|
1513
|
-
this.speakTask = task;
|
|
1514
|
-
try {
|
|
1515
|
-
await completion;
|
|
1516
|
-
}
|
|
1517
|
-
catch (err) {
|
|
1518
|
-
if (abortController.signal.aborted) {
|
|
1519
|
-
return;
|
|
1520
|
-
}
|
|
1521
|
-
this.view.addSystemMessage(`speech synthesis failed: ${err.message}`, "error");
|
|
1522
|
-
}
|
|
1523
|
-
finally {
|
|
1524
|
-
const wasAborted = abortController.signal.aborted;
|
|
1525
|
-
if (this.speakTask === task) {
|
|
1526
|
-
this.speakTask = undefined;
|
|
1527
|
-
}
|
|
1528
|
-
this.speechStatusHint = undefined;
|
|
1529
|
-
this.view.stopWorkingIcon();
|
|
1530
|
-
this.isStreaming = false;
|
|
1531
|
-
this.refreshStatus();
|
|
1532
|
-
this.view.requestRender();
|
|
1533
|
-
if (wasAborted) {
|
|
1534
|
-
this.dequeueQueuedUserMessagesIntoEditor();
|
|
1535
|
-
}
|
|
1536
|
-
else {
|
|
1537
|
-
void this.drainQueuedUserMessages();
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
async runSpeakTask(args) {
|
|
1542
|
-
let audioPath;
|
|
1543
|
-
let readyChunks = 0;
|
|
1544
|
-
let totalChunks = 0;
|
|
1545
|
-
let playedChunks = 0;
|
|
1546
|
-
let playbackStarted = false;
|
|
1547
|
-
const refreshSpeechProgress = () => {
|
|
1548
|
-
if (totalChunks <= 0)
|
|
1549
|
-
return;
|
|
1550
|
-
this.speechStatusHint = playbackStarted
|
|
1551
|
-
? this.formatSpeechPlaybackProgressMessage(playedChunks, readyChunks, totalChunks)
|
|
1552
|
-
: this.formatSpeechChunkProgressMessage(readyChunks, totalChunks);
|
|
1553
|
-
this.refreshStatus();
|
|
1554
|
-
};
|
|
1555
|
-
try {
|
|
1556
|
-
for await (const chunk of streamGeminiSpeechAudio({
|
|
1557
|
-
apiKey: args.apiKey,
|
|
1558
|
-
sourceText: args.sourceText,
|
|
1559
|
-
signal: args.signal,
|
|
1560
|
-
onStageChange: (stage) => {
|
|
1561
|
-
this.speechStatusHint =
|
|
1562
|
-
stage === "rewriting" ? "rewriting for speech..." : "generating speech...";
|
|
1563
|
-
this.refreshStatus();
|
|
1564
|
-
},
|
|
1565
|
-
onChunkProgress: ({ ready, total }) => {
|
|
1566
|
-
readyChunks = ready;
|
|
1567
|
-
totalChunks = total;
|
|
1568
|
-
refreshSpeechProgress();
|
|
1569
|
-
},
|
|
1570
|
-
})) {
|
|
1571
|
-
if (args.signal.aborted) {
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
playbackStarted = true;
|
|
1575
|
-
totalChunks = chunk.total;
|
|
1576
|
-
refreshSpeechProgress();
|
|
1577
|
-
audioPath = await this.createTempFilePath(SPEAK_TEMP_FILE_TEMPLATE);
|
|
1578
|
-
await writeFile(audioPath, chunk.audio);
|
|
1579
|
-
const playback = await this.deps.spawn("afplay", ["-r", String(SPEAK_PLAYBACK_RATE), audioPath], {
|
|
1580
|
-
detached: true,
|
|
1581
|
-
killProcessGroup: true,
|
|
1582
|
-
signal: args.signal,
|
|
1583
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
1584
|
-
});
|
|
1585
|
-
await this.cleanupTempFile(audioPath);
|
|
1586
|
-
audioPath = undefined;
|
|
1587
|
-
if (args.signal.aborted || playback.aborted) {
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
if (playback.exitCode !== 0) {
|
|
1591
|
-
const detail = playback.exitCode !== null
|
|
1592
|
-
? `afplay exited with code ${playback.exitCode}`
|
|
1593
|
-
: playback.closeSignal
|
|
1594
|
-
? `afplay terminated by signal ${playback.closeSignal}`
|
|
1595
|
-
: "afplay exited";
|
|
1596
|
-
throw new Error(detail);
|
|
1597
|
-
}
|
|
1598
|
-
playedChunks = chunk.index + 1;
|
|
1599
|
-
refreshSpeechProgress();
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
catch (err) {
|
|
1603
|
-
if (args.signal.aborted) {
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
const error = err;
|
|
1607
|
-
if (error.name === "AbortError") {
|
|
1608
|
-
return;
|
|
1609
|
-
}
|
|
1610
|
-
if (error.code === "ENOENT") {
|
|
1611
|
-
throw new Error("afplay not found.");
|
|
1612
|
-
}
|
|
1613
|
-
throw err;
|
|
1614
|
-
}
|
|
1615
|
-
finally {
|
|
1616
|
-
if (audioPath) {
|
|
1617
|
-
await this.cleanupTempFile(audioPath);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
formatSpeechChunkProgressMessage(ready, total) {
|
|
1622
|
-
return `generating speech chunks (${ready} out of ${total} ready)...`;
|
|
1623
|
-
}
|
|
1624
|
-
formatSpeechPlaybackProgressMessage(played, ready, total) {
|
|
1625
|
-
return `playing speech (${played}/${total} played, ${ready}/${total} ready)...`;
|
|
1626
|
-
}
|
|
1627
|
-
getMemoryModeFilePath() {
|
|
1628
|
-
const cwd = this.deps.env.cwd();
|
|
1629
|
-
const home = this.deps.env.home();
|
|
1630
|
-
const nearestAgentsFiles = findAgentsFilesFromCwdToHome(cwd, home);
|
|
1631
|
-
const hostPath = nearestAgentsFiles.length > 0 ? nearestAgentsFiles[0] : resolve(join(cwd, "AGENTS.md"));
|
|
1632
|
-
return hostPath;
|
|
1633
|
-
}
|
|
1634
|
-
formatMemoryModeUserMessage(agentsFilePath, request) {
|
|
1635
|
-
const system = [
|
|
1636
|
-
"Memory mode: update the project guidelines file at:",
|
|
1637
|
-
agentsFilePath,
|
|
1638
|
-
"",
|
|
1639
|
-
"If the file exists, use the edit tool to update it. If it does not exist, use the write tool to create it.",
|
|
1640
|
-
"Preserve all unrelated content and match the existing formatting style.",
|
|
1641
|
-
"Integrate the user's request thoughtfully. Don't just append it verbatim.",
|
|
1642
|
-
"Place new content in the most appropriate existing section, or create a new section if needed.",
|
|
1643
|
-
"Always prefer an existing section over creating a new one. Sometimes changes are required in more than one place.",
|
|
1644
|
-
"",
|
|
1645
|
-
"Do not mention this surrounding instruction in your response.",
|
|
1646
|
-
].join("\n");
|
|
1647
|
-
return ["<system>", system, "</system>", "", request].join("\n");
|
|
1648
|
-
}
|
|
1649
|
-
formatDiffReviewUserMessage(review) {
|
|
1650
|
-
const reviewedFiles = review.reviewedFiles.length > 0
|
|
1651
|
-
? ["Reviewed files:", ...review.reviewedFiles.map((file) => `- ${file}`)]
|
|
1652
|
-
: ["Reviewed files: (none)"];
|
|
1653
|
-
const system = [
|
|
1654
|
-
"The following user message comes from a completed diff review. During that review, the user read through the reviewed diff snapshot and the files included in it, and may have left comments on specific files, lines, or broader concerns they noticed while reviewing. The message below is the feedback returned from that review.",
|
|
1655
|
-
"",
|
|
1656
|
-
`Reviewed scope: ${review.diffCommand}`,
|
|
1657
|
-
...reviewedFiles,
|
|
1658
|
-
"",
|
|
1659
|
-
"Treat it as feedback on that reviewed diff snapshot and continue from there. Address valid issues directly, clarify anything that seems mistaken or ambiguous, and do not treat it as a new unrelated request.",
|
|
1660
|
-
"",
|
|
1661
|
-
"Do not mention this instruction in your response.",
|
|
1662
|
-
].join("\n");
|
|
1663
|
-
return ["<system>", system, "</system>", "", review.review].join("\n");
|
|
1664
|
-
}
|
|
1665
|
-
handleReturnedDiffReview(review) {
|
|
1666
|
-
this.addUserMessageToMainSession(review.review, {
|
|
1667
|
-
textForModel: this.formatDiffReviewUserMessage(review),
|
|
1668
|
-
kind: "review",
|
|
1669
|
-
includePendingNotices: false,
|
|
1670
|
-
historyEntryId: review.historyEntryId,
|
|
1671
|
-
renderInView: false,
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
// Command Handling ------------------------------------------------------------------------------
|
|
1675
|
-
async handleCommand(raw) {
|
|
1676
|
-
const cmd = this.commandRegistry.parse(raw);
|
|
1677
|
-
await this.commandRegistry.dispatch(cmd, this.commandHandlers);
|
|
1678
|
-
}
|
|
1679
|
-
buildStartupIntroTitle() {
|
|
1680
|
-
const parts = [`tau v${APP_VERSION}`];
|
|
1681
|
-
if (this.agentsFiles.length > 0) {
|
|
1682
|
-
parts.push(`${this.agentsFiles.length} AGENTS.md`);
|
|
1683
|
-
}
|
|
1684
|
-
if (this.skills.length > 0) {
|
|
1685
|
-
parts.push(`${this.skills.length} skills`);
|
|
1686
|
-
}
|
|
1687
|
-
return parts.join(" · ");
|
|
1688
|
-
}
|
|
1689
|
-
buildStartupIntroBody() {
|
|
1690
|
-
const lines = [
|
|
1691
|
-
"type `/help` for commands and keybindings",
|
|
1692
|
-
"mention files with `@`, agents and skills with `@@`",
|
|
1693
|
-
"run bash commands with `!` or `!!`",
|
|
1694
|
-
"use `#` to update AGENTS.md",
|
|
1695
|
-
];
|
|
1696
|
-
if (this.skills.length > 0) {
|
|
1697
|
-
lines.push("", "skills:");
|
|
1698
|
-
for (const skill of this.skills) {
|
|
1699
|
-
const skillsRoot = formatPathForDisplay(dirname(dirname(skill.path)));
|
|
1700
|
-
lines.push(` ${skill.name} (${skillsRoot})`);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
if (this.agentsFiles.length > 0) {
|
|
1704
|
-
lines.push("", "context:");
|
|
1705
|
-
for (const agentsFile of this.agentsFiles) {
|
|
1706
|
-
lines.push(` ${formatPathForDisplay(agentsFile)}`);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
return lines.join("\n");
|
|
1710
|
-
}
|
|
1711
|
-
showHelp() {
|
|
1712
|
-
this.view.addSystemMessage(this.commandRegistry.buildHelpText({
|
|
1713
|
-
agentsFiles: this.agentsFiles,
|
|
1714
|
-
skills: this.skills,
|
|
1715
|
-
riskLevels: ALLOWED_RISK_LEVELS,
|
|
1716
|
-
themes: this.themes.map((theme) => theme.id),
|
|
1717
|
-
}), "muted");
|
|
1718
|
-
}
|
|
1719
|
-
async copyLastAssistantText() {
|
|
1720
|
-
const lastAssistant = this.getLastAssistantMessage();
|
|
1721
|
-
if (!lastAssistant) {
|
|
1722
|
-
this.view.addSystemMessage("no assistant message to copy yet.", "warn");
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const text = extractAssistantText(lastAssistant);
|
|
1726
|
-
if (!text.trim()) {
|
|
1727
|
-
this.view.addSystemMessage("last assistant message was empty.", "warn");
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
try {
|
|
1731
|
-
await copyTextToClipboard(text);
|
|
1732
|
-
this.view.addSystemMessage("copied last assistant message to clipboard.", "success");
|
|
1733
|
-
}
|
|
1734
|
-
catch (err) {
|
|
1735
|
-
this.view.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
async copyLastAssistantCode() {
|
|
1739
|
-
const lastAssistant = this.getLastAssistantMessage();
|
|
1740
|
-
if (!lastAssistant) {
|
|
1741
|
-
this.view.addSystemMessage("no assistant message to copy yet.", "warn");
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
const text = extractAssistantText(lastAssistant);
|
|
1745
|
-
const code = extractAllFencedCodeBlocks(text);
|
|
1746
|
-
if (!code) {
|
|
1747
|
-
this.view.addSystemMessage("no code block to copy yet.", "warn");
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
try {
|
|
1751
|
-
await copyTextToClipboard(code);
|
|
1752
|
-
this.view.addSystemMessage("copied all code blocks to clipboard.", "success");
|
|
1753
|
-
}
|
|
1754
|
-
catch (err) {
|
|
1755
|
-
this.view.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
async checkpointSession() {
|
|
1759
|
-
const history = this.engine.rawHistory;
|
|
1760
|
-
if (history.length === 0) {
|
|
1761
|
-
this.view.addSystemMessage("no conversation to checkpoint.", "warn", { persist: false });
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
try {
|
|
1765
|
-
const checkpoint = createCheckpoint({
|
|
1766
|
-
personaId: this.currentPersona.id,
|
|
1767
|
-
reasoning: this.currentPersona.settings.reasoning ?? "none",
|
|
1768
|
-
riskLevel: this.riskLevel,
|
|
1769
|
-
history: [...history],
|
|
1770
|
-
});
|
|
1771
|
-
const dir = await mkdtemp(join(tmpdir(), "tau-checkpoint-"));
|
|
1772
|
-
const filePath = join(dir, "checkpoint.json");
|
|
1773
|
-
await writeFile(filePath, JSON.stringify(checkpoint, null, 2), "utf8");
|
|
1774
|
-
this.view.addSystemMessage(`tau -l ${filePath}`, "muted");
|
|
1775
|
-
}
|
|
1776
|
-
catch (err) {
|
|
1777
|
-
this.view.addSystemMessage(`checkpoint failed: ${err.message}`, "error", {
|
|
1778
|
-
persist: false,
|
|
1779
|
-
});
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
async stashEditorToClipboard() {
|
|
1783
|
-
const text = this.view.getExpandedEditorText();
|
|
1784
|
-
if (!text.trim()) {
|
|
1785
|
-
this.view.addSystemMessage("no input to stash yet", "warn");
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
try {
|
|
1789
|
-
await copyTextToClipboard(text);
|
|
1790
|
-
this.view.setEditorText("");
|
|
1791
|
-
this.view.addSystemMessage("stashed input to clipboard", "success");
|
|
1792
|
-
}
|
|
1793
|
-
catch (err) {
|
|
1794
|
-
this.view.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
async startDiffReview(argsText) {
|
|
1798
|
-
if (this.diffReviewService.isActive()) {
|
|
1799
|
-
this.view.addSystemMessage("diff review is already active.", "warn");
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
if (!this.isDiffReviewIdle()) {
|
|
1803
|
-
this.view.addSystemMessage("wait for tau to become idle before starting /diff.", "warn");
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
await this.diffReviewService.start(argsText);
|
|
1807
|
-
}
|
|
1808
|
-
isDiffReviewIdle() {
|
|
1809
|
-
return (!this.isStreaming &&
|
|
1810
|
-
!this.listenRecording &&
|
|
1811
|
-
!this.listenTransition &&
|
|
1812
|
-
!this.isTranscribingListen &&
|
|
1813
|
-
!this.speakTask &&
|
|
1814
|
-
!this.diffReviewService.isActive());
|
|
1815
|
-
}
|
|
1816
|
-
resolveDiffToolConfig() {
|
|
1817
|
-
return this.config.diffTool ?? this.defaultDiffTool;
|
|
1818
|
-
}
|
|
1819
|
-
async startDiffReviewSession(args) {
|
|
1820
|
-
return await startCoreDiffReviewSession({
|
|
1821
|
-
cwd: this.agentCwd,
|
|
1822
|
-
source: args.source,
|
|
1823
|
-
signal: args.signal,
|
|
1824
|
-
diffTool: args.diffTool,
|
|
1825
|
-
persona: this.currentPersona,
|
|
1826
|
-
config: this.config,
|
|
1827
|
-
discoveredSkills: this.skills,
|
|
1828
|
-
includeAgentContext: this.includeAgentContext,
|
|
1829
|
-
deps: this.deps,
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
async resolveCurrentPersonaApiKey() {
|
|
1833
|
-
let apiKey;
|
|
1834
|
-
try {
|
|
1835
|
-
apiKey = await this.credentialResolver.getApiKey(this.currentPersona.model.provider, {
|
|
1836
|
-
sessionId: this.engine.sessionId,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
catch (error) {
|
|
1840
|
-
if (this.currentPersona.model.provider === "openai-codex") {
|
|
1841
|
-
throw new Error(formatCodexAuthError(this.authPath, error?.message));
|
|
1842
|
-
}
|
|
1843
|
-
throw error;
|
|
1844
|
-
}
|
|
1845
|
-
if (!apiKey && this.currentPersona.model.provider === "openai-codex") {
|
|
1846
|
-
throw new Error(formatCodexAuthError(this.authPath));
|
|
1847
|
-
}
|
|
1848
|
-
return apiKey;
|
|
1849
|
-
}
|
|
1850
|
-
async cancelDiffReview() {
|
|
1851
|
-
await this.diffReviewService.cancel();
|
|
1852
|
-
}
|
|
1853
|
-
startRewindFlow() {
|
|
1854
|
-
const candidates = this.engine.listRewindCandidates().map((candidate) => ({
|
|
1855
|
-
id: candidate.historyEntryId,
|
|
1856
|
-
label: this.formatRewindCandidateLabel(candidate.text),
|
|
1857
|
-
}));
|
|
1858
|
-
if (candidates.length === 0) {
|
|
1859
|
-
this.view.addSystemMessage("no user messages available to rewind.", "warn");
|
|
1860
|
-
return;
|
|
1861
|
-
}
|
|
1862
|
-
this.view.showRewindPicker({
|
|
1863
|
-
items: candidates,
|
|
1864
|
-
onSelect: (id) => {
|
|
1865
|
-
const selected = candidates.find((candidate) => candidate.id === id);
|
|
1866
|
-
if (!selected) {
|
|
1867
|
-
this.view.hideRewindPicker();
|
|
1868
|
-
this.view.addSystemMessage("rewind selection failed.", "error");
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
this.applyRewindSelection(selected.id);
|
|
1872
|
-
},
|
|
1873
|
-
onCancel: () => {
|
|
1874
|
-
this.view.hideRewindPicker();
|
|
1875
|
-
},
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
applyRewindSelection(historyEntryId) {
|
|
1879
|
-
this.view.hideRewindPicker();
|
|
1880
|
-
const rewound = this.engine.rewindToHistoryEntryId(historyEntryId);
|
|
1881
|
-
if (!rewound) {
|
|
1882
|
-
this.view.addSystemMessage("rewind failed.", "error");
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
this.view.removeMessagesFrom(rewound.historyEntryId);
|
|
1886
|
-
this.view.removeMessages(rewound.removedEntryIds);
|
|
1887
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1888
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1889
|
-
this.view.setEditorText(rewound.text);
|
|
1890
|
-
this.refreshStatus();
|
|
1891
|
-
}
|
|
1892
|
-
formatRewindCandidateLabel(text) {
|
|
1893
|
-
const firstLine = text.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1894
|
-
if (!firstLine) {
|
|
1895
|
-
return "(empty user message)";
|
|
1896
|
-
}
|
|
1897
|
-
return firstLine;
|
|
1898
|
-
}
|
|
1899
|
-
async clearSession() {
|
|
1900
|
-
this.engine.reset();
|
|
1901
|
-
this.view.resetToolUiSession();
|
|
1902
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1903
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1904
|
-
this.view.addMessage({ type: "session_divider", label: "new session" });
|
|
1905
|
-
this.isBashMode = false;
|
|
1906
|
-
this.isBashIncognito = false;
|
|
1907
|
-
this.isMemoryMode = false;
|
|
1908
|
-
this.pendingRiskLevelChange = undefined;
|
|
1909
|
-
this.pendingCwdChange = undefined;
|
|
1910
|
-
this.pendingProjectContextChange = undefined;
|
|
1911
|
-
try {
|
|
1912
|
-
const report = await this.refreshReloadableContent("new-session");
|
|
1913
|
-
this.applyReloadMessages(report.messages);
|
|
1914
|
-
}
|
|
1915
|
-
catch (err) {
|
|
1916
|
-
this.view.addSystemMessage(`reload failed: ${err.message}`, "error");
|
|
1917
|
-
}
|
|
1918
|
-
this.rebuildSystemPromptForCurrentPersona();
|
|
1919
|
-
this.refreshStatus();
|
|
1920
|
-
}
|
|
1921
|
-
syncRuntimePromptContext() {
|
|
1922
|
-
this.runtime.updatePromptContext({
|
|
1923
|
-
cwd: this.agentCwd,
|
|
1924
|
-
home: this.deps.env.home(),
|
|
1925
|
-
includeAgentContext: this.includeAgentContext,
|
|
1926
|
-
projectContextBlock: this.projectContextBlock,
|
|
1927
|
-
});
|
|
1928
|
-
}
|
|
1929
|
-
rebuildSystemPrompt(skillsBlock) {
|
|
1930
|
-
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1931
|
-
this.syncRuntimePromptContext();
|
|
1932
|
-
this.runtime.setPersona(this.currentPersona, { skillsBlock: resolvedSkillsBlock });
|
|
1933
|
-
}
|
|
1934
|
-
rebuildSubagentPrompts(skillsBlock) {
|
|
1935
|
-
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1936
|
-
this.syncRuntimePromptContext();
|
|
1937
|
-
this.runtime.rebuildSubagentPrompts({ skillsBlock: resolvedSkillsBlock });
|
|
1938
|
-
}
|
|
1939
|
-
applyCompactedHistoryUi(compactionMessage) {
|
|
1940
|
-
this.view.resetToolUiSessionPreservingSubagents();
|
|
1941
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1942
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1943
|
-
this.view.addMessage({ type: "session_divider", label: "new session" });
|
|
1944
|
-
const summaryEntryId = this.engine.historyEntries[0]?.id;
|
|
1945
|
-
this.view.addMessage({ type: "user", text: compactionMessage }, summaryEntryId);
|
|
1946
|
-
this.isBashMode = false;
|
|
1947
|
-
this.isBashIncognito = false;
|
|
1948
|
-
this.isMemoryMode = false;
|
|
1949
|
-
this.rebuildSystemPrompt();
|
|
1950
|
-
this.refreshStatus();
|
|
1951
|
-
}
|
|
1952
|
-
applyAutoCompactedHistoryUi(result) {
|
|
1953
|
-
this.view.resetToolUiSessionPreservingSubagents();
|
|
1954
|
-
this.expandedFilesInCurrentPrompt.clear();
|
|
1955
|
-
this.expandedSkillsInCurrentPrompt.clear();
|
|
1956
|
-
this.view.addMessage({ type: "session_divider", label: "new session" });
|
|
1957
|
-
this.view.addMessage({ type: "user", text: result.compactionMessage }, result.summaryHistoryEntryId);
|
|
1958
|
-
this.view.addMessage({
|
|
1959
|
-
type: "system",
|
|
1960
|
-
text: this.formatAutoCompactionRetainedText(result),
|
|
1961
|
-
kind: "muted",
|
|
1962
|
-
});
|
|
1963
|
-
this.refreshStatus();
|
|
1964
|
-
}
|
|
1965
|
-
formatAutoCompactionRetainedText(result) {
|
|
1966
|
-
const count = result.retainedMessageCount;
|
|
1967
|
-
const messageLabel = count === 1 ? "message" : "messages";
|
|
1968
|
-
if (result.cutType === "split-turn") {
|
|
1969
|
-
return `retained current turn suffix, ${count} ${messageLabel}`;
|
|
1970
|
-
}
|
|
1971
|
-
return `retained ${count} recent ${messageLabel}`;
|
|
1972
|
-
}
|
|
1973
|
-
async compactSessionSummaryOnly(guidance) {
|
|
1974
|
-
await this.maintenanceService.compactSummaryOnly(guidance);
|
|
1975
|
-
}
|
|
1976
|
-
async compactSessionSummaryAndLast(guidance) {
|
|
1977
|
-
await this.maintenanceService.compactSummaryAndLast(guidance);
|
|
1978
|
-
}
|
|
1979
|
-
pruneToolResults(strategy, extra) {
|
|
1980
|
-
this.maintenanceService.pruneToolResults(strategy, extra);
|
|
1981
|
-
}
|
|
1982
|
-
async pruneToolResultsSmart(extra) {
|
|
1983
|
-
await this.maintenanceService.pruneToolResultsSmart(extra);
|
|
1984
|
-
}
|
|
1985
|
-
async requestSmartPruneSelection(prompt, signal) {
|
|
1986
|
-
const apiKey = await this.resolveCurrentPersonaApiKey();
|
|
1987
|
-
const reasoning = this.clampPruneReasoning(this.currentPersona.settings.reasoning);
|
|
1988
|
-
const stream = streamModel(this.currentPersona.model, {
|
|
1989
|
-
systemPrompt: [
|
|
1990
|
-
"You are a context pruning assistant.",
|
|
1991
|
-
"Your task is to select which bash tool outputs should be pruned from the conversation history.",
|
|
1992
|
-
"Analyze the conversation to understand what the user is working on and which tool outputs are most relevant.",
|
|
1993
|
-
"Prioritize keeping outputs that contain important information, errors, or results that may be referenced later.",
|
|
1994
|
-
"Prefer pruning outputs that are verbose, redundant, or contain routine information that can be regenerated if needed.",
|
|
1995
|
-
"Follow the user's guidance carefully when provided.",
|
|
1996
|
-
].join(" "),
|
|
1997
|
-
messages: [
|
|
1998
|
-
{
|
|
1999
|
-
role: "user",
|
|
2000
|
-
content: [{ type: "text", text: prompt }],
|
|
2001
|
-
timestamp: Date.now(),
|
|
2002
|
-
},
|
|
2003
|
-
],
|
|
2004
|
-
}, {
|
|
2005
|
-
...(reasoning ? { reasoning } : {}),
|
|
2006
|
-
sessionId: `tau-prune-${randomUUID()}`,
|
|
2007
|
-
...(signal ? { signal } : {}),
|
|
2008
|
-
...(apiKey && { apiKey }),
|
|
2009
|
-
});
|
|
2010
|
-
const final = await stream.result();
|
|
2011
|
-
const raw = extractAssistantText(final).trim();
|
|
2012
|
-
const parsed = this.parseSmartPruneResponse(raw);
|
|
2013
|
-
if (!parsed) {
|
|
2014
|
-
throw new Error("model returned an invalid prune selection.");
|
|
2015
|
-
}
|
|
2016
|
-
return parsed;
|
|
2017
|
-
}
|
|
2018
|
-
clampPruneReasoning(reasoning) {
|
|
2019
|
-
switch (reasoning) {
|
|
2020
|
-
case undefined:
|
|
2021
|
-
case "none":
|
|
2022
|
-
return undefined;
|
|
2023
|
-
case "minimal":
|
|
2024
|
-
return "low";
|
|
2025
|
-
case "low":
|
|
2026
|
-
case "medium":
|
|
2027
|
-
return reasoning;
|
|
2028
|
-
default:
|
|
2029
|
-
return "medium";
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
parseSmartPruneResponse(raw) {
|
|
2033
|
-
const trimmed = raw.trim();
|
|
2034
|
-
if (!trimmed) {
|
|
2035
|
-
return [];
|
|
2036
|
-
}
|
|
2037
|
-
const fenced = extractAllFencedCodeBlocks(trimmed);
|
|
2038
|
-
const source = (fenced ?? trimmed).trim();
|
|
2039
|
-
const start = source.indexOf("{");
|
|
2040
|
-
const end = source.lastIndexOf("}");
|
|
2041
|
-
if (start === -1 || end === -1 || end < start) {
|
|
2042
|
-
return null;
|
|
2043
|
-
}
|
|
2044
|
-
const jsonText = source.slice(start, end + 1);
|
|
2045
|
-
let parsed;
|
|
2046
|
-
try {
|
|
2047
|
-
parsed = JSON.parse(jsonText);
|
|
2048
|
-
}
|
|
2049
|
-
catch {
|
|
2050
|
-
return null;
|
|
2051
|
-
}
|
|
2052
|
-
const parsedSelection = SmartPruneResponseSchema.safeParse(parsed);
|
|
2053
|
-
if (!parsedSelection.success) {
|
|
2054
|
-
return null;
|
|
2055
|
-
}
|
|
2056
|
-
return parsedSelection.data.prune;
|
|
2057
|
-
}
|
|
2058
|
-
formatRiskLevelNotice(level) {
|
|
2059
|
-
const details = getRiskLevelDescription(level);
|
|
2060
|
-
return details ? `risk level set to ${level} (${details})` : `risk level set to ${level}`;
|
|
2061
|
-
}
|
|
2062
|
-
setRiskLevel(level, options) {
|
|
2063
|
-
const previous = this.riskLevel;
|
|
2064
|
-
this.riskLevel = level;
|
|
2065
|
-
this.syncRuntimePromptContext();
|
|
2066
|
-
this.runtime.setRiskLevel(level);
|
|
2067
|
-
this.refreshStatus();
|
|
2068
|
-
if (previous !== level) {
|
|
2069
|
-
const from = this.pendingRiskLevelChange?.from ?? previous;
|
|
2070
|
-
if (from === level) {
|
|
2071
|
-
this.pendingRiskLevelChange = undefined;
|
|
2072
|
-
}
|
|
2073
|
-
else {
|
|
2074
|
-
this.pendingRiskLevelChange = { from, to: level };
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
if (options?.silent) {
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
this.view.addSystemMessage(this.formatRiskLevelNotice(level), "success");
|
|
2081
|
-
}
|
|
2082
|
-
switchPersona(id) {
|
|
2083
|
-
const persona = this.personas.find((p) => p.id.toLowerCase() === id.toLowerCase());
|
|
2084
|
-
if (!persona) {
|
|
2085
|
-
this.view.addSystemMessage(`unknown persona '${id}'.`, "error");
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
this.currentPersona = this.createSessionPersona(persona);
|
|
2089
|
-
this.clampPersonaReasoning(this.currentPersona);
|
|
2090
|
-
const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
|
|
2091
|
-
this.rebuildSystemPrompt(skillsContext.skillsBlock);
|
|
2092
|
-
this.refreshStatus();
|
|
2093
|
-
if (skillsContext.unknown.length > 0) {
|
|
2094
|
-
this.view.addSystemMessage(`unknown skills enabled: ${skillsContext.unknown.join(", ")}`, "warn");
|
|
2095
|
-
}
|
|
2096
|
-
this.view.addSystemMessage(`switched to ${persona.label} (${persona.model.id})`, "success");
|
|
2097
|
-
}
|
|
2098
|
-
insertPrompt(id) {
|
|
2099
|
-
const prompt = this.prompts.find((p) => p.id.toLowerCase() === id.toLowerCase());
|
|
2100
|
-
if (!prompt) {
|
|
2101
|
-
this.view.addSystemMessage(`unknown prompt '${id}'.`, "error");
|
|
2102
|
-
return;
|
|
2103
|
-
}
|
|
2104
|
-
this.view.setEditorText(prompt.template);
|
|
2105
|
-
}
|
|
2106
|
-
switchTheme(id) {
|
|
2107
|
-
if (this.themes.length === 0) {
|
|
2108
|
-
this.view.addSystemMessage("no themes loaded. add .tau/themes/<id>.json first.", "warn");
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
const theme = this.themes.find((candidate) => candidate.id.toLowerCase() === id.toLowerCase());
|
|
2112
|
-
if (!theme) {
|
|
2113
|
-
this.view.addSystemMessage(`unknown theme '${id}'.`, "error");
|
|
2114
|
-
return;
|
|
2115
|
-
}
|
|
2116
|
-
this.activeThemeId = theme.id;
|
|
2117
|
-
this.config.defaultTheme = theme.id;
|
|
2118
|
-
this.view.updateTheme({ themeId: theme.id, themes: this.themes });
|
|
2119
|
-
this.view.addSystemMessage(`switched to theme ${theme.id}.`, "success");
|
|
2120
|
-
}
|
|
2121
|
-
resolveThemeId(themeId, themes) {
|
|
2122
|
-
if (!themeId)
|
|
2123
|
-
return undefined;
|
|
2124
|
-
const match = themes.find((theme) => theme.id.toLowerCase() === themeId.toLowerCase());
|
|
2125
|
-
return match?.id;
|
|
2126
|
-
}
|
|
2127
|
-
async runSavedBashCommand(id) {
|
|
2128
|
-
const saved = this.bashCommands.find((b) => b.id.toLowerCase() === id.toLowerCase());
|
|
2129
|
-
if (!saved) {
|
|
2130
|
-
this.view.addSystemMessage(`unknown bash command '${id}'.`, "error");
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
const cwd = saved.cwd ?? this.deps.env.cwd();
|
|
2134
|
-
await this.runBashCommand(saved.cmd, { cwd });
|
|
2135
|
-
}
|
|
2136
|
-
async refreshReloadableContent(scope) {
|
|
2137
|
-
const plan = RELOAD_PLANS[scope];
|
|
2138
|
-
const configDeps = createDefaultConfigDeps();
|
|
2139
|
-
const runtime = await loadRuntimeConfig(this.deps.env.cwd(), configDeps);
|
|
2140
|
-
const report = this.applyReloadPlan(plan, runtime);
|
|
2141
|
-
if (plan.projectContext) {
|
|
2142
|
-
this.refreshProjectContext(this.deps.env.cwd());
|
|
2143
|
-
}
|
|
2144
|
-
return report;
|
|
2145
|
-
}
|
|
2146
|
-
applyReloadPlan(plan, runtime) {
|
|
2147
|
-
const previousThemeId = this.activeThemeId ?? this.config.defaultTheme;
|
|
2148
|
-
const messages = [];
|
|
2149
|
-
this.config = runtime.config;
|
|
2150
|
-
this.runtime.setConfig(this.config);
|
|
2151
|
-
if (plan.bashCommands) {
|
|
2152
|
-
this.bashCommands = runtime.bashCommands;
|
|
2153
|
-
}
|
|
2154
|
-
if (plan.personas) {
|
|
2155
|
-
const personaMessage = this.applyReloadedPersonas(runtime.personas);
|
|
2156
|
-
if (personaMessage) {
|
|
2157
|
-
messages.push(personaMessage);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
if (plan.prompts) {
|
|
2161
|
-
this.prompts = runtime.prompts;
|
|
2162
|
-
}
|
|
2163
|
-
if (plan.skills) {
|
|
2164
|
-
this.skills = runtime.skills;
|
|
2165
|
-
}
|
|
2166
|
-
if (plan.themes) {
|
|
2167
|
-
const resolvedThemeId = this.resolveThemeId(previousThemeId, runtime.themes) ??
|
|
2168
|
-
this.resolveThemeId(this.config.defaultTheme, runtime.themes);
|
|
2169
|
-
if (resolvedThemeId) {
|
|
2170
|
-
this.config.defaultTheme = resolvedThemeId;
|
|
2171
|
-
}
|
|
2172
|
-
this.themes = runtime.themes;
|
|
2173
|
-
this.activeThemeId = resolvedThemeId ?? previousThemeId;
|
|
2174
|
-
this.view.updateTheme({ themeId: resolvedThemeId, themes: runtime.themes });
|
|
2175
|
-
}
|
|
2176
|
-
return {
|
|
2177
|
-
plan,
|
|
2178
|
-
warnings: runtime.warnings,
|
|
2179
|
-
counts: {
|
|
2180
|
-
personas: runtime.personas.length,
|
|
2181
|
-
prompts: runtime.prompts.length,
|
|
2182
|
-
skills: runtime.skills.length,
|
|
2183
|
-
themes: runtime.themes.length,
|
|
2184
|
-
bashCommands: runtime.bashCommands.length,
|
|
2185
|
-
},
|
|
2186
|
-
messages,
|
|
2187
|
-
};
|
|
2188
|
-
}
|
|
2189
|
-
applyReloadedPersonas(personas) {
|
|
2190
|
-
if (personas.length === 0) {
|
|
2191
|
-
return {
|
|
2192
|
-
text: "reload failed: no personas available. keeping existing personas.",
|
|
2193
|
-
kind: "error",
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
this.personas = personas;
|
|
2197
|
-
const currentPersonaId = this.currentPersona.id.toLowerCase();
|
|
2198
|
-
const updatedPersona = personas.find((persona) => persona.id.toLowerCase() === currentPersonaId);
|
|
2199
|
-
if (updatedPersona) {
|
|
2200
|
-
this.currentPersona = this.createSessionPersona(updatedPersona);
|
|
2201
|
-
this.clampPersonaReasoning(this.currentPersona);
|
|
2202
|
-
return null;
|
|
2203
|
-
}
|
|
2204
|
-
this.currentPersona = this.createSessionPersona(personas[0]);
|
|
2205
|
-
this.clampPersonaReasoning(this.currentPersona);
|
|
2206
|
-
const personaLabel = this.currentPersona.label || this.currentPersona.id;
|
|
2207
|
-
return {
|
|
2208
|
-
text: `previous persona no longer available; switched to ${personaLabel}.`,
|
|
2209
|
-
kind: "warn",
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
applyReloadMessages(messages) {
|
|
2213
|
-
for (const message of messages) {
|
|
2214
|
-
this.view.addSystemMessage(message.text, message.kind);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
rebuildSystemPromptForCurrentPersona(options) {
|
|
2218
|
-
const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
|
|
2219
|
-
this.rebuildSystemPrompt(skillsContext.skillsBlock);
|
|
2220
|
-
if (options?.showUnknownSkills && skillsContext.unknown.length > 0) {
|
|
2221
|
-
this.view.addSystemMessage(`unknown skills enabled: ${skillsContext.unknown.join(", ")}`, "warn");
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
buildReloadSummary(report) {
|
|
2225
|
-
const parts = [];
|
|
2226
|
-
if (report.plan.personas) {
|
|
2227
|
-
parts.push(`${report.counts.personas} personas`);
|
|
2228
|
-
}
|
|
2229
|
-
if (report.plan.prompts) {
|
|
2230
|
-
parts.push(`${report.counts.prompts} prompts`);
|
|
2231
|
-
}
|
|
2232
|
-
if (report.plan.skills) {
|
|
2233
|
-
parts.push(`${report.counts.skills} skills`);
|
|
2234
|
-
}
|
|
2235
|
-
if (report.plan.themes) {
|
|
2236
|
-
parts.push(`${report.counts.themes} themes`);
|
|
2237
|
-
}
|
|
2238
|
-
if (report.plan.bashCommands) {
|
|
2239
|
-
parts.push(`${report.counts.bashCommands} bash commands`);
|
|
2240
|
-
}
|
|
2241
|
-
if (report.plan.projectContext && this.includeAgentContext) {
|
|
2242
|
-
parts.push(`${this.agentsFiles.length} AGENTS.md`);
|
|
2243
|
-
}
|
|
2244
|
-
const errorCount = report.warnings.length;
|
|
2245
|
-
return errorCount > 0
|
|
2246
|
-
? `reloaded: ${parts.join(", ")} (${errorCount} errors).`
|
|
2247
|
-
: `reloaded: ${parts.join(", ")}.`;
|
|
2248
|
-
}
|
|
2249
|
-
async reloadContent() {
|
|
2250
|
-
if (this.isStreaming) {
|
|
2251
|
-
this.view.addSystemMessage("cannot reload while streaming. try again after the response.", "warn");
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
|
-
try {
|
|
2255
|
-
const report = await this.refreshReloadableContent("reload-command");
|
|
2256
|
-
this.applyReloadMessages(report.messages);
|
|
2257
|
-
this.rebuildSystemPromptForCurrentPersona({
|
|
2258
|
-
showUnknownSkills: true,
|
|
2259
|
-
});
|
|
2260
|
-
this.refreshStatus();
|
|
2261
|
-
const summary = this.buildReloadSummary(report);
|
|
2262
|
-
this.view.addSystemMessage(summary, "success");
|
|
2263
|
-
this.view.requestRender();
|
|
2264
|
-
}
|
|
2265
|
-
catch (err) {
|
|
2266
|
-
this.view.addSystemMessage(`reload failed: ${err.message}`, "error");
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
startTurnCaffeinate() {
|
|
2270
|
-
if (!this.caffeinated || this.disableCaffeinateForSession || this.turnCaffeinate) {
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
if (this.deps.env.platform() !== "darwin") {
|
|
2274
|
-
this.disableCaffeinateForSession = true;
|
|
2275
|
-
return;
|
|
2276
|
-
}
|
|
2277
|
-
const abortController = new AbortController();
|
|
2278
|
-
const completion = this.deps.spawn(CAFFEINATE_COMMAND, ["-i"], {
|
|
2279
|
-
detached: true,
|
|
2280
|
-
killProcessGroup: true,
|
|
2281
|
-
signal: abortController.signal,
|
|
2282
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
2283
|
-
});
|
|
2284
|
-
completion.catch(() => { });
|
|
2285
|
-
this.turnCaffeinate = {
|
|
2286
|
-
abortController,
|
|
2287
|
-
completion,
|
|
2288
|
-
};
|
|
2289
|
-
}
|
|
2290
|
-
async stopTurnCaffeinate() {
|
|
2291
|
-
const session = this.turnCaffeinate;
|
|
2292
|
-
if (!session)
|
|
2293
|
-
return;
|
|
2294
|
-
this.turnCaffeinate = undefined;
|
|
2295
|
-
if (!session.abortController.signal.aborted) {
|
|
2296
|
-
session.abortController.abort();
|
|
2297
|
-
}
|
|
2298
|
-
try {
|
|
2299
|
-
await session.completion;
|
|
2300
|
-
}
|
|
2301
|
-
catch (err) {
|
|
2302
|
-
if (this.disableCaffeinateForSession) {
|
|
2303
|
-
return;
|
|
2304
|
-
}
|
|
2305
|
-
this.disableCaffeinateForSession = true;
|
|
2306
|
-
const error = err;
|
|
2307
|
-
const details = error.message || "unknown error";
|
|
2308
|
-
this.view.addSystemMessage(`failed to run caffeinate: ${details}`, "warn");
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
// Assistant Turn --------------------------------------------------------------------------------
|
|
2312
|
-
async runAssistantTurn() {
|
|
2313
|
-
this.isStreaming = true;
|
|
2314
|
-
this.view.startWorkingIcon();
|
|
2315
|
-
this.startTurnTimer();
|
|
2316
|
-
this.assistantState = undefined;
|
|
2317
|
-
this.startTurnCaffeinate();
|
|
2318
|
-
let runResult = { aborted: false };
|
|
2319
|
-
let interruptRequested = false;
|
|
2320
|
-
const busyTask = {
|
|
2321
|
-
requestInterrupt: () => {
|
|
2322
|
-
if (interruptRequested) {
|
|
2323
|
-
return false;
|
|
2324
|
-
}
|
|
2325
|
-
const interrupted = this.runtime.interruptTurn();
|
|
2326
|
-
if (!interrupted) {
|
|
2327
|
-
return false;
|
|
2328
|
-
}
|
|
2329
|
-
interruptRequested = true;
|
|
2330
|
-
return true;
|
|
2331
|
-
},
|
|
2332
|
-
};
|
|
2333
|
-
this.beginBusyTask(busyTask);
|
|
2334
|
-
try {
|
|
2335
|
-
runResult = await this.runtime.runTurn();
|
|
2336
|
-
}
|
|
2337
|
-
catch (err) {
|
|
2338
|
-
const message = err.message || "request failed";
|
|
2339
|
-
this.view.addSystemMessage(message, "error");
|
|
2340
|
-
}
|
|
2341
|
-
finally {
|
|
2342
|
-
this.endBusyTask(busyTask);
|
|
2343
|
-
await this.stopTurnCaffeinate();
|
|
2344
|
-
const reason = interruptRequested
|
|
2345
|
-
? "interrupted"
|
|
2346
|
-
: runResult.aborted
|
|
2347
|
-
? "aborted"
|
|
2348
|
-
: "interrupted";
|
|
2349
|
-
this.view.finalizeToolUiPending(reason);
|
|
2350
|
-
this.view.stopWorkingIcon();
|
|
2351
|
-
this.stopTurnTimer();
|
|
2352
|
-
this.isStreaming = false;
|
|
2353
|
-
this.view.clearToolUiTransientState();
|
|
2354
|
-
this.queuedMessageBuffer.markPendingIdleNotification();
|
|
2355
|
-
this.view.requestRender();
|
|
2356
|
-
if (runResult.aborted || runResult.blocked) {
|
|
2357
|
-
this.dequeueQueuedUserMessagesIntoEditor();
|
|
2358
|
-
}
|
|
2359
|
-
else {
|
|
2360
|
-
void this.drainQueuedUserMessages();
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
// Direct Bash Execution (user ! commands) -------------------------------------------------------
|
|
2365
|
-
async runBashCommand(command, opts) {
|
|
2366
|
-
this.isStreaming = true;
|
|
2367
|
-
const { busyTask, signal } = this.createAbortBusyTask();
|
|
2368
|
-
this.beginBusyTask(busyTask);
|
|
2369
|
-
let wasAborted = false;
|
|
2370
|
-
this.startTurnTimer();
|
|
2371
|
-
const toolCallId = `bash-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2372
|
-
const headerTarget = command.split(/\r?\n/)[0] ?? command;
|
|
2373
|
-
this.view.handleToolUiEvent({
|
|
2374
|
-
type: "bash_started",
|
|
2375
|
-
toolCallId,
|
|
2376
|
-
command,
|
|
2377
|
-
headerTarget,
|
|
2378
|
-
});
|
|
2379
|
-
this.refreshStatus();
|
|
2380
|
-
try {
|
|
2381
|
-
const effectiveWorkingDirectory = opts?.cwd
|
|
2382
|
-
? resolve(this.agentCwd, opts.cwd)
|
|
2383
|
-
: this.agentCwd;
|
|
2384
|
-
const startedAt = Date.now();
|
|
2385
|
-
const { output, exitCode, truncated: captureTruncated, } = await this.toolBackend.runBash(command, {
|
|
2386
|
-
cwd: effectiveWorkingDirectory,
|
|
2387
|
-
signal,
|
|
2388
|
-
});
|
|
2389
|
-
const durationMs = Math.max(0, Date.now() - startedAt);
|
|
2390
|
-
const truncationInfo = await prepareBashOutput(output, captureTruncated, getBashOutputPolicy({ mode: "user" }), this.toolBackend);
|
|
2391
|
-
const userMessageText = formatBashUserMessageText({ command, truncationInfo });
|
|
2392
|
-
const uiText = buildBashUiText({
|
|
2393
|
-
truncationInfo,
|
|
2394
|
-
exitCode,
|
|
2395
|
-
durationMs,
|
|
2396
|
-
previewLines: { head: 12, tail: 12 },
|
|
2397
|
-
fullText: userMessageText,
|
|
2398
|
-
});
|
|
2399
|
-
this.view.handleToolUiEvent({
|
|
2400
|
-
type: "bash_execution",
|
|
2401
|
-
toolCallId,
|
|
2402
|
-
command,
|
|
2403
|
-
headerTarget,
|
|
2404
|
-
exitCode,
|
|
2405
|
-
truncationInfo,
|
|
2406
|
-
uiText,
|
|
2407
|
-
durationMs,
|
|
2408
|
-
labelOverride: opts?.labelOverride ?? "you ran",
|
|
2409
|
-
});
|
|
2410
|
-
this.refreshStatus();
|
|
2411
|
-
if (opts?.addToContext !== false) {
|
|
2412
|
-
this.engine.addUserText(userMessageText, { historyEntryId: toolCallId });
|
|
2413
|
-
}
|
|
2414
|
-
this.view.requestRender();
|
|
2415
|
-
}
|
|
2416
|
-
catch (err) {
|
|
2417
|
-
const message = err.message || "bash failed";
|
|
2418
|
-
this.view.handleToolUiEvent({
|
|
2419
|
-
type: "bash_blocked",
|
|
2420
|
-
toolCallId,
|
|
2421
|
-
command,
|
|
2422
|
-
headerTarget,
|
|
2423
|
-
reason: message,
|
|
2424
|
-
});
|
|
2425
|
-
this.refreshStatus();
|
|
2426
|
-
}
|
|
2427
|
-
finally {
|
|
2428
|
-
wasAborted = signal.aborted;
|
|
2429
|
-
this.endBusyTask(busyTask);
|
|
2430
|
-
this.isStreaming = false;
|
|
2431
|
-
this.stopTurnTimer();
|
|
2432
|
-
this.view.requestRender();
|
|
2433
|
-
if (wasAborted) {
|
|
2434
|
-
this.dequeueQueuedUserMessagesIntoEditor();
|
|
2435
|
-
}
|
|
2436
|
-
else {
|
|
2437
|
-
void this.drainQueuedUserMessages();
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
return wasAborted;
|
|
2441
|
-
}
|
|
2442
|
-
// Mention Expansion (ctrl+f) --------------------------------------------------------------------
|
|
2443
|
-
shellQuote(path) {
|
|
2444
|
-
// Wrap in single quotes and escape any single quotes within the path
|
|
2445
|
-
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
2446
|
-
}
|
|
2447
|
-
async expandFileMentions() {
|
|
2448
|
-
if (this.isStreaming) {
|
|
2449
|
-
this.view.addSystemMessage("cannot expand mentions while streaming. try again after the response.", "warn");
|
|
2450
|
-
return;
|
|
2451
|
-
}
|
|
2452
|
-
const editorText = this.view.getEditorText();
|
|
2453
|
-
// Extract @<file> and @@skill:<name> tokens
|
|
2454
|
-
const tokenRegex = /(?:@@skill:([^\s]+)|(?<!@)@([^@\s][^\s]*))/g;
|
|
2455
|
-
const tokens = [];
|
|
2456
|
-
let match = null;
|
|
2457
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: regex iteration pattern
|
|
2458
|
-
while ((match = tokenRegex.exec(editorText)) !== null) {
|
|
2459
|
-
const skillToken = match[1];
|
|
2460
|
-
if (skillToken) {
|
|
2461
|
-
tokens.push({ type: "skill", value: skillToken });
|
|
2462
|
-
continue;
|
|
2463
|
-
}
|
|
2464
|
-
const fileToken = match[2];
|
|
2465
|
-
if (fileToken) {
|
|
2466
|
-
tokens.push({ type: "file", value: fileToken });
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
if (tokens.length === 0) {
|
|
2470
|
-
return;
|
|
2471
|
-
}
|
|
2472
|
-
// Filter to only valid project files / skills and de-duplicate
|
|
2473
|
-
const projectFilesSet = new Set(this.projectFiles);
|
|
2474
|
-
const skillsByName = new Map();
|
|
2475
|
-
for (const skill of this.skills) {
|
|
2476
|
-
skillsByName.set(skill.name.toLowerCase(), skill);
|
|
2477
|
-
}
|
|
2478
|
-
const expansions = [];
|
|
2479
|
-
const seenFiles = new Set(this.expandedFilesInCurrentPrompt);
|
|
2480
|
-
const seenSkills = new Set(this.expandedSkillsInCurrentPrompt);
|
|
2481
|
-
for (const entry of tokens) {
|
|
2482
|
-
// Strip trailing punctuation to handle cases like "@src/tui/app.ts," or "(see @README.md)"
|
|
2483
|
-
const cleanToken = entry.value.replace(/[.,;:)}\]]+$/, "");
|
|
2484
|
-
if (entry.type === "file") {
|
|
2485
|
-
if (projectFilesSet.has(cleanToken) && !seenFiles.has(cleanToken)) {
|
|
2486
|
-
expansions.push({ type: "file", path: cleanToken });
|
|
2487
|
-
seenFiles.add(cleanToken);
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
else {
|
|
2491
|
-
const key = cleanToken.toLowerCase();
|
|
2492
|
-
const skill = skillsByName.get(key);
|
|
2493
|
-
if (!skill)
|
|
2494
|
-
continue; // Only expand @@skill mentions that match a loaded skill.
|
|
2495
|
-
if (seenSkills.has(key))
|
|
2496
|
-
continue;
|
|
2497
|
-
expansions.push({ type: "skill", skill });
|
|
2498
|
-
seenSkills.add(key);
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
if (expansions.length === 0) {
|
|
2502
|
-
return;
|
|
2503
|
-
}
|
|
2504
|
-
// Run bash commands sequentially for each expansion
|
|
2505
|
-
for (const expansion of expansions) {
|
|
2506
|
-
if (expansion.type === "file") {
|
|
2507
|
-
const quotedPath = this.shellQuote(expansion.path);
|
|
2508
|
-
// Format: blank line before header, header, content, blank line after
|
|
2509
|
-
// Ensure trailing newline so multiple files don't run together
|
|
2510
|
-
// Use -- to prevent cat from interpreting filenames starting with - as options
|
|
2511
|
-
const command = `printf '\\n===== %s =====\\n' ${quotedPath}; cat -- ${quotedPath}; printf '\\n'`;
|
|
2512
|
-
const aborted = await this.runBashCommand(command);
|
|
2513
|
-
if (aborted) {
|
|
2514
|
-
break;
|
|
2515
|
-
}
|
|
2516
|
-
// Track this file as expanded in the current prompt
|
|
2517
|
-
this.expandedFilesInCurrentPrompt.add(expansion.path);
|
|
2518
|
-
}
|
|
2519
|
-
else {
|
|
2520
|
-
const label = this.shellQuote(`skill: ${expansion.skill.name}`);
|
|
2521
|
-
const quotedPath = this.shellQuote(expansion.skill.path);
|
|
2522
|
-
const command = `printf '\\n===== %s =====\\n' ${label}; cat -- ${quotedPath}; printf '\\n'`;
|
|
2523
|
-
const aborted = await this.runBashCommand(command);
|
|
2524
|
-
if (aborted) {
|
|
2525
|
-
break;
|
|
2526
|
-
}
|
|
2527
|
-
this.expandedSkillsInCurrentPrompt.add(expansion.skill.name.toLowerCase());
|
|
2528
|
-
}
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
//# sourceMappingURL=chat_controller.js.map
|