@xingjisoft/neutrino 0.5.2 → 0.6.2
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 +1 -0
- package/assets/fast.txt +18 -0
- package/assets/pro/common.txt +4 -0
- package/assets/pro/main_model.txt +2 -0
- package/assets/pro/plan_model.txt +3 -1
- package/assets/prompt.txt +9 -0
- package/assets/ptc.txt +27 -0
- package/assets/subagent.txt +22 -1
- package/dist/app.js +2 -0
- package/dist/config.js +33 -1
- package/dist/llm/client.js +61 -8
- package/dist/llm/codeExecutor.js +239 -0
- package/dist/llm/codemode.js +164 -0
- package/dist/llm/context.js +110 -0
- package/dist/llm/plan.js +97 -9
- package/dist/llm/risk.js +10 -0
- package/dist/llm/runtime.js +382 -0
- package/dist/llm/search.js +426 -0
- package/dist/llm/searchNetwork.js +129 -0
- package/dist/llm/subagent.js +247 -112
- package/dist/llm/tools.js +101 -4
- package/dist/pro/agents.js +49 -19
- package/dist/pro/preflight.js +24 -2
- package/dist/screens/chat.js +337 -69
- package/dist/web/hub.js +9 -0
- package/dist/web/server.js +57 -8
- package/dist/web/session.js +342 -62
- package/dist/web/terminalBridge.js +10 -1
- package/package.json +10 -5
- package/web/README.md +3 -2
- package/web/app.js +276 -51
- package/web/i18n.js +20 -0
- package/web/index.html +10 -0
- package/web/style.css +57 -11
- package/web/turns.js +7 -8
package/dist/screens/chat.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { Screen } from "../terminal/screen.js";
|
|
2
3
|
import { C } from "../colors.js";
|
|
3
|
-
import { findModel, findConfiguredModel, intensityForModel, contextCompressionThreshold, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled,
|
|
4
|
+
import { findModel, findConfiguredModel, intensityForModel, contextCompressionThreshold, modelContextWindow, sessionDir, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadAgentPrompt, normalizeAgentMode, saveSession, } from "../config.js";
|
|
4
5
|
import { drawHeader } from "../header.js";
|
|
5
6
|
import { computeInputWindow, cursorFromColumn, drawInputBox, drawScrollbar, wrapText, approvalToggle, drawApprovalToggle, inRect } from "../ui.js";
|
|
6
7
|
import { drawSpans, renderMarkdownText } from "../markdown.js";
|
|
7
8
|
import { ellipsize, displayWidth } from "../width.js";
|
|
8
9
|
import { sanitizeText } from "../sanitize.js";
|
|
10
|
+
import { TaskRuntime, TaskControlError, currentTask, linkSignals, taskLimits, taskBudgetScope, resumeTaskSnapshot } from "../llm/runtime.js";
|
|
11
|
+
import { prepareContext, outputAllowance, finalTaskReport } from "../llm/context.js";
|
|
12
|
+
import { settlePlan } from "../llm/plan.js";
|
|
9
13
|
import { LLMClient, describeError } from "../llm/client.js";
|
|
10
14
|
import { formatDelay } from "../llm/retry.js";
|
|
11
15
|
import { advancePlan, buildPlannerInput, lastRequestText, needsPlan, parsePlan, planCardRows, planMessage, planRowsWidth, planTranscript, renderStepDirective, resumePlan, stopPlan, } from "../llm/plan.js";
|
|
12
16
|
import { DELEGATE_TOOL, executeTool, getSubAgentToolDefinitions, getToolDefinitions } from "../llm/tools.js";
|
|
13
|
-
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, } from "../llm/subagent.js";
|
|
17
|
+
import { buildSubAgentTask, formatSubAgentResult, runSubAgent as runSubAgentTask, subAgentRounds, } from "../llm/subagent.js";
|
|
14
18
|
import { assessToolCall } from "../llm/risk.js";
|
|
15
19
|
import { ApprovalGate, captureApprovalContext, describeApprovalIntent, statedReason } from "../llm/approval.js";
|
|
16
20
|
import { COMPACT_KEEP, COMPACT_MAX_CHARS_PER_MESSAGE, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
@@ -21,8 +25,8 @@ const TOOLS = getToolDefinitions();
|
|
|
21
25
|
/** A sub-agent gets the same tools minus delegation: one level, never a tree. */
|
|
22
26
|
const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
|
|
23
27
|
/**
|
|
24
|
-
* A finished turn folds its *work* away — the
|
|
25
|
-
*
|
|
28
|
+
* A finished turn folds its *work* away — the thinking and the tool calls —
|
|
29
|
+
* while the full user prompt and final reply always stay on screen. Doing
|
|
26
30
|
* that is only worth a click when the work is substantial: four or more blocks,
|
|
27
31
|
* or roughly six wrapped lines of text. Short exchanges stay readable without a
|
|
28
32
|
* click. `web/turns.js` applies the same rule in the browser, so both UIs fold
|
|
@@ -79,6 +83,8 @@ export class ChatScreen extends Screen {
|
|
|
79
83
|
loadedContext = null;
|
|
80
84
|
loadedApprovalMode = null;
|
|
81
85
|
loadedPlan = null;
|
|
86
|
+
loadedTask;
|
|
87
|
+
loadedAgentMode = null;
|
|
82
88
|
approvalMode = "request";
|
|
83
89
|
modelProvider = "DeepSeek";
|
|
84
90
|
model = { name: "deepseek-chat", url: "https://api.deepseek.com/v1", api_key: "", supports_streaming: true };
|
|
@@ -105,7 +111,13 @@ export class ChatScreen extends Screen {
|
|
|
105
111
|
busy = false;
|
|
106
112
|
paused = false;
|
|
107
113
|
queued = null;
|
|
114
|
+
taskRuntime = null;
|
|
115
|
+
lastTask;
|
|
116
|
+
taskOutcome = "completed";
|
|
108
117
|
turnToken = 0;
|
|
118
|
+
agentMode = "standard";
|
|
119
|
+
activeAgentMode = "standard";
|
|
120
|
+
turnAbort = null;
|
|
109
121
|
inputValue = "";
|
|
110
122
|
cursor = 0;
|
|
111
123
|
scrollTop = 0;
|
|
@@ -164,6 +176,7 @@ export class ChatScreen extends Screen {
|
|
|
164
176
|
if (this.initialized)
|
|
165
177
|
return;
|
|
166
178
|
this.initialized = true;
|
|
179
|
+
this.agentMode = normalizeAgentMode(this.loadedAgentMode);
|
|
167
180
|
this.approvalMode = normalizeApprovalMode(this.loadedApprovalMode ?? loadConfig().approval_mode);
|
|
168
181
|
const st = this.app.state;
|
|
169
182
|
this.modelProvider = this.loadedProvider ?? st.selectedProvider;
|
|
@@ -201,6 +214,7 @@ export class ChatScreen extends Screen {
|
|
|
201
214
|
for (const start of this.userMessageIndexes())
|
|
202
215
|
this.finishedTurns.add(start);
|
|
203
216
|
this.plan = resumePlan(this.loadedPlan);
|
|
217
|
+
this.lastTask = resumeTaskSnapshot(this.loadedTask);
|
|
204
218
|
this.context = normalizeContext(this.loadedContext, this.messages.filter((m) => m.role !== "system").length);
|
|
205
219
|
this.liveDir = this.loadedCwd ?? process.cwd();
|
|
206
220
|
this.checkpoint.register();
|
|
@@ -258,6 +272,9 @@ export class ChatScreen extends Screen {
|
|
|
258
272
|
model: this.model.name,
|
|
259
273
|
intensity: this.intensity,
|
|
260
274
|
approvalMode: this.approvalMode,
|
|
275
|
+
agentMode: this.agentMode,
|
|
276
|
+
activeAgentMode: this.busy ? this.activeAgentMode : this.agentMode,
|
|
277
|
+
task: this.taskRuntime?.snapshot() ?? this.lastTask,
|
|
261
278
|
busy: this.busy,
|
|
262
279
|
paused: this.paused,
|
|
263
280
|
queued: this.queued,
|
|
@@ -300,6 +317,11 @@ export class ChatScreen extends Screen {
|
|
|
300
317
|
if (this.paused || (!this.busy && !this.queued))
|
|
301
318
|
return;
|
|
302
319
|
this.paused = true;
|
|
320
|
+
this.subagent = null;
|
|
321
|
+
this.retryNotice = null;
|
|
322
|
+
this.reviewing = null;
|
|
323
|
+
this.planning = false;
|
|
324
|
+
this.turnAbort?.abort();
|
|
303
325
|
this.turnToken++;
|
|
304
326
|
// A queued decision would otherwise keep the agent awaiting forever.
|
|
305
327
|
if (this.pendingApproval)
|
|
@@ -323,6 +345,12 @@ export class ChatScreen extends Screen {
|
|
|
323
345
|
this.settleApproval(approved);
|
|
324
346
|
return true;
|
|
325
347
|
}
|
|
348
|
+
liveSetAgentMode(mode) {
|
|
349
|
+
this.agentMode = mode;
|
|
350
|
+
this.save();
|
|
351
|
+
this.publishLive();
|
|
352
|
+
return { ok: true };
|
|
353
|
+
}
|
|
326
354
|
liveSetApprovalMode(mode) {
|
|
327
355
|
if (this.approvalMode === mode)
|
|
328
356
|
return;
|
|
@@ -344,7 +372,10 @@ export class ChatScreen extends Screen {
|
|
|
344
372
|
this.app.render();
|
|
345
373
|
return;
|
|
346
374
|
}
|
|
375
|
+
this.turnAbort?.abort();
|
|
347
376
|
this.turnToken++;
|
|
377
|
+
this.taskRuntime = null;
|
|
378
|
+
this.lastTask = undefined;
|
|
348
379
|
if (this.pendingApproval)
|
|
349
380
|
this.settleApproval(false);
|
|
350
381
|
this.busy = false;
|
|
@@ -658,6 +689,11 @@ export class ChatScreen extends Screen {
|
|
|
658
689
|
return;
|
|
659
690
|
this.paused = !this.paused;
|
|
660
691
|
if (this.paused) {
|
|
692
|
+
this.reviewing = null;
|
|
693
|
+
this.planning = false;
|
|
694
|
+
this.subagent = null;
|
|
695
|
+
this.retryNotice = null;
|
|
696
|
+
this.turnAbort?.abort();
|
|
661
697
|
this.turnToken++;
|
|
662
698
|
this.busy = false;
|
|
663
699
|
this.app.state.isThinking = false;
|
|
@@ -723,9 +759,9 @@ export class ChatScreen extends Screen {
|
|
|
723
759
|
return null;
|
|
724
760
|
}
|
|
725
761
|
finally {
|
|
726
|
-
this.planning = false;
|
|
727
|
-
this.retryNotice = null;
|
|
728
762
|
if (token === this.turnToken) {
|
|
763
|
+
this.planning = false;
|
|
764
|
+
this.retryNotice = null;
|
|
729
765
|
this.app.render();
|
|
730
766
|
this.publishLive();
|
|
731
767
|
}
|
|
@@ -754,16 +790,59 @@ export class ChatScreen extends Screen {
|
|
|
754
790
|
* own. Without a plan the turn runs in one pass, as before.
|
|
755
791
|
*/
|
|
756
792
|
async runTurn(token, turnMessages, approvalContext) {
|
|
793
|
+
const runtime = new TaskRuntime({
|
|
794
|
+
request: approvalContext.prompt, priorUserRequests: approvalContext.priorUserRequests,
|
|
795
|
+
cwd: this.liveDir, signal: this.turnAbort?.signal, limits: taskLimits(loadConfig("standard")),
|
|
796
|
+
budgetScope: taskBudgetScope(loadConfig("standard")),
|
|
797
|
+
journalDir: path.join(sessionDir("standard"), "tasks", path.basename(this.sessionId || "unsaved")),
|
|
798
|
+
});
|
|
799
|
+
this.taskRuntime = runtime;
|
|
800
|
+
this.taskOutcome = "completed";
|
|
801
|
+
let failed = false;
|
|
802
|
+
try {
|
|
803
|
+
await runtime.run(() => this.runTaskTurn(token, turnMessages, approvalContext));
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
failed = true;
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
finally {
|
|
810
|
+
await runtime.drain();
|
|
811
|
+
if (runtime.signal.aborted)
|
|
812
|
+
settlePlan(runtime.plan ?? null, true);
|
|
813
|
+
runtime.finish(runtime.signal.aborted ? "cancelled" : failed ? "failed" :
|
|
814
|
+
runtime.plan?.steps.some(step => step.status === "failed") ? "failed" :
|
|
815
|
+
runtime.plan?.steps.some(step => !["done", "skipped"].includes(step.status)) ? "partial" : this.taskOutcome);
|
|
816
|
+
if (this.taskRuntime === runtime) {
|
|
817
|
+
if (runtime.signal.aborted && token === this.turnToken && this.busy && !this.paused) {
|
|
818
|
+
if (this.taskOutcome !== "cancelled")
|
|
819
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(runtime.signal.reason)}]\n${runtime.partialReport()}` });
|
|
820
|
+
this.streamingContent = null;
|
|
821
|
+
this.onTurnFinished(false);
|
|
822
|
+
}
|
|
823
|
+
if (this.taskRuntime === runtime) {
|
|
824
|
+
this.lastTask = runtime.snapshot();
|
|
825
|
+
this.save();
|
|
826
|
+
this.publishLive();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async runTaskTurn(token, turnMessages, approvalContext) {
|
|
757
832
|
// Shrink the history before building the request when it has outgrown the
|
|
758
833
|
// configured budget.
|
|
759
834
|
await this.autoCompress(token);
|
|
760
835
|
if (token !== this.turnToken)
|
|
761
836
|
return;
|
|
762
|
-
|
|
837
|
+
if (this.activeAgentMode !== "standard")
|
|
838
|
+
this.plan = null;
|
|
839
|
+
const plan = this.activeAgentMode === "standard" ? await this.makePlan(token) : null;
|
|
763
840
|
if (token !== this.turnToken)
|
|
764
841
|
return;
|
|
765
842
|
if (plan) {
|
|
766
843
|
this.plan = plan;
|
|
844
|
+
if (currentTask())
|
|
845
|
+
currentTask().runtime.plan = plan;
|
|
767
846
|
// The plan is an ordinary assistant message: it is saved, folded, replayed
|
|
768
847
|
// and sent back to the model exactly like any other answer, so later turns
|
|
769
848
|
// (and the user) still know what was agreed.
|
|
@@ -783,7 +862,7 @@ export class ChatScreen extends Screen {
|
|
|
783
862
|
const finished = await this.agentLoop(this.buildApiMessages(this.turnView(turnMessages), directive), token, approvalContext);
|
|
784
863
|
if (token !== this.turnToken)
|
|
785
864
|
return;
|
|
786
|
-
this.setStep(i, finished ? "done" : "failed");
|
|
865
|
+
this.setStep(i, currentTask()?.signal.aborted ? "cancelled" : finished ? "done" : "failed");
|
|
787
866
|
// A step that failed stops the plan: the remaining steps assumed this one
|
|
788
867
|
// succeeded, and the user has to see the failure before anything runs on
|
|
789
868
|
// top of it.
|
|
@@ -833,6 +912,19 @@ export class ChatScreen extends Screen {
|
|
|
833
912
|
this.startTurn();
|
|
834
913
|
}
|
|
835
914
|
startTurn() {
|
|
915
|
+
if (this.taskRuntime?.settling) {
|
|
916
|
+
const previous = this.taskRuntime;
|
|
917
|
+
this.queued = lastRequestText(this.messages);
|
|
918
|
+
this.paused = false;
|
|
919
|
+
void previous.drain().then(() => {
|
|
920
|
+
if (this.taskRuntime === previous && this.queued && !this.paused) {
|
|
921
|
+
this.queued = null;
|
|
922
|
+
this.startTurn();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
this.publishLive();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
836
928
|
// Sending closes every earlier turn, including one left unfinished by
|
|
837
929
|
// ESC ESC: it is folded from here on, but never while it is still paused.
|
|
838
930
|
const starts = this.userMessageIndexes();
|
|
@@ -846,6 +938,9 @@ export class ChatScreen extends Screen {
|
|
|
846
938
|
this.busy = true;
|
|
847
939
|
this.paused = false;
|
|
848
940
|
this.app.state.isThinking = true;
|
|
941
|
+
this.activeAgentMode = this.agentMode;
|
|
942
|
+
this.turnAbort?.abort();
|
|
943
|
+
this.turnAbort = new AbortController();
|
|
849
944
|
const token = ++this.turnToken;
|
|
850
945
|
this.streamingContent = "";
|
|
851
946
|
this.app.render();
|
|
@@ -890,7 +985,9 @@ export class ChatScreen extends Screen {
|
|
|
890
985
|
buildApiMessages(messages = this.messages, directive = "") {
|
|
891
986
|
// A step directive extends the system prompt rather than adding a user
|
|
892
987
|
// message: the plan is an instruction, not something the user just said.
|
|
893
|
-
const
|
|
988
|
+
const prompt = loadAgentPrompt(this.activeAgentMode);
|
|
989
|
+
const system = (directive ? `${prompt}\n\n${directive}` : prompt) +
|
|
990
|
+
(currentTask() ? `\n\n${currentTask().runtime.instructions()}` : "");
|
|
894
991
|
const api = [{ role: "system", content: system }];
|
|
895
992
|
for (const m of modelHistory(messages, this.context)) {
|
|
896
993
|
if (m.role === "user" || m.role === "assistant") {
|
|
@@ -912,6 +1009,9 @@ export class ChatScreen extends Screen {
|
|
|
912
1009
|
* because a planned turn runs this once per step.
|
|
913
1010
|
*/
|
|
914
1011
|
async agentLoop(apiMessages, token, approvalContext) {
|
|
1012
|
+
const tools = this.activeAgentMode === "ptc"
|
|
1013
|
+
? [...TOOLS, (await import("../llm/codemode.js")).getCodeToolDefinition()] : TOOLS;
|
|
1014
|
+
let emptyReplies = 0;
|
|
915
1015
|
try {
|
|
916
1016
|
while (true) {
|
|
917
1017
|
if (token !== this.turnToken)
|
|
@@ -919,11 +1019,19 @@ export class ChatScreen extends Screen {
|
|
|
919
1019
|
// Keep the payload inside budget during a long single turn: once the
|
|
920
1020
|
// context is too large, drop the body of older tool results.
|
|
921
1021
|
apiMessages = this.trimApiMessages(apiMessages);
|
|
1022
|
+
if (currentTask())
|
|
1023
|
+
await prepareContext({
|
|
1024
|
+
messages: apiMessages, tools, capacity: modelContextWindow(this.modelProvider, this.model, "standard"),
|
|
1025
|
+
createClient: () => new LLMClient(this.model, this.modelProvider, this.intensity),
|
|
1026
|
+
stream: { signal: currentTask().runtime.signal, cancelled: () => token !== this.turnToken || this.paused },
|
|
1027
|
+
onUsage: usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; },
|
|
1028
|
+
});
|
|
922
1029
|
const client = new LLMClient(this.model, this.modelProvider, this.intensity);
|
|
923
1030
|
let content = "";
|
|
924
1031
|
let hasContent = false;
|
|
925
1032
|
let aborted = false;
|
|
926
|
-
for await (const chunk of client.streamTurn(apiMessages,
|
|
1033
|
+
for await (const chunk of client.streamTurn(apiMessages, tools, {
|
|
1034
|
+
maxOutputTokens: outputAllowance(modelContextWindow(this.modelProvider, this.model)),
|
|
927
1035
|
// A retried request can go quiet for up to 160s, so the status line
|
|
928
1036
|
// says why instead of looking hung. The moment the user pauses or a
|
|
929
1037
|
// newer turn takes over, the backoff is dropped instead of waited out.
|
|
@@ -968,8 +1076,42 @@ export class ChatScreen extends Screen {
|
|
|
968
1076
|
this.app.render();
|
|
969
1077
|
return false;
|
|
970
1078
|
}
|
|
1079
|
+
if (currentTask()?.signal.aborted || client.outcome?.status === "cancelled") {
|
|
1080
|
+
this.taskOutcome = "cancelled";
|
|
1081
|
+
this.streamingContent = null;
|
|
1082
|
+
this.messages.push({ role: "assistant", content: `[Task stopped: ${String(currentTask()?.signal.reason ?? "Task cancelled")}]\n${currentTask()?.runtime.partialReport() ?? ""}` });
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
if (client.outcome?.error?.code.endsWith("_budget") && currentTask()) {
|
|
1086
|
+
const report = await finalTaskReport(currentTask().runtime, () => new LLMClient(this.model, this.modelProvider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; }, apiMessages);
|
|
1087
|
+
if (token !== this.turnToken || this.paused)
|
|
1088
|
+
return false;
|
|
1089
|
+
this.taskOutcome = "partial";
|
|
1090
|
+
this.streamingContent = null;
|
|
1091
|
+
this.messages.push({ role: "assistant", content: report });
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
if (client.outcome?.status === "failed") {
|
|
1095
|
+
const recorded = currentTask()?.runtime.evidence.length ? currentTask().runtime.partialReport() : "";
|
|
1096
|
+
const message = client.outcome.error?.message ?? "Model request failed";
|
|
1097
|
+
this.taskOutcome = recorded ? "partial" : "failed";
|
|
1098
|
+
this.streamingContent = null;
|
|
1099
|
+
this.messages.push({ role: "assistant", content: content +
|
|
1100
|
+
(content.includes(message) ? "" : `\n[Error: ${message}]`) + (recorded ? `\n\n${recorded}` : "") });
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
971
1103
|
const toolCalls = client.toolCalls;
|
|
972
1104
|
if (toolCalls.length === 0) {
|
|
1105
|
+
if (!content.trim() && emptyReplies++ === 0)
|
|
1106
|
+
continue;
|
|
1107
|
+
if (!content.trim()) {
|
|
1108
|
+
this.taskOutcome = "failed";
|
|
1109
|
+
if (!content.trim())
|
|
1110
|
+
this.messages.push({ role: "assistant", content: "[Error: model returned an empty completion.]" });
|
|
1111
|
+
else
|
|
1112
|
+
this.messages.push({ role: "assistant", content });
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
973
1115
|
if (hasContent)
|
|
974
1116
|
this.messages.push({ role: "assistant", content });
|
|
975
1117
|
this.streamingContent = null;
|
|
@@ -1026,7 +1168,15 @@ export class ChatScreen extends Screen {
|
|
|
1026
1168
|
this.app.render();
|
|
1027
1169
|
continue;
|
|
1028
1170
|
}
|
|
1029
|
-
|
|
1171
|
+
let result;
|
|
1172
|
+
try {
|
|
1173
|
+
result = await this.runTool(tc.name, args, toolMsg, token, apiMessages, approvalContext);
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
toolMsg.result = `[Not executed: ${describeError(error)}]`;
|
|
1177
|
+
this.save();
|
|
1178
|
+
throw error;
|
|
1179
|
+
}
|
|
1030
1180
|
// A clear starts a different transcript while an old tool may finish.
|
|
1031
1181
|
if (!this.messages.includes(toolMsg))
|
|
1032
1182
|
return false;
|
|
@@ -1044,6 +1194,16 @@ export class ChatScreen extends Screen {
|
|
|
1044
1194
|
catch (e) {
|
|
1045
1195
|
if (token !== this.turnToken)
|
|
1046
1196
|
return false;
|
|
1197
|
+
if (e instanceof TaskControlError && e.code.endsWith("_budget") && currentTask()) {
|
|
1198
|
+
const report = await finalTaskReport(currentTask().runtime, () => new LLMClient(this.model, this.modelProvider, this.intensity), usage => { this.tokens.prompt += usage.prompt; this.tokens.completion += usage.completion; }, apiMessages);
|
|
1199
|
+
if (token !== this.turnToken || this.paused)
|
|
1200
|
+
return false;
|
|
1201
|
+
this.taskOutcome = "partial";
|
|
1202
|
+
this.streamingContent = null;
|
|
1203
|
+
this.messages.push({ role: "assistant", content: report });
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
this.taskOutcome = "failed";
|
|
1047
1207
|
const err = describeError(e);
|
|
1048
1208
|
this.messages.push({ role: "assistant", content: (this.streamingContent ?? "") + `\n[Error: ${err}]` });
|
|
1049
1209
|
this.streamingContent = null;
|
|
@@ -1075,18 +1235,34 @@ export class ChatScreen extends Screen {
|
|
|
1075
1235
|
}
|
|
1076
1236
|
// ---- approval gate ---------------------------------------------------
|
|
1077
1237
|
/** Classify a tool call and, when it is risky, run it past the gate. */
|
|
1078
|
-
async requestApproval(tool, args, context = captureApprovalContext(this.messages)) {
|
|
1238
|
+
async requestApproval(tool, args, context = captureApprovalContext(this.messages), signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
1239
|
+
const linked = linkSignals(signal, currentTask()?.signal);
|
|
1240
|
+
signal = linked.signal;
|
|
1241
|
+
try {
|
|
1242
|
+
currentTask()?.runtime.assertTool(tool, args);
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
linked.dispose();
|
|
1246
|
+
return { approved: false, source: "input", reason: error instanceof Error ? error.message : String(error) };
|
|
1247
|
+
}
|
|
1079
1248
|
const risk = assessToolCall(tool, args, this.liveDir);
|
|
1080
|
-
if (!risk)
|
|
1249
|
+
if (!risk) {
|
|
1250
|
+
linked.dispose();
|
|
1081
1251
|
return { approved: true, source: "prompt", reason: "routine call" };
|
|
1252
|
+
}
|
|
1253
|
+
const approvalToken = this.turnToken;
|
|
1254
|
+
const cancelled = () => approvalToken !== this.turnToken || this.paused || signal?.aborted === true;
|
|
1082
1255
|
const gate = new ApprovalGate({
|
|
1083
|
-
|
|
1256
|
+
cancelled,
|
|
1257
|
+
askHuman: () => cancelled() ? Promise.resolve(false) : this.askHuman(tool, args, risk),
|
|
1084
1258
|
reviewer: () => this.reviewerModel(),
|
|
1085
1259
|
autoApprovalEnabled: () => this.approvalMode === "auto",
|
|
1086
1260
|
extremeAutoApprovalEnabled: () => loadConfig().auto_approve_extreme_risk === true,
|
|
1087
1261
|
// Reviews happen before any card is shown, so without this the app looks
|
|
1088
1262
|
// frozen while the independent assessments run.
|
|
1089
1263
|
onReview: (info) => {
|
|
1264
|
+
if (cancelled())
|
|
1265
|
+
return;
|
|
1090
1266
|
this.reviewing = info ? {
|
|
1091
1267
|
total: info.total, round: info.round ?? 1, rounds: info.rounds ?? 1,
|
|
1092
1268
|
request: liveApprovalDetails(tool, args, risk, this.liveDir),
|
|
@@ -1103,7 +1279,18 @@ export class ChatScreen extends Screen {
|
|
|
1103
1279
|
this.app.showToast(`independent review running (${which}${info.total} reviewer${info.total > 1 ? "s" : ""})...`);
|
|
1104
1280
|
},
|
|
1105
1281
|
});
|
|
1106
|
-
|
|
1282
|
+
const abortApproval = () => {
|
|
1283
|
+
if (this.pendingApproval?.args === args)
|
|
1284
|
+
this.settleApproval(false);
|
|
1285
|
+
};
|
|
1286
|
+
signal.addEventListener("abort", abortApproval, { once: true });
|
|
1287
|
+
try {
|
|
1288
|
+
return await gate.decide({ tool, args, cwd: this.liveDir, ...context, risk });
|
|
1289
|
+
}
|
|
1290
|
+
finally {
|
|
1291
|
+
signal.removeEventListener("abort", abortApproval);
|
|
1292
|
+
linked.dispose();
|
|
1293
|
+
}
|
|
1107
1294
|
}
|
|
1108
1295
|
/** Park the turn until the user answers; the header turns orange meanwhile. */
|
|
1109
1296
|
askHuman(tool, args, risk) {
|
|
@@ -1193,15 +1380,97 @@ export class ChatScreen extends Screen {
|
|
|
1193
1380
|
};
|
|
1194
1381
|
}
|
|
1195
1382
|
// ---- sub-agents ------------------------------------------------------
|
|
1383
|
+
async runProgram(args, token, apiMessages, approvalContext) {
|
|
1384
|
+
if (this.activeAgentMode !== "ptc")
|
|
1385
|
+
return "[Error: execute_ts requires PTC mode]";
|
|
1386
|
+
const { executeCodeProgram, ProgramToolDenied } = await import("../llm/codemode.js");
|
|
1387
|
+
const signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal;
|
|
1388
|
+
const cancelled = () => token !== this.turnToken || this.paused || signal?.aborted;
|
|
1389
|
+
const result = await executeCodeProgram(args, {
|
|
1390
|
+
signal,
|
|
1391
|
+
onPlan: plan => {
|
|
1392
|
+
if (cancelled())
|
|
1393
|
+
return;
|
|
1394
|
+
this.plan = plan;
|
|
1395
|
+
if (currentTask())
|
|
1396
|
+
currentTask().runtime.plan = plan;
|
|
1397
|
+
this.save();
|
|
1398
|
+
this.publishLive();
|
|
1399
|
+
this.app.render();
|
|
1400
|
+
},
|
|
1401
|
+
onStep: (index, status) => { if (!cancelled())
|
|
1402
|
+
this.setStep(index, status); },
|
|
1403
|
+
call: async (name, callArgs, programSignal) => {
|
|
1404
|
+
if (cancelled() || programSignal?.aborted)
|
|
1405
|
+
throw new Error("Execution cancelled");
|
|
1406
|
+
const message = { role: "tool", name, arguments: callArgs, result: "…" };
|
|
1407
|
+
this.messages.push(message);
|
|
1408
|
+
this.save();
|
|
1409
|
+
this.publishLive();
|
|
1410
|
+
this.app.render();
|
|
1411
|
+
const stopApproval = () => {
|
|
1412
|
+
if (this.pendingApproval?.args !== callArgs)
|
|
1413
|
+
return;
|
|
1414
|
+
this.settleApproval(false);
|
|
1415
|
+
};
|
|
1416
|
+
programSignal?.addEventListener("abort", stopApproval, { once: true });
|
|
1417
|
+
try {
|
|
1418
|
+
const decision = await this.requestApproval(name, callArgs, {
|
|
1419
|
+
...approvalContext, context: captureApprovalContext(apiMessages).context,
|
|
1420
|
+
}, programSignal);
|
|
1421
|
+
if (cancelled() || programSignal?.aborted)
|
|
1422
|
+
throw new Error("Execution cancelled");
|
|
1423
|
+
if (!decision.approved) {
|
|
1424
|
+
message.result = `[Not executed: ${decision.reason}]`;
|
|
1425
|
+
throw new ProgramToolDenied(`Approval denied: ${decision.reason}. Do not retry or work around this denial.`);
|
|
1426
|
+
}
|
|
1427
|
+
const value = await this.runTool(name, callArgs, message, token, apiMessages, approvalContext, programSignal);
|
|
1428
|
+
message.result = value;
|
|
1429
|
+
if (cancelled() || programSignal?.aborted)
|
|
1430
|
+
throw new Error("Execution cancelled");
|
|
1431
|
+
return value;
|
|
1432
|
+
}
|
|
1433
|
+
catch (error) {
|
|
1434
|
+
if (message.result === "…")
|
|
1435
|
+
message.result = `[Error: ${error instanceof Error ? error.message : String(error)}]`;
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
finally {
|
|
1439
|
+
programSignal?.removeEventListener("abort", stopApproval);
|
|
1440
|
+
if (this.messages.includes(message)) {
|
|
1441
|
+
this.save();
|
|
1442
|
+
this.publishLive();
|
|
1443
|
+
this.app.render();
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
if (!cancelled()) {
|
|
1449
|
+
this.save();
|
|
1450
|
+
this.publishLive();
|
|
1451
|
+
this.app.render();
|
|
1452
|
+
}
|
|
1453
|
+
return result;
|
|
1454
|
+
}
|
|
1196
1455
|
/**
|
|
1197
1456
|
* Run one approved tool call. `delegate` is handled here rather than inside
|
|
1198
1457
|
* tools.ts: a sub-agent has to share this turn's model, approval gate, working
|
|
1199
1458
|
* directory and abort signal, and only the session knows about those.
|
|
1200
1459
|
*/
|
|
1201
|
-
async runTool(name, args, toolMsg, token, apiMessages, approvalContext) {
|
|
1460
|
+
async runTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
1461
|
+
const runtime = currentTask()?.runtime;
|
|
1462
|
+
const execute = () => this.dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, executionSignal);
|
|
1463
|
+
if (!runtime)
|
|
1464
|
+
return execute();
|
|
1465
|
+
const result = await runtime.tool(name, args, execute);
|
|
1466
|
+
return result;
|
|
1467
|
+
}
|
|
1468
|
+
async dispatchTool(name, args, toolMsg, token, apiMessages, approvalContext, signal = currentTask()?.runtime.signal ?? this.turnAbort?.signal) {
|
|
1469
|
+
if (name === "execute_ts")
|
|
1470
|
+
return this.runProgram(args, token, apiMessages, approvalContext);
|
|
1202
1471
|
if (name !== DELEGATE_TOOL)
|
|
1203
|
-
return executeTool(name, args, this.liveDir);
|
|
1204
|
-
return this.delegate(args, toolMsg, token, apiMessages, approvalContext);
|
|
1472
|
+
return executeTool(name, args, this.liveDir, signal);
|
|
1473
|
+
return this.delegate(args, toolMsg, token, apiMessages, approvalContext, signal);
|
|
1205
1474
|
}
|
|
1206
1475
|
/**
|
|
1207
1476
|
* Run a sub-agent for one `delegate` call and return its report.
|
|
@@ -1211,22 +1480,33 @@ export class ChatScreen extends Screen {
|
|
|
1211
1480
|
* part of this transcript. While it runs, its current phase is shown in the
|
|
1212
1481
|
* status line and in the tool block, so a quiet log still has an explanation.
|
|
1213
1482
|
*/
|
|
1214
|
-
async delegate(args, toolMsg, token, apiMessages, approvalContext) {
|
|
1483
|
+
async delegate(args, toolMsg, token, apiMessages, approvalContext, executionSignal) {
|
|
1215
1484
|
const task = String(args.task ?? "").trim();
|
|
1216
1485
|
if (!task)
|
|
1217
1486
|
return "[Error: delegate needs a task: what should the sub-agent do?]";
|
|
1218
1487
|
const context = typeof args.context === "string" ? args.context.trim() : "";
|
|
1219
1488
|
const approval = { ...approvalContext, context: captureApprovalContext(apiMessages).context };
|
|
1489
|
+
const maxRounds = subAgentRounds(loadConfig().subagent_max_rounds);
|
|
1490
|
+
const runtime = currentTask()?.runtime;
|
|
1491
|
+
const signal = executionSignal ?? runtime?.signal ?? this.turnAbort?.signal;
|
|
1492
|
+
const stopped = () => token !== this.turnToken || this.paused || signal?.aborted === true;
|
|
1220
1493
|
const run = await runSubAgentTask({
|
|
1494
|
+
runtime, signal,
|
|
1495
|
+
readOnly: args.read_only === true,
|
|
1496
|
+
writePaths: Array.isArray(args.write_paths) ? args.write_paths.filter((p) => typeof p === "string") : undefined,
|
|
1497
|
+
contextWindow: modelContextWindow(this.modelProvider, this.model, "standard"),
|
|
1221
1498
|
createClient: () => new LLMClient(this.model, this.modelProvider, this.intensity),
|
|
1222
1499
|
system: loadSubAgentPrompt(),
|
|
1223
|
-
task: buildSubAgentTask({ task, context, cwd: this.liveDir }),
|
|
1500
|
+
task: buildSubAgentTask({ task, context, cwd: this.liveDir, rounds: maxRounds }),
|
|
1224
1501
|
tools: SUB_AGENT_TOOLS,
|
|
1225
|
-
|
|
1502
|
+
maxRounds,
|
|
1503
|
+
approve: (name, callArgs) => this.requestApproval(name, callArgs, approval, signal),
|
|
1226
1504
|
execute: (name, callArgs) => name === DELEGATE_TOOL
|
|
1227
1505
|
? Promise.resolve("[Error: sub-agents cannot delegate further]")
|
|
1228
|
-
: executeTool(name, callArgs, this.liveDir),
|
|
1506
|
+
: executeTool(name, callArgs, this.liveDir, signal),
|
|
1229
1507
|
onProgress: (label) => {
|
|
1508
|
+
if (stopped())
|
|
1509
|
+
return;
|
|
1230
1510
|
this.subagent = label;
|
|
1231
1511
|
// The tool block is what a reader of the transcript sees while the child
|
|
1232
1512
|
// works; the status line says the same thing at the top of the screen.
|
|
@@ -1235,24 +1515,30 @@ export class ChatScreen extends Screen {
|
|
|
1235
1515
|
this.publishLive();
|
|
1236
1516
|
},
|
|
1237
1517
|
onUsage: (usage) => {
|
|
1518
|
+
if (stopped())
|
|
1519
|
+
return;
|
|
1238
1520
|
this.tokens.prompt += usage.prompt;
|
|
1239
1521
|
this.tokens.completion += usage.completion;
|
|
1240
1522
|
},
|
|
1241
1523
|
onRetry: (notice) => {
|
|
1524
|
+
if (stopped())
|
|
1525
|
+
return;
|
|
1242
1526
|
this.retryNotice =
|
|
1243
1527
|
`Sub-agent — network hiccup, retrying in ${formatDelay(notice.delayMs)} ` +
|
|
1244
1528
|
`(attempt ${notice.attempt}/${notice.attempts})`;
|
|
1245
1529
|
this.app.render();
|
|
1246
1530
|
},
|
|
1247
|
-
cancelled: () => token !== this.turnToken || this.paused,
|
|
1531
|
+
cancelled: () => token !== this.turnToken || this.paused || signal?.aborted === true,
|
|
1248
1532
|
});
|
|
1249
1533
|
// The final report replaces the live marker, and the status line goes back
|
|
1250
1534
|
// to the parent's own state.
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1535
|
+
if (!stopped()) {
|
|
1536
|
+
this.subagent = null;
|
|
1537
|
+
this.retryNotice = null;
|
|
1538
|
+
toolMsg.result = "";
|
|
1539
|
+
this.app.render();
|
|
1540
|
+
this.publishLive();
|
|
1541
|
+
}
|
|
1256
1542
|
return formatSubAgentResult(run);
|
|
1257
1543
|
}
|
|
1258
1544
|
// ---- turn folding ----------------------------------------------------
|
|
@@ -1276,7 +1562,7 @@ export class ChatScreen extends Screen {
|
|
|
1276
1562
|
/**
|
|
1277
1563
|
* A finished turn is only worth folding when the work it hides is substantial.
|
|
1278
1564
|
* Whether it is folded right now is a separate question (`expandedTurns`), and
|
|
1279
|
-
* its reply
|
|
1565
|
+
* its user input and reply are never part of what it hides.
|
|
1280
1566
|
*/
|
|
1281
1567
|
turnHides(start, end) {
|
|
1282
1568
|
if (!this.finishedTurns.has(start))
|
|
@@ -1285,12 +1571,12 @@ export class ChatScreen extends Screen {
|
|
|
1285
1571
|
let blocks = 0;
|
|
1286
1572
|
let chars = 0;
|
|
1287
1573
|
for (let i = start; i < end; i++) {
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1574
|
+
// User input and the reply stay on screen, so neither counts toward the
|
|
1575
|
+
// folding threshold, however long either one is.
|
|
1290
1576
|
if (i === answer)
|
|
1291
1577
|
continue;
|
|
1292
1578
|
const m = this.messages[i];
|
|
1293
|
-
if (m.role === "
|
|
1579
|
+
if (m.role === "assistant") {
|
|
1294
1580
|
blocks++;
|
|
1295
1581
|
chars += m.content.length;
|
|
1296
1582
|
}
|
|
@@ -1345,23 +1631,6 @@ export class ChatScreen extends Screen {
|
|
|
1345
1631
|
}
|
|
1346
1632
|
return map;
|
|
1347
1633
|
}
|
|
1348
|
-
/**
|
|
1349
|
-
* The first line of the first message in `role`. Only one line: the header is
|
|
1350
|
-
* a signpost, and the full text is one click away.
|
|
1351
|
-
*/
|
|
1352
|
-
turnPreview(start, end, role) {
|
|
1353
|
-
for (const m of this.messages.slice(start, end)) {
|
|
1354
|
-
if (m.role !== role)
|
|
1355
|
-
continue;
|
|
1356
|
-
const line = sanitizeText(m.content)
|
|
1357
|
-
.split("\n")
|
|
1358
|
-
.map((part) => part.trim())
|
|
1359
|
-
.find((part) => part.length > 0);
|
|
1360
|
-
if (line)
|
|
1361
|
-
return line;
|
|
1362
|
-
}
|
|
1363
|
-
return "";
|
|
1364
|
-
}
|
|
1365
1634
|
/** The header standing in for a folded turn, or the way back into one. */
|
|
1366
1635
|
pushTurnHeader(start, end, width, add) {
|
|
1367
1636
|
const count = end - start;
|
|
@@ -1378,13 +1647,10 @@ export class ChatScreen extends Screen {
|
|
|
1378
1647
|
}, -1, start);
|
|
1379
1648
|
return;
|
|
1380
1649
|
}
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
// The prompt and the reply sit either side of the work this fold hides, so
|
|
1384
|
-
// the control line goes between them and the answer stays on screen below.
|
|
1385
|
-
const hidden = this.turnAnswer(start, end) < 0 ? count : count - 1;
|
|
1650
|
+
const answer = this.turnAnswer(start, end);
|
|
1651
|
+
const hidden = this.messages.slice(start, end).filter((message, offset) => message.role !== "user" && message.role !== "system" && start + offset !== answer).length;
|
|
1386
1652
|
const meta = `${hidden} of ${count} message${count === 1 ? "" : "s"} folded`;
|
|
1387
|
-
add({ spans: [{ text: ellipsize(
|
|
1653
|
+
add({ spans: [{ text: ellipsize(`▸ ${meta} — click to expand`, width), style: { fg: C.dim } }] }, -1, start);
|
|
1388
1654
|
}
|
|
1389
1655
|
/** Fold or unfold one turn. */
|
|
1390
1656
|
toggleTurn(start) {
|
|
@@ -1525,7 +1791,7 @@ export class ChatScreen extends Screen {
|
|
|
1525
1791
|
if (chatMessages.length === 0)
|
|
1526
1792
|
return true;
|
|
1527
1793
|
try {
|
|
1528
|
-
this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan);
|
|
1794
|
+
this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, this.liveDir, this.sessionId ?? undefined, this.tokens, this.context, this.approvalMode, this.plan, "standard", undefined, this.agentMode, this.taskRuntime?.snapshot() ?? this.lastTask);
|
|
1529
1795
|
this.saveError = null;
|
|
1530
1796
|
return true;
|
|
1531
1797
|
}
|
|
@@ -1577,26 +1843,24 @@ export class ChatScreen extends Screen {
|
|
|
1577
1843
|
archived = context !== null && recordIndex < context.end;
|
|
1578
1844
|
recordIndex++;
|
|
1579
1845
|
}
|
|
1580
|
-
// A foldable turn always shows its control lines first; its work then
|
|
1581
|
-
// renders when the reader has opened it, and its reply always does.
|
|
1582
|
-
if (m.role !== "system" && turn === mi && !(archived && !this.expandedContext)) {
|
|
1583
|
-
this.pushTurnHeader(mi, this.turnEnd(mi), width, add);
|
|
1584
|
-
}
|
|
1585
1846
|
const folded = turn >= 0 && !this.expandedTurns.has(turn);
|
|
1586
1847
|
const reply = folded && answers[mi] === mi;
|
|
1587
1848
|
if (m.role === "system") {
|
|
1588
1849
|
pushText(sanitizeText(m.content), { fg: C.dim });
|
|
1589
1850
|
}
|
|
1590
|
-
else if ((archived && !this.expandedContext) || (folded && !reply)) {
|
|
1591
|
-
// Hidden: by the collapsed "Earlier conversation" fold, or by the turn's
|
|
1592
|
-
// own fold, which hides the prompt body and the work but never the
|
|
1593
|
-
// reply. Either way the message stays in the transcript, on disk and in
|
|
1594
|
-
// the request; the header above says how to get it back on screen.
|
|
1595
|
-
}
|
|
1596
1851
|
else if (m.role === "user") {
|
|
1597
1852
|
add({ spans: [{ text: "You:", style: { fg: C.accent, bold: true } }] });
|
|
1598
1853
|
pushMarkdown(sanitizeText(m.content), { fg: C.user });
|
|
1599
1854
|
add({ spans: [] });
|
|
1855
|
+
// The fold control follows the complete prompt, which stays visible
|
|
1856
|
+
// even when the turn or the earlier conversation is collapsed.
|
|
1857
|
+
if (turn === mi && !(archived && !this.expandedContext)) {
|
|
1858
|
+
this.pushTurnHeader(mi, this.turnEnd(mi), width, add);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
else if ((archived && !this.expandedContext) || (folded && !reply)) {
|
|
1862
|
+
// Only AI output is hidden. Every message remains in the transcript,
|
|
1863
|
+
// on disk and in the request; the controls reveal it again.
|
|
1600
1864
|
}
|
|
1601
1865
|
else if (m.role === "assistant") {
|
|
1602
1866
|
add({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
|
|
@@ -1621,7 +1885,7 @@ export class ChatScreen extends Screen {
|
|
|
1621
1885
|
add({ spans: [{ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } }] }, mi);
|
|
1622
1886
|
// The line under the tool name is the call's own material: a command, a
|
|
1623
1887
|
// path, or — for `delegate` — the task handed to the sub-agent.
|
|
1624
|
-
const argText = sanitizeText(m.arguments.command ?? m.arguments.path ?? m.arguments.task ?? "", { singleLine: true });
|
|
1888
|
+
const argText = sanitizeText(m.arguments.command ?? m.arguments.path ?? m.arguments.query ?? m.arguments.task ?? "", { singleLine: true });
|
|
1625
1889
|
if (argText)
|
|
1626
1890
|
add({ spans: [{ text: " " + argText, style: { fg: C.dim } }] }, mi);
|
|
1627
1891
|
const decision = this.approvals.get(m);
|
|
@@ -1650,7 +1914,7 @@ export class ChatScreen extends Screen {
|
|
|
1650
1914
|
}
|
|
1651
1915
|
add({ spans: [] });
|
|
1652
1916
|
}
|
|
1653
|
-
if (context && archived && recordIndex === context.end) {
|
|
1917
|
+
if (context && archived && recordIndex === context.end && this.expandedContext) {
|
|
1654
1918
|
this.contextLines.add(lines.length);
|
|
1655
1919
|
add({ spans: [{ text: "▾ Collapse earlier conversation", style: { fg: C.amber } }] });
|
|
1656
1920
|
add({ spans: [] });
|
|
@@ -1674,6 +1938,10 @@ export class ChatScreen extends Screen {
|
|
|
1674
1938
|
status = `History save failed: ${sanitizeText(this.saveError, { singleLine: true })}`;
|
|
1675
1939
|
statusStyle = { fg: C.orange, bold: true };
|
|
1676
1940
|
}
|
|
1941
|
+
else if (this.taskRuntime?.snapshot().state === "cancelling") {
|
|
1942
|
+
status = "Stopping tasks…";
|
|
1943
|
+
statusStyle = { fg: C.amber, bold: true };
|
|
1944
|
+
}
|
|
1677
1945
|
else if (this.compressing) {
|
|
1678
1946
|
status = "Compressing context…";
|
|
1679
1947
|
statusStyle = { fg: C.amber, bold: true };
|