@markusylisiurunen/tau 0.2.35 → 0.2.37
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 +63 -5
- package/dist/core/cli.js +20 -1
- package/dist/core/cli.js.map +1 -1
- package/dist/core/commands/registry.js +16 -0
- package/dist/core/commands/registry.js.map +1 -1
- package/dist/core/config/builtin_themes.js +31 -0
- package/dist/core/config/builtin_themes.js.map +1 -1
- package/dist/core/config/index.js +1 -1
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/schema.js +24 -7
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/index.js +5 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/modes/index.js +2 -0
- package/dist/core/modes/index.js.map +1 -1
- package/dist/core/modes/rpc_protocol.js +240 -0
- package/dist/core/modes/rpc_protocol.js.map +1 -0
- package/dist/core/modes/rpc_server.js +275 -0
- package/dist/core/modes/rpc_server.js.map +1 -0
- package/dist/core/runtime/chat_runtime.js +124 -0
- package/dist/core/runtime/chat_runtime.js.map +1 -0
- package/dist/core/runtime/conversation_turn_runtime.js +49 -0
- package/dist/core/runtime/conversation_turn_runtime.js.map +1 -0
- package/dist/core/runtime/session_prompt_composer.js +55 -0
- package/dist/core/runtime/session_prompt_composer.js.map +1 -0
- package/dist/core/utils/auto_retry.js +1 -1
- package/dist/core/utils/auto_retry.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/main.js +140 -5
- package/dist/main.js.map +1 -1
- package/dist/sdk/client.d.ts +2 -0
- package/dist/sdk/client.js +409 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/errors.d.ts +32 -0
- package/dist/sdk/errors.js +37 -0
- package/dist/sdk/errors.js.map +1 -0
- package/dist/sdk/index.d.ts +4 -0
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/types.d.ts +83 -0
- package/dist/sdk/types.js +2 -0
- package/dist/sdk/types.js.map +1 -0
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/chat_controller.js +378 -101
- package/dist/tui/chat_controller.js.map +1 -1
- package/dist/tui/chat_view.js +59 -0
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/ui/custom_editor.js +12 -0
- package/dist/tui/ui/custom_editor.js.map +1 -1
- package/dist/tui/ui/theme/palette.js +1 -0
- package/dist/tui/ui/theme/palette.js.map +1 -1
- package/dist/tui/ui/theme/theme.js.map +1 -1
- package/package.json +11 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, readFile, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { formatCodexAuthError } from "../core/auth/auth_messages.js";
|
|
@@ -8,11 +8,10 @@ import { getAuthPath } from "../core/auth/auth_paths.js";
|
|
|
8
8
|
import { AuthStorage } from "../core/auth/auth_storage.js";
|
|
9
9
|
import { createCredentialResolver, } from "../core/auth/credential_resolver.js";
|
|
10
10
|
import { createCommandRegistry, getRiskLevelDescription, } from "../core/commands/index.js";
|
|
11
|
-
import { createDefaultConfigDeps, loadRuntimeConfig, } from "../core/config/index.js";
|
|
11
|
+
import { createDefaultConfigDeps, getMistralApiKey, loadRuntimeConfig, } from "../core/config/index.js";
|
|
12
|
+
import { ChatRuntime } from "../core/runtime/chat_runtime.js";
|
|
12
13
|
import { createDefaultCoreDeps } from "../core/runtime/deps.js";
|
|
13
14
|
import { createCheckpoint } from "../core/session/checkpoint.js";
|
|
14
|
-
import { CoreSession } from "../core/session/core_session.js";
|
|
15
|
-
import { formatSubagentsForPrompt, getSubagentBasePrompt } from "../core/subagents/registry.js";
|
|
16
15
|
import { buildBashUiText, formatBashUserMessageText, getBashOutputPolicy, prepareBashOutput, } from "../core/tools/bash.js";
|
|
17
16
|
import { ToolCatalog } from "../core/tools/catalog.js";
|
|
18
17
|
import { createLocalToolExecutionBackend } from "../core/tools/execution_backend.js";
|
|
@@ -20,7 +19,7 @@ import { TOOL_NAME_BASH, TOOL_NAME_EDIT } from "../core/tools/tool_names.js";
|
|
|
20
19
|
import { REASONING_LEVELS, } from "../core/types.js";
|
|
21
20
|
import { resolveAgentCwd, resolveSandboxPath } from "../core/utils/agent_environment.js";
|
|
22
21
|
import { findAgentsFilesFromCwdToHome } from "../core/utils/agents_files.js";
|
|
23
|
-
import {
|
|
22
|
+
import { buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesInScopeDetailed, formatCwdChangeNotice, formatRiskLevelChangeNotice, } from "../core/utils/context.js";
|
|
24
23
|
import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "../core/utils/format.js";
|
|
25
24
|
import { getGitRoot } from "../core/utils/git.js";
|
|
26
25
|
import { buildLineDiff, collapseLongUnchangedDiffRuns } from "../core/utils/line_diff.js";
|
|
@@ -59,6 +58,11 @@ const PRUNED_EDIT_ARGUMENT_MARKER = "[content pruned]";
|
|
|
59
58
|
const PRUNE_EDIT_UNCHANGED_CONTEXT_LINES = 4;
|
|
60
59
|
const PRUNE_PREVIEW_MAX_TOKENS = 512;
|
|
61
60
|
const PRUNE_MAX_OVERAGE_RATIO = 0.1;
|
|
61
|
+
const SPEAK_TEMP_FILE_TEMPLATE = "/tmp/tau-speak.XXXXXX";
|
|
62
|
+
const SPEAK_MISTRAL_TRANSCRIBE_MODEL = "voxtral-mini-latest";
|
|
63
|
+
const SPEAK_RECORDING_MIN_BYTES = 1024;
|
|
64
|
+
const SPEAK_RECORDING_MAX_DURATION_MS = 5 * 60 * 1000;
|
|
65
|
+
const CAFFEINATE_COMMAND = "/usr/bin/caffeinate";
|
|
62
66
|
export class ChatController {
|
|
63
67
|
view;
|
|
64
68
|
personas;
|
|
@@ -73,9 +77,11 @@ export class ChatController {
|
|
|
73
77
|
credentialResolver;
|
|
74
78
|
authPath;
|
|
75
79
|
sandboxEnabled;
|
|
80
|
+
caffeinated;
|
|
76
81
|
sandboxRootReal;
|
|
77
82
|
agentCwd;
|
|
78
83
|
includeAgentContext;
|
|
84
|
+
runtime;
|
|
79
85
|
engine;
|
|
80
86
|
commandRegistry;
|
|
81
87
|
commandHandlers;
|
|
@@ -93,9 +99,9 @@ export class ChatController {
|
|
|
93
99
|
showThinking = false;
|
|
94
100
|
compactToolUi = true;
|
|
95
101
|
commandHint;
|
|
102
|
+
assistantTurnInterruptRequested = false;
|
|
96
103
|
currentTurnAbort;
|
|
97
104
|
riskLevel = "read-only";
|
|
98
|
-
environmentTag = "";
|
|
99
105
|
projectContextBlock;
|
|
100
106
|
projectFiles = [];
|
|
101
107
|
projectFilesCwd;
|
|
@@ -103,8 +109,6 @@ export class ChatController {
|
|
|
103
109
|
isInFileAutocomplete = false;
|
|
104
110
|
agentsFiles;
|
|
105
111
|
agentsConfigErrors;
|
|
106
|
-
baseSystemPrompt = "";
|
|
107
|
-
subagentPrompts = {};
|
|
108
112
|
pendingRiskLevelChange;
|
|
109
113
|
pendingCwdChange;
|
|
110
114
|
expandedFilesInCurrentPrompt = new Set();
|
|
@@ -114,6 +118,11 @@ export class ChatController {
|
|
|
114
118
|
lastTurnDurationMs = 0;
|
|
115
119
|
turnTimer;
|
|
116
120
|
lastEmptySubmitAt;
|
|
121
|
+
speakRecording;
|
|
122
|
+
isTranscribingSpeak = false;
|
|
123
|
+
speakTransition;
|
|
124
|
+
turnCaffeinate;
|
|
125
|
+
disableCaffeinateForSession = false;
|
|
117
126
|
constructor(options) {
|
|
118
127
|
this.view = options.view;
|
|
119
128
|
this.deps = options.deps ?? createDefaultCoreDeps();
|
|
@@ -129,7 +138,8 @@ export class ChatController {
|
|
|
129
138
|
this.bashCommands = options.bashCommands ?? [];
|
|
130
139
|
this.initialUserMessage = options.initialUserMessage;
|
|
131
140
|
this.config = options.config ?? {};
|
|
132
|
-
this.sandboxEnabled = options.sandboxEnabled
|
|
141
|
+
this.sandboxEnabled = options.sandboxEnabled;
|
|
142
|
+
this.caffeinated = options.caffeinated ?? false;
|
|
133
143
|
this.sandboxRootReal = this.sandboxEnabled ? this.resolveSandboxRoot(cwd) : undefined;
|
|
134
144
|
this.agentCwd = resolveAgentCwd({
|
|
135
145
|
cwd,
|
|
@@ -175,7 +185,6 @@ export class ChatController {
|
|
|
175
185
|
this.riskLevel = options.initialRiskLevel;
|
|
176
186
|
}
|
|
177
187
|
const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
|
|
178
|
-
this.updateSystemPrompts(skillsContext.skillsBlock);
|
|
179
188
|
this.toolBackend =
|
|
180
189
|
options.toolBackend ??
|
|
181
190
|
createLocalToolExecutionBackend({
|
|
@@ -186,15 +195,26 @@ export class ChatController {
|
|
|
186
195
|
throw new Error("sandbox enabled but tool backend is not sandboxed.");
|
|
187
196
|
}
|
|
188
197
|
const toolRegistry = ToolCatalog.createRegistry(this.toolBackend);
|
|
189
|
-
this.
|
|
198
|
+
this.runtime = ChatRuntime.create({
|
|
190
199
|
persona: this.currentPersona,
|
|
191
|
-
systemPrompt: this.baseSystemPrompt,
|
|
192
|
-
subagentPrompts: this.subagentPrompts,
|
|
193
200
|
riskLevel: this.riskLevel,
|
|
194
201
|
toolRegistry,
|
|
202
|
+
promptContext: {
|
|
203
|
+
cwd: this.agentCwd,
|
|
204
|
+
projectContextBlock: this.projectContextBlock,
|
|
205
|
+
sandboxEnabled: this.sandboxEnabled,
|
|
206
|
+
sandboxEnvironmentInfo: this.config.sandbox?.environmentInfo,
|
|
207
|
+
skillsBlock: skillsContext.skillsBlock,
|
|
208
|
+
},
|
|
209
|
+
environment: {
|
|
210
|
+
now: () => this.deps.clock.now(),
|
|
211
|
+
platform: () => this.deps.env.platform(),
|
|
212
|
+
nodeVersion: () => this.deps.env.nodeVersion(),
|
|
213
|
+
},
|
|
195
214
|
config: this.config,
|
|
196
215
|
deps: this.deps,
|
|
197
216
|
});
|
|
217
|
+
this.engine = this.runtime.session;
|
|
198
218
|
this.subagentUnsubscribe = this.engine.onSubagentEvent((event) => this.onEvent(event));
|
|
199
219
|
this.commandRegistry = createCommandRegistry();
|
|
200
220
|
this.commandHandlers = {
|
|
@@ -211,6 +231,7 @@ export class ChatController {
|
|
|
211
231
|
pruneLargest: (extra) => this.pruneToolResults("largest", extra),
|
|
212
232
|
pruneSmart: (extra) => this.pruneToolResultsSmart(extra),
|
|
213
233
|
reload: () => this.reloadContent(),
|
|
234
|
+
speak: () => this.toggleSpeakCapture(),
|
|
214
235
|
risk: (level) => this.setRiskLevel(level),
|
|
215
236
|
persona: (id) => this.switchPersona(id),
|
|
216
237
|
prompt: (id) => this.insertPrompt(id),
|
|
@@ -263,6 +284,7 @@ export class ChatController {
|
|
|
263
284
|
onCtrlR: () => this.cycleRiskLevel(),
|
|
264
285
|
onCtrlP: () => this.cyclePersonality(),
|
|
265
286
|
onCtrlS: () => void this.stashEditorToClipboard(),
|
|
287
|
+
onCtrlY: () => void this.toggleSpeakCapture(),
|
|
266
288
|
onEscape: () => this.onInterrupt(),
|
|
267
289
|
onCtrlF: () => {
|
|
268
290
|
this.expandFileMentions().catch((err) => {
|
|
@@ -284,6 +306,11 @@ export class ChatController {
|
|
|
284
306
|
}
|
|
285
307
|
async dispose() {
|
|
286
308
|
this.subagentUnsubscribe?.();
|
|
309
|
+
if (this.speakTransition) {
|
|
310
|
+
await this.speakTransition;
|
|
311
|
+
}
|
|
312
|
+
await this.cancelSpeakCapture();
|
|
313
|
+
await this.stopTurnCaffeinate();
|
|
287
314
|
if (!this.toolBackendDispose)
|
|
288
315
|
return;
|
|
289
316
|
await this.toolBackendDispose();
|
|
@@ -293,6 +320,10 @@ export class ChatController {
|
|
|
293
320
|
await this.handleSubmit(text);
|
|
294
321
|
}
|
|
295
322
|
onInterrupt() {
|
|
323
|
+
if (this.speakRecording) {
|
|
324
|
+
void this.runSpeakTransition(() => this.stopSpeakCapture());
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
296
327
|
this.interruptAssistantTurn();
|
|
297
328
|
}
|
|
298
329
|
onEvent(event) {
|
|
@@ -444,6 +475,8 @@ export class ChatController {
|
|
|
444
475
|
});
|
|
445
476
|
}
|
|
446
477
|
getInputMode() {
|
|
478
|
+
if (this.speakRecording)
|
|
479
|
+
return "recording";
|
|
447
480
|
if (this.isBashIncognito)
|
|
448
481
|
return "bash_incognito";
|
|
449
482
|
if (this.isBashMode)
|
|
@@ -760,13 +793,25 @@ export class ChatController {
|
|
|
760
793
|
});
|
|
761
794
|
}
|
|
762
795
|
interruptAssistantTurn() {
|
|
763
|
-
if (!this.isStreaming
|
|
796
|
+
if (!this.isStreaming)
|
|
797
|
+
return;
|
|
798
|
+
if (this.runtime.isTurnRunning) {
|
|
799
|
+
if (this.assistantTurnInterruptRequested)
|
|
800
|
+
return;
|
|
801
|
+
this.assistantTurnInterruptRequested = true;
|
|
802
|
+
this.runtime.interruptTurn();
|
|
803
|
+
this.view.addSystemMessage("interrupted", "error");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (this.currentTurnAbort?.signal.aborted)
|
|
764
807
|
return;
|
|
765
808
|
this.currentTurnAbort?.abort();
|
|
766
809
|
this.view.addSystemMessage("interrupted", "error");
|
|
767
810
|
}
|
|
768
811
|
// Input Handling --------------------------------------------------------------------------------
|
|
769
812
|
beforeSubmit(text) {
|
|
813
|
+
if (this.speakRecording)
|
|
814
|
+
return false;
|
|
770
815
|
if (!this.isStreaming)
|
|
771
816
|
return true;
|
|
772
817
|
const trimmed = text.trimStart();
|
|
@@ -853,6 +898,8 @@ export class ChatController {
|
|
|
853
898
|
return "prune smart-selected tool results and compact edit calls, optional fraction and guidance";
|
|
854
899
|
case "reload":
|
|
855
900
|
return "reload prompts, skills, themes, bash commands, and AGENTS.md";
|
|
901
|
+
case "speak":
|
|
902
|
+
return "toggle microphone recording and transcribe to editor";
|
|
856
903
|
case "risk":
|
|
857
904
|
return "set risk level: /risk:read-only or /risk:read-write";
|
|
858
905
|
case "bash":
|
|
@@ -1089,6 +1136,258 @@ export class ChatController {
|
|
|
1089
1136
|
this.view.addMessage({ type: "user", text: trimmed }, historyEntryId);
|
|
1090
1137
|
await this.runAssistantTurn();
|
|
1091
1138
|
}
|
|
1139
|
+
async toggleSpeakCapture() {
|
|
1140
|
+
if (this.speakTransition) {
|
|
1141
|
+
this.view.addSystemMessage("speech recording state change already in progress", "warn");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (this.speakRecording) {
|
|
1145
|
+
await this.runSpeakTransition(() => this.stopSpeakCapture());
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (this.isTranscribingSpeak) {
|
|
1149
|
+
this.view.addSystemMessage("speech transcription already in progress", "warn");
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (this.isStreaming) {
|
|
1153
|
+
this.view.addSystemMessage("wait for the assistant to finish before recording", "warn");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
await this.runSpeakTransition(() => this.startSpeakCapture());
|
|
1157
|
+
}
|
|
1158
|
+
async runSpeakTransition(task) {
|
|
1159
|
+
if (this.speakTransition) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const transition = task();
|
|
1163
|
+
this.speakTransition = transition;
|
|
1164
|
+
try {
|
|
1165
|
+
await transition;
|
|
1166
|
+
}
|
|
1167
|
+
finally {
|
|
1168
|
+
if (this.speakTransition === transition) {
|
|
1169
|
+
this.speakTransition = undefined;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async startSpeakCapture() {
|
|
1174
|
+
const apiKey = getMistralApiKey(this.config, this.deps.env.env());
|
|
1175
|
+
if (!apiKey) {
|
|
1176
|
+
this.view.addSystemMessage("set MISTRAL_API_KEY or apiKeys.mistral to use /speak", "error");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
let audioPath;
|
|
1180
|
+
try {
|
|
1181
|
+
audioPath = await this.createSpeakTempFilePath();
|
|
1182
|
+
const abortController = new AbortController();
|
|
1183
|
+
const completion = this.deps.spawn("ffmpeg", [
|
|
1184
|
+
"-hide_banner",
|
|
1185
|
+
"-loglevel",
|
|
1186
|
+
"error",
|
|
1187
|
+
"-nostdin",
|
|
1188
|
+
"-f",
|
|
1189
|
+
"avfoundation",
|
|
1190
|
+
"-i",
|
|
1191
|
+
":0",
|
|
1192
|
+
"-ac",
|
|
1193
|
+
"1",
|
|
1194
|
+
"-ar",
|
|
1195
|
+
"16000",
|
|
1196
|
+
"-c:a",
|
|
1197
|
+
"pcm_s16le",
|
|
1198
|
+
"-f",
|
|
1199
|
+
"wav",
|
|
1200
|
+
"-y",
|
|
1201
|
+
audioPath,
|
|
1202
|
+
], {
|
|
1203
|
+
detached: true,
|
|
1204
|
+
killProcessGroup: true,
|
|
1205
|
+
signal: abortController.signal,
|
|
1206
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
1207
|
+
});
|
|
1208
|
+
const recording = {
|
|
1209
|
+
audioPath,
|
|
1210
|
+
stopRequested: false,
|
|
1211
|
+
abortController,
|
|
1212
|
+
completion,
|
|
1213
|
+
};
|
|
1214
|
+
recording.maxDurationTimeout = setTimeout(() => {
|
|
1215
|
+
if (this.speakRecording !== recording || this.speakTransition)
|
|
1216
|
+
return;
|
|
1217
|
+
void this.runSpeakTransition(() => this.stopSpeakCapture());
|
|
1218
|
+
}, SPEAK_RECORDING_MAX_DURATION_MS);
|
|
1219
|
+
this.speakRecording = recording;
|
|
1220
|
+
this.view.setEditorInputEnabled(false);
|
|
1221
|
+
this.refreshStatus();
|
|
1222
|
+
void this.watchSpeakRecording(recording);
|
|
1223
|
+
}
|
|
1224
|
+
catch (err) {
|
|
1225
|
+
if (audioPath) {
|
|
1226
|
+
await this.cleanupSpeakTempFile(audioPath);
|
|
1227
|
+
}
|
|
1228
|
+
this.view.addSystemMessage(`failed to start recording: ${err.message}`, "error");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async stopSpeakCapture() {
|
|
1232
|
+
const recording = this.speakRecording;
|
|
1233
|
+
if (!recording)
|
|
1234
|
+
return;
|
|
1235
|
+
recording.stopRequested = true;
|
|
1236
|
+
this.clearSpeakRecordingMaxDurationTimeout(recording);
|
|
1237
|
+
this.speakRecording = undefined;
|
|
1238
|
+
this.view.setEditorInputEnabled(true);
|
|
1239
|
+
this.refreshStatus();
|
|
1240
|
+
recording.abortController.abort();
|
|
1241
|
+
try {
|
|
1242
|
+
await recording.completion;
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
this.view.addSystemMessage(`recording failed: ${err.message}`, "error");
|
|
1246
|
+
await this.cleanupSpeakTempFile(recording.audioPath);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
this.isTranscribingSpeak = true;
|
|
1250
|
+
try {
|
|
1251
|
+
const audio = await readFile(recording.audioPath);
|
|
1252
|
+
if (audio.byteLength < SPEAK_RECORDING_MIN_BYTES) {
|
|
1253
|
+
this.view.addSystemMessage("recording too short, try again", "warn");
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const transcript = await this.transcribeSpeakAudio(audio);
|
|
1257
|
+
const text = transcript.trim();
|
|
1258
|
+
if (!text) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
this.view.insertEditorTextAtCursor(text);
|
|
1262
|
+
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
this.view.addSystemMessage(`speech transcription failed: ${err.message}`, "error");
|
|
1265
|
+
}
|
|
1266
|
+
finally {
|
|
1267
|
+
this.isTranscribingSpeak = false;
|
|
1268
|
+
await this.cleanupSpeakTempFile(recording.audioPath);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async cancelSpeakCapture() {
|
|
1272
|
+
const recording = this.speakRecording;
|
|
1273
|
+
if (!recording)
|
|
1274
|
+
return;
|
|
1275
|
+
recording.stopRequested = true;
|
|
1276
|
+
this.clearSpeakRecordingMaxDurationTimeout(recording);
|
|
1277
|
+
this.speakRecording = undefined;
|
|
1278
|
+
this.view.setEditorInputEnabled(true);
|
|
1279
|
+
this.refreshStatus();
|
|
1280
|
+
recording.abortController.abort();
|
|
1281
|
+
try {
|
|
1282
|
+
await recording.completion;
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
// ignore disposal errors
|
|
1286
|
+
}
|
|
1287
|
+
await this.cleanupSpeakTempFile(recording.audioPath);
|
|
1288
|
+
}
|
|
1289
|
+
async watchSpeakRecording(recording) {
|
|
1290
|
+
try {
|
|
1291
|
+
const result = await recording.completion;
|
|
1292
|
+
this.clearSpeakRecordingMaxDurationTimeout(recording);
|
|
1293
|
+
if (this.speakRecording !== recording || recording.stopRequested)
|
|
1294
|
+
return;
|
|
1295
|
+
this.speakRecording = undefined;
|
|
1296
|
+
this.view.setEditorInputEnabled(true);
|
|
1297
|
+
this.refreshStatus();
|
|
1298
|
+
const detail = result.exitCode !== null
|
|
1299
|
+
? `ffmpeg exited with code ${result.exitCode}`
|
|
1300
|
+
: result.closeSignal
|
|
1301
|
+
? `ffmpeg terminated by signal ${result.closeSignal}`
|
|
1302
|
+
: "ffmpeg exited";
|
|
1303
|
+
this.view.addSystemMessage(`recording stopped unexpectedly (${detail})`, "error");
|
|
1304
|
+
await this.cleanupSpeakTempFile(recording.audioPath);
|
|
1305
|
+
}
|
|
1306
|
+
catch (err) {
|
|
1307
|
+
this.clearSpeakRecordingMaxDurationTimeout(recording);
|
|
1308
|
+
if (this.speakRecording !== recording || recording.stopRequested)
|
|
1309
|
+
return;
|
|
1310
|
+
this.speakRecording = undefined;
|
|
1311
|
+
this.view.setEditorInputEnabled(true);
|
|
1312
|
+
this.refreshStatus();
|
|
1313
|
+
const error = err;
|
|
1314
|
+
if (error.code === "ENOENT") {
|
|
1315
|
+
this.view.addSystemMessage("ffmpeg not found. install it with: brew install ffmpeg", "error");
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
this.view.addSystemMessage(`recording failed: ${error.message}`, "error");
|
|
1319
|
+
}
|
|
1320
|
+
await this.cleanupSpeakTempFile(recording.audioPath);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
clearSpeakRecordingMaxDurationTimeout(recording) {
|
|
1324
|
+
if (!recording.maxDurationTimeout)
|
|
1325
|
+
return;
|
|
1326
|
+
clearTimeout(recording.maxDurationTimeout);
|
|
1327
|
+
recording.maxDurationTimeout = undefined;
|
|
1328
|
+
}
|
|
1329
|
+
async createSpeakTempFilePath() {
|
|
1330
|
+
const result = await this.deps.spawn("mktemp", [SPEAK_TEMP_FILE_TEMPLATE]);
|
|
1331
|
+
if (result.exitCode !== 0) {
|
|
1332
|
+
const message = result.stderr.trim() || result.stdout.trim() || "mktemp failed";
|
|
1333
|
+
throw new Error(message);
|
|
1334
|
+
}
|
|
1335
|
+
const path = result.stdout.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
1336
|
+
if (!path) {
|
|
1337
|
+
throw new Error("mktemp returned an empty path");
|
|
1338
|
+
}
|
|
1339
|
+
return path;
|
|
1340
|
+
}
|
|
1341
|
+
async transcribeSpeakAudio(audio) {
|
|
1342
|
+
const apiKey = getMistralApiKey(this.config, this.deps.env.env());
|
|
1343
|
+
if (!apiKey) {
|
|
1344
|
+
throw new Error("missing MISTRAL_API_KEY or apiKeys.mistral");
|
|
1345
|
+
}
|
|
1346
|
+
const formData = new FormData();
|
|
1347
|
+
formData.append("model", SPEAK_MISTRAL_TRANSCRIBE_MODEL);
|
|
1348
|
+
formData.append("file", new Blob([audio], { type: "audio/wav" }), "speech.wav");
|
|
1349
|
+
formData.append("language", "en");
|
|
1350
|
+
const response = await fetch("https://api.mistral.ai/v1/audio/transcriptions", {
|
|
1351
|
+
method: "POST",
|
|
1352
|
+
headers: {
|
|
1353
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1354
|
+
},
|
|
1355
|
+
body: formData,
|
|
1356
|
+
});
|
|
1357
|
+
let payload;
|
|
1358
|
+
const responseText = await response.text();
|
|
1359
|
+
if (responseText) {
|
|
1360
|
+
try {
|
|
1361
|
+
payload = JSON.parse(responseText);
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
payload = undefined;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!response.ok) {
|
|
1368
|
+
const fromObject = payload && typeof payload === "object" && "message" in payload
|
|
1369
|
+
? payload.message
|
|
1370
|
+
: undefined;
|
|
1371
|
+
const fromString = typeof fromObject === "string" ? fromObject : undefined;
|
|
1372
|
+
const fallback = responseText.trim() || `HTTP ${response.status}`;
|
|
1373
|
+
throw new Error(fromString || fallback);
|
|
1374
|
+
}
|
|
1375
|
+
const text = payload && typeof payload === "object" && "text" in payload
|
|
1376
|
+
? payload.text
|
|
1377
|
+
: undefined;
|
|
1378
|
+
if (typeof text !== "string") {
|
|
1379
|
+
return "";
|
|
1380
|
+
}
|
|
1381
|
+
return text;
|
|
1382
|
+
}
|
|
1383
|
+
async cleanupSpeakTempFile(path) {
|
|
1384
|
+
try {
|
|
1385
|
+
await unlink(path);
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
// best-effort cleanup
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1092
1391
|
getMemoryModeFilePath() {
|
|
1093
1392
|
const cwd = this.deps.env.cwd();
|
|
1094
1393
|
const home = this.deps.env.home();
|
|
@@ -1280,86 +1579,23 @@ export class ChatController {
|
|
|
1280
1579
|
escapeXmlAttribute(text) {
|
|
1281
1580
|
return this.escapeXml(text).replace(/"/g, """).replace(/'/g, "'");
|
|
1282
1581
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
this.environmentTag = buildEnvironmentTag({
|
|
1286
|
-
riskLevel: this.riskLevel,
|
|
1582
|
+
syncRuntimePromptContext() {
|
|
1583
|
+
this.runtime.updatePromptContext({
|
|
1287
1584
|
cwd: this.agentCwd,
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
});
|
|
1292
|
-
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1293
|
-
this.baseSystemPrompt = this.buildSystemPrompt({
|
|
1294
|
-
persona: this.currentPersona,
|
|
1295
|
-
skillsBlock: resolvedSkillsBlock,
|
|
1296
|
-
});
|
|
1297
|
-
this.subagentPrompts = this.buildSubagentPromptMap({
|
|
1298
|
-
persona: this.currentPersona,
|
|
1299
|
-
skillsBlock: resolvedSkillsBlock,
|
|
1300
|
-
timestamp,
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
updateSubagentPrompts(skillsBlock) {
|
|
1304
|
-
const timestamp = new Date(this.deps.clock.now()).toISOString();
|
|
1305
|
-
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1306
|
-
this.subagentPrompts = this.buildSubagentPromptMap({
|
|
1307
|
-
persona: this.currentPersona,
|
|
1308
|
-
skillsBlock: resolvedSkillsBlock,
|
|
1309
|
-
timestamp,
|
|
1585
|
+
projectContextBlock: this.projectContextBlock,
|
|
1586
|
+
sandboxEnabled: this.sandboxEnabled,
|
|
1587
|
+
sandboxEnvironmentInfo: this.config.sandbox?.environmentInfo,
|
|
1310
1588
|
});
|
|
1311
1589
|
}
|
|
1312
1590
|
rebuildSystemPrompt(skillsBlock) {
|
|
1313
|
-
this.
|
|
1314
|
-
this.
|
|
1591
|
+
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1592
|
+
this.syncRuntimePromptContext();
|
|
1593
|
+
this.runtime.setPersona(this.currentPersona, { skillsBlock: resolvedSkillsBlock });
|
|
1315
1594
|
}
|
|
1316
1595
|
rebuildSubagentPrompts(skillsBlock) {
|
|
1317
|
-
this.
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1320
|
-
buildSystemPrompt(args) {
|
|
1321
|
-
return buildBaseSystemPrompt({
|
|
1322
|
-
personaSystemPrompt: args.persona.systemPrompt,
|
|
1323
|
-
skillsBlock: args.skillsBlock,
|
|
1324
|
-
projectContextBlock: this.projectContextBlock,
|
|
1325
|
-
sandboxInfoBlock: this.sandboxEnabled
|
|
1326
|
-
? buildSandboxInfoBlock(this.config.sandbox?.environmentInfo)
|
|
1327
|
-
: undefined,
|
|
1328
|
-
environmentTag: this.environmentTag,
|
|
1329
|
-
subagentsBlock: formatSubagentsForPrompt(args.persona),
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
buildSubagentPromptMap(args) {
|
|
1333
|
-
const subagents = args.persona.subagents;
|
|
1334
|
-
if (!subagents || Object.keys(subagents).length === 0) {
|
|
1335
|
-
return {};
|
|
1336
|
-
}
|
|
1337
|
-
const sandboxInfoBlock = this.sandboxEnabled
|
|
1338
|
-
? buildSandboxInfoBlock(this.config.sandbox?.environmentInfo)
|
|
1339
|
-
: undefined;
|
|
1340
|
-
const prompts = {};
|
|
1341
|
-
for (const [name, cfg] of Object.entries(subagents)) {
|
|
1342
|
-
if (!cfg)
|
|
1343
|
-
continue;
|
|
1344
|
-
const basePrompt = getSubagentBasePrompt(name, cfg);
|
|
1345
|
-
if (!basePrompt)
|
|
1346
|
-
continue;
|
|
1347
|
-
const environmentTag = buildEnvironmentTag({
|
|
1348
|
-
riskLevel: cfg.riskLevel ?? this.riskLevel,
|
|
1349
|
-
cwd: this.agentCwd,
|
|
1350
|
-
datetime: args.timestamp,
|
|
1351
|
-
platform: this.deps.env.platform(),
|
|
1352
|
-
nodeVersion: this.deps.env.nodeVersion(),
|
|
1353
|
-
});
|
|
1354
|
-
prompts[name] = buildBaseSystemPrompt({
|
|
1355
|
-
personaSystemPrompt: basePrompt,
|
|
1356
|
-
skillsBlock: args.skillsBlock,
|
|
1357
|
-
projectContextBlock: this.projectContextBlock,
|
|
1358
|
-
sandboxInfoBlock,
|
|
1359
|
-
environmentTag,
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
return prompts;
|
|
1596
|
+
const resolvedSkillsBlock = skillsBlock ?? this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock;
|
|
1597
|
+
this.syncRuntimePromptContext();
|
|
1598
|
+
this.runtime.rebuildSubagentPrompts({ skillsBlock: resolvedSkillsBlock });
|
|
1363
1599
|
}
|
|
1364
1600
|
applyCompactedHistoryUi(compactionMessage) {
|
|
1365
1601
|
this.view.resetToolUiSession();
|
|
@@ -2181,10 +2417,10 @@ export class ChatController {
|
|
|
2181
2417
|
setRiskLevel(level, options) {
|
|
2182
2418
|
const previous = this.riskLevel;
|
|
2183
2419
|
this.riskLevel = level;
|
|
2184
|
-
this.
|
|
2420
|
+
this.syncRuntimePromptContext();
|
|
2421
|
+
this.runtime.setRiskLevel(level);
|
|
2185
2422
|
this.refreshStatus();
|
|
2186
2423
|
if (previous !== level) {
|
|
2187
|
-
this.rebuildSubagentPrompts();
|
|
2188
2424
|
const from = this.pendingRiskLevelChange?.from ?? previous;
|
|
2189
2425
|
if (from === level) {
|
|
2190
2426
|
this.pendingRiskLevelChange = undefined;
|
|
@@ -2271,7 +2507,7 @@ export class ChatController {
|
|
|
2271
2507
|
const previousThemeId = this.activeThemeId ?? this.config.defaultTheme;
|
|
2272
2508
|
const messages = [];
|
|
2273
2509
|
this.config = runtime.config;
|
|
2274
|
-
this.
|
|
2510
|
+
this.runtime.setConfig(this.config);
|
|
2275
2511
|
if (plan.bashCommands) {
|
|
2276
2512
|
this.bashCommands = runtime.bashCommands;
|
|
2277
2513
|
}
|
|
@@ -2390,36 +2626,77 @@ export class ChatController {
|
|
|
2390
2626
|
this.view.addSystemMessage(`reload failed: ${err.message}`, "error");
|
|
2391
2627
|
}
|
|
2392
2628
|
}
|
|
2629
|
+
startTurnCaffeinate() {
|
|
2630
|
+
if (!this.caffeinated || this.disableCaffeinateForSession || this.turnCaffeinate) {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
if (this.deps.env.platform() !== "darwin") {
|
|
2634
|
+
this.disableCaffeinateForSession = true;
|
|
2635
|
+
this.view.addSystemMessage("--caffeinated is only supported on macOS.", "warn");
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
const abortController = new AbortController();
|
|
2639
|
+
const completion = this.deps.spawn(CAFFEINATE_COMMAND, ["-i"], {
|
|
2640
|
+
detached: true,
|
|
2641
|
+
killProcessGroup: true,
|
|
2642
|
+
signal: abortController.signal,
|
|
2643
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
2644
|
+
});
|
|
2645
|
+
completion.catch(() => { });
|
|
2646
|
+
this.turnCaffeinate = {
|
|
2647
|
+
abortController,
|
|
2648
|
+
completion,
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
async stopTurnCaffeinate() {
|
|
2652
|
+
const session = this.turnCaffeinate;
|
|
2653
|
+
if (!session)
|
|
2654
|
+
return;
|
|
2655
|
+
this.turnCaffeinate = undefined;
|
|
2656
|
+
if (!session.abortController.signal.aborted) {
|
|
2657
|
+
session.abortController.abort();
|
|
2658
|
+
}
|
|
2659
|
+
try {
|
|
2660
|
+
await session.completion;
|
|
2661
|
+
}
|
|
2662
|
+
catch (err) {
|
|
2663
|
+
if (this.disableCaffeinateForSession) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
this.disableCaffeinateForSession = true;
|
|
2667
|
+
const error = err;
|
|
2668
|
+
const details = error.message || "unknown error";
|
|
2669
|
+
this.view.addSystemMessage(`failed to run caffeinate: ${details}`, "warn");
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2393
2672
|
// Assistant Turn --------------------------------------------------------------------------------
|
|
2394
2673
|
async runAssistantTurn() {
|
|
2395
2674
|
this.isStreaming = true;
|
|
2396
|
-
this.currentTurnAbort = new AbortController();
|
|
2397
2675
|
this.view.startWorkingIcon();
|
|
2398
2676
|
this.startTurnTimer();
|
|
2399
2677
|
this.assistantState = undefined;
|
|
2678
|
+
this.assistantTurnInterruptRequested = false;
|
|
2679
|
+
this.startTurnCaffeinate();
|
|
2680
|
+
let runResult = { aborted: false };
|
|
2400
2681
|
try {
|
|
2401
|
-
|
|
2402
|
-
if (this.currentTurnAbort.signal.aborted)
|
|
2403
|
-
break;
|
|
2404
|
-
this.onEvent(event);
|
|
2405
|
-
}
|
|
2682
|
+
runResult = await this.runtime.runTurn((event) => this.onEvent(event));
|
|
2406
2683
|
}
|
|
2407
2684
|
catch (err) {
|
|
2408
2685
|
const message = err.message || "request failed";
|
|
2409
2686
|
this.view.addSystemMessage(message, "error");
|
|
2410
2687
|
}
|
|
2411
2688
|
finally {
|
|
2412
|
-
|
|
2413
|
-
const reason =
|
|
2689
|
+
await this.stopTurnCaffeinate();
|
|
2690
|
+
const reason = runResult.aborted ? "aborted" : "interrupted";
|
|
2414
2691
|
this.view.finalizeToolUiPending(reason);
|
|
2415
2692
|
this.view.stopWorkingIcon();
|
|
2416
2693
|
this.stopTurnTimer();
|
|
2417
2694
|
this.isStreaming = false;
|
|
2418
|
-
this.
|
|
2695
|
+
this.assistantTurnInterruptRequested = false;
|
|
2419
2696
|
this.view.clearToolUiTransientState();
|
|
2420
2697
|
this.pendingIdleNotification = true;
|
|
2421
2698
|
this.view.requestRender();
|
|
2422
|
-
if (
|
|
2699
|
+
if (runResult.aborted) {
|
|
2423
2700
|
this.dequeueQueuedUserMessagesIntoEditor();
|
|
2424
2701
|
}
|
|
2425
2702
|
else {
|