@kernelonpanic/kitcode 1.4.5 → 1.4.6

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/dist/index.js CHANGED
@@ -69,6 +69,7 @@ var diagnosticsSchema = z.object({
69
69
  autoRun: z.boolean().default(true),
70
70
  commands: z.array(z.string().trim().min(1).max(2e4)).max(8).default([])
71
71
  });
72
+ var MAX_SYSTEM_PROMPT_CHARS = 2e4;
72
73
  var configSchema = z.object({
73
74
  version: z.literal(1).default(1),
74
75
  model: z.string().optional(),
@@ -82,7 +83,7 @@ var configSchema = z.object({
82
83
  // comes from the provider catalogue or the endpoint's model list.
83
84
  contextWindow: z.number().int().min(1e3).max(1e7).optional(),
84
85
  // Extra instructions appended to the built-in system prompt on every turn.
85
- systemPrompt: z.string().max(2e4).optional(),
86
+ systemPrompt: z.string().max(MAX_SYSTEM_PROMPT_CHARS).optional(),
86
87
  // When false, systemPrompt is kept in the config but not sent to the model.
87
88
  systemPromptEnabled: z.boolean().default(true),
88
89
  // Hard cap on model calls in one turn. Higher values let large refactors
@@ -1276,8 +1277,40 @@ function isSubsequence(needle, haystack) {
1276
1277
  return needle.length === 0;
1277
1278
  }
1278
1279
 
1279
- // src/tools/jobs.ts
1280
+ // src/tools/kill-tree.ts
1280
1281
  import { spawn } from "child_process";
1282
+ function killTree(child) {
1283
+ if (child.pid == null) return;
1284
+ const fallback = () => {
1285
+ try {
1286
+ child.kill("SIGKILL");
1287
+ } catch {
1288
+ }
1289
+ };
1290
+ if (process.platform === "win32") {
1291
+ try {
1292
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
1293
+ stdio: "ignore",
1294
+ windowsHide: true
1295
+ });
1296
+ killer.once("error", fallback);
1297
+ killer.once("close", (code) => {
1298
+ if (code !== 0) fallback();
1299
+ });
1300
+ } catch {
1301
+ fallback();
1302
+ }
1303
+ return;
1304
+ }
1305
+ try {
1306
+ process.kill(-child.pid, "SIGKILL");
1307
+ } catch {
1308
+ fallback();
1309
+ }
1310
+ }
1311
+
1312
+ // src/tools/jobs.ts
1313
+ import { spawn as spawn2 } from "child_process";
1281
1314
  import { StringDecoder } from "string_decoder";
1282
1315
  var MAX_JOBS = 8;
1283
1316
  var MAX_JOB_OUTPUT = 1e5;
@@ -1293,7 +1326,7 @@ function startJob(command, cwd) {
1293
1326
  installExitCleanup();
1294
1327
  counter += 1;
1295
1328
  const id = `bg-${counter}`;
1296
- const child = spawn(command, {
1329
+ const child = spawn2(command, {
1297
1330
  shell: true,
1298
1331
  cwd,
1299
1332
  detached: process.platform !== "win32",
@@ -1399,22 +1432,6 @@ function summary(job) {
1399
1432
  signal: job.signal
1400
1433
  };
1401
1434
  }
1402
- function killTree(child) {
1403
- if (child.pid == null) return;
1404
- if (process.platform === "win32") {
1405
- try {
1406
- spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
1407
- } catch {
1408
- child.kill("SIGKILL");
1409
- }
1410
- return;
1411
- }
1412
- try {
1413
- process.kill(-child.pid, "SIGKILL");
1414
- } catch {
1415
- child.kill("SIGKILL");
1416
- }
1417
- }
1418
1435
 
1419
1436
  // src/core/memory.ts
1420
1437
  import { createHash } from "crypto";
@@ -1892,10 +1909,115 @@ function validSessionId(id) {
1892
1909
  return /^[A-Za-z0-9_-]{1,240}$/.test(id);
1893
1910
  }
1894
1911
 
1912
+ // src/core/retry.ts
1913
+ var MAX_RETRIES2 = 9;
1914
+ var INITIAL_BACKOFF_MS = 1e3;
1915
+ async function* streamWithRetry(provider, request, signal) {
1916
+ request = { ...request, signal };
1917
+ let lastError;
1918
+ for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
1919
+ if (signal.aborted) return;
1920
+ let emittedOutput = false;
1921
+ try {
1922
+ const stream = provider.stream(request)[Symbol.asyncIterator]();
1923
+ let cancel;
1924
+ let cancelTimer;
1925
+ const cancelled = new Promise((resolve3) => {
1926
+ cancel = () => {
1927
+ cancelTimer ??= setTimeout(() => resolve3({ done: true, value: void 0 }), 100);
1928
+ };
1929
+ signal.addEventListener("abort", cancel, { once: true });
1930
+ if (signal.aborted) cancel();
1931
+ });
1932
+ try {
1933
+ for (; ; ) {
1934
+ const next = await Promise.race([stream.next(), cancelled]);
1935
+ if (next.done) break;
1936
+ if (!signal.aborted || next.value.type === "usage" || next.value.type === "done") {
1937
+ if (next.value.type !== "rate_limits" && next.value.type !== "retrying") emittedOutput = true;
1938
+ yield next.value;
1939
+ }
1940
+ if (next.value.type === "done") break;
1941
+ }
1942
+ } finally {
1943
+ signal.removeEventListener("abort", cancel);
1944
+ if (cancelTimer !== void 0) clearTimeout(cancelTimer);
1945
+ void stream.return?.().catch(() => void 0);
1946
+ }
1947
+ return;
1948
+ } catch (error) {
1949
+ if (signal.aborted) return;
1950
+ if (emittedOutput) throw error;
1951
+ lastError = error;
1952
+ if (attempt >= MAX_RETRIES2) break;
1953
+ const waitMs = retryBackoffMs(error, attempt);
1954
+ if (waitMs > 0) {
1955
+ yield {
1956
+ type: "retrying",
1957
+ attempt: attempt + 2,
1958
+ waitMs,
1959
+ reason: error instanceof Error ? error.message : String(error)
1960
+ };
1961
+ try {
1962
+ await sleep(waitMs, signal);
1963
+ } catch (error2) {
1964
+ if (signal.aborted) return;
1965
+ throw error2;
1966
+ }
1967
+ continue;
1968
+ }
1969
+ break;
1970
+ }
1971
+ }
1972
+ if (lastError) throw lastError;
1973
+ }
1974
+ function retryBackoffMs(error, attempt) {
1975
+ if (!isRateLimitError(error)) return 0;
1976
+ const serverWait = retryAfterFromError(error);
1977
+ if (serverWait > 0) return Math.min(serverWait, 6e4);
1978
+ return Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, 3e4);
1979
+ }
1980
+ function isRateLimitError(error) {
1981
+ if (!(error instanceof ProviderError)) return false;
1982
+ return error.status === 429;
1983
+ }
1984
+ function retryAfterFromError(error) {
1985
+ const cause = error?.cause;
1986
+ const headers = cause?.headers;
1987
+ if (headers && typeof headers.get === "function") {
1988
+ const ms = Number(headers.get("retry-after-ms"));
1989
+ if (Number.isFinite(ms) && ms > 0) return ms;
1990
+ const raw = headers.get("retry-after");
1991
+ if (raw) {
1992
+ const s = Number(raw);
1993
+ if (Number.isFinite(s) && s > 0) return s * 1e3;
1994
+ const date = Date.parse(raw);
1995
+ if (!Number.isFinite(s) && Number.isFinite(date)) return Math.max(0, date - Date.now());
1996
+ }
1997
+ }
1998
+ return 0;
1999
+ }
2000
+ function sleep(ms, signal) {
2001
+ return new Promise((resolve3, reject) => {
2002
+ if (signal.aborted) {
2003
+ reject(signal.reason ?? new Error("Aborted"));
2004
+ return;
2005
+ }
2006
+ const finish = () => {
2007
+ signal.removeEventListener("abort", onAbort);
2008
+ resolve3();
2009
+ };
2010
+ const onAbort = () => {
2011
+ clearTimeout(id);
2012
+ reject(signal.reason ?? new Error("Aborted"));
2013
+ };
2014
+ const id = setTimeout(finish, ms);
2015
+ signal.addEventListener("abort", onAbort, { once: true });
2016
+ });
2017
+ }
2018
+
1895
2019
  // src/core/agent.ts
1896
2020
  var MAX_PAUSE_RESUMES = 5;
1897
- var MAX_RETRIES2 = 3;
1898
- var INITIAL_BACKOFF_MS = 1e3;
1899
2021
  var DEFAULT_MAX_TURN_STEPS = 256;
1900
2022
  function createExecutionLane() {
1901
2023
  let tail2 = Promise.resolve();
@@ -1946,6 +2068,7 @@ async function runTurn(cfg, history, hooks, signal) {
1946
2068
  }
1947
2069
  }
1948
2070
  const request = async () => {
2071
+ hooks.onHistory?.(sanitizeHistory(messages));
1949
2072
  if (signal.aborted) return { content: [], stopReason: "aborted" };
1950
2073
  const budgetDecision = cfg.budget?.beforeRequest({
1951
2074
  modelRef: cfg.modelRef,
@@ -1969,6 +2092,7 @@ async function runTurn(cfg, history, hooks, signal) {
1969
2092
  const outcome = await (cfg.modelRequestLane?.run(request) ?? request());
1970
2093
  if (!outcome) return messages;
1971
2094
  messages.push({ role: "assistant", content: outcome.content });
2095
+ hooks.onHistory?.(sanitizeHistory(messages));
1972
2096
  if (outcome.stopReason === "aborted") {
1973
2097
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1974
2098
  return sanitizeHistory(messages);
@@ -1976,6 +2100,7 @@ async function runTurn(cfg, history, hooks, signal) {
1976
2100
  if (outcome.stopReason === "tool_use") {
1977
2101
  const results = await runToolCalls(cfg, outcome.content, hooks, signal);
1978
2102
  if (results.length > 0) messages.push({ role: "user", content: results });
2103
+ hooks.onHistory?.(sanitizeHistory(messages));
1979
2104
  if (signal.aborted) {
1980
2105
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1981
2106
  return sanitizeHistory(messages);
@@ -2022,7 +2147,7 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2022
2147
  let outputTokens = 0;
2023
2148
  const startedAt = Date.now();
2024
2149
  let firstTokenAt;
2025
- for await (const event of streamWithRetry(cfg.provider, request, cfg.modelRef, signal)) {
2150
+ for await (const event of streamWithRetry(cfg.provider, request, signal)) {
2026
2151
  switch (event.type) {
2027
2152
  case "text_delta":
2028
2153
  firstTokenAt ??= Date.now();
@@ -2044,6 +2169,13 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2044
2169
  case "rate_limits":
2045
2170
  hooks.onEvent({ type: "rate_limits", model: cfg.modelRef, limits: event.limits });
2046
2171
  break;
2172
+ case "retrying":
2173
+ hooks.onEvent({
2174
+ type: "notice",
2175
+ level: "warn",
2176
+ text: `Rate limited; retrying the same request in ${Math.ceil(event.waitMs / 1e3)}s (attempt ${event.attempt}). The conversation keeps its progress.`
2177
+ });
2178
+ break;
2047
2179
  case "tool_call":
2048
2180
  break;
2049
2181
  case "done":
@@ -2068,96 +2200,6 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2068
2200
  `"${cfg.provider.id}" ended the stream without completing the turn \u2014 nothing was generated. The endpoint answered, but not with a usable ${cfg.provider.kind} response.`
2069
2201
  );
2070
2202
  }
2071
- async function* streamWithRetry(provider, request, modelRef, signal) {
2072
- let lastError;
2073
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
2074
- if (signal.aborted) return;
2075
- try {
2076
- const stream = provider.stream(request)[Symbol.asyncIterator]();
2077
- let cancel;
2078
- let cancelTimer;
2079
- const cancelled = new Promise((resolve3) => {
2080
- cancel = () => {
2081
- cancelTimer ??= setTimeout(() => resolve3({ done: true, value: void 0 }), 100);
2082
- };
2083
- signal.addEventListener("abort", cancel, { once: true });
2084
- if (signal.aborted) cancel();
2085
- });
2086
- try {
2087
- for (; ; ) {
2088
- const next = await Promise.race([stream.next(), cancelled]);
2089
- if (next.done) break;
2090
- if (!signal.aborted || next.value.type === "usage" || next.value.type === "done") {
2091
- yield next.value;
2092
- }
2093
- if (next.value.type === "done") break;
2094
- }
2095
- } finally {
2096
- signal.removeEventListener("abort", cancel);
2097
- if (cancelTimer !== void 0) clearTimeout(cancelTimer);
2098
- void stream.return?.().catch(() => void 0);
2099
- }
2100
- return;
2101
- } catch (error) {
2102
- lastError = error;
2103
- if (attempt >= MAX_RETRIES2) break;
2104
- const waitMs = retryBackoffMs(error, attempt);
2105
- if (waitMs > 0) {
2106
- try {
2107
- await sleep(waitMs, signal);
2108
- } catch (error2) {
2109
- if (signal.aborted) return;
2110
- throw error2;
2111
- }
2112
- continue;
2113
- }
2114
- break;
2115
- }
2116
- }
2117
- if (lastError) throw lastError;
2118
- }
2119
- function retryBackoffMs(error, attempt) {
2120
- if (!isRateLimitError(error)) return 0;
2121
- const serverWait = retryAfterFromError(error);
2122
- if (serverWait > 0) return Math.min(serverWait, 6e4);
2123
- return Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, 3e4);
2124
- }
2125
- function isRateLimitError(error) {
2126
- if (!(error instanceof ProviderError)) return false;
2127
- return error.status === 429;
2128
- }
2129
- function retryAfterFromError(error) {
2130
- const cause = error?.cause;
2131
- const headers = cause?.headers;
2132
- if (headers && typeof headers.get === "function") {
2133
- const ms = Number(headers.get("retry-after-ms"));
2134
- if (Number.isFinite(ms) && ms > 0) return ms;
2135
- const raw = headers.get("retry-after");
2136
- if (raw) {
2137
- const s = Number(raw);
2138
- if (Number.isFinite(s) && s > 0) return s * 1e3;
2139
- }
2140
- }
2141
- return 0;
2142
- }
2143
- function sleep(ms, signal) {
2144
- return new Promise((resolve3, reject) => {
2145
- if (signal.aborted) {
2146
- reject(signal.reason ?? new Error("Aborted"));
2147
- return;
2148
- }
2149
- const finish = () => {
2150
- signal.removeEventListener("abort", onAbort);
2151
- resolve3();
2152
- };
2153
- const onAbort = () => {
2154
- clearTimeout(id);
2155
- reject(signal.reason ?? new Error("Aborted"));
2156
- };
2157
- const id = setTimeout(finish, ms);
2158
- signal.addEventListener("abort", onAbort, { once: true });
2159
- });
2160
- }
2161
2203
  var CHARS_PER_TOKEN = 4;
2162
2204
  function estimateRequestTokens(cfg, messages) {
2163
2205
  let characters3 = cfg.system.length;
@@ -2184,8 +2226,10 @@ async function runToolCalls(cfg, content, hooks, signal) {
2184
2226
  for (const call of calls) if (call.name !== "task") out.push(await run(call));
2185
2227
  return out;
2186
2228
  })();
2187
- const [settledOthers] = await Promise.allSettled([others]);
2188
- const settledTasks = await Promise.allSettled(tasks);
2229
+ const [[settledOthers], settledTasks] = await Promise.all([
2230
+ Promise.allSettled([others]),
2231
+ Promise.allSettled(tasks)
2232
+ ]);
2189
2233
  for (const result of [settledOthers, ...settledTasks]) {
2190
2234
  if (result.status === "rejected") throw result.reason;
2191
2235
  }
@@ -2763,7 +2807,7 @@ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2763
2807
  import path7 from "path";
2764
2808
 
2765
2809
  // src/tools/bash.ts
2766
- import { spawn as spawn2 } from "child_process";
2810
+ import { spawn as spawn3 } from "child_process";
2767
2811
  import { StringDecoder as StringDecoder2 } from "string_decoder";
2768
2812
  var DEFAULT_TIMEOUT = 12e4;
2769
2813
  var MAX_TIMEOUT = 6e5;
@@ -2825,7 +2869,7 @@ var bashTool = {
2825
2869
  });
2826
2870
  }
2827
2871
  return new Promise((settle) => {
2828
- const child = spawn2(command, {
2872
+ const child = spawn3(command, {
2829
2873
  shell: true,
2830
2874
  cwd: ctx.cwd,
2831
2875
  detached: process.platform !== "win32",
@@ -2865,31 +2909,15 @@ var bashTool = {
2865
2909
  };
2866
2910
  child.stdout.on("data", collector());
2867
2911
  child.stderr.on("data", collector());
2868
- const killTree2 = () => {
2869
- if (child.pid == null) return;
2870
- if (process.platform === "win32") {
2871
- try {
2872
- spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2873
- } catch {
2874
- child.kill("SIGKILL");
2875
- }
2876
- return;
2877
- }
2878
- try {
2879
- process.kill(-child.pid, "SIGKILL");
2880
- } catch {
2881
- child.kill("SIGKILL");
2882
- }
2883
- };
2884
2912
  let timedOut = false;
2885
2913
  let aborted = false;
2886
2914
  const timer = setTimeout(() => {
2887
2915
  timedOut = true;
2888
- killTree2();
2916
+ killTree(child);
2889
2917
  }, timeout);
2890
2918
  const onAbort = () => {
2891
2919
  aborted = true;
2892
- killTree2();
2920
+ killTree(child);
2893
2921
  };
2894
2922
  ctx.signal.addEventListener("abort", onAbort, { once: true });
2895
2923
  const finish = (result) => {
@@ -4032,26 +4060,31 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
4032
4060
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
4033
4061
  let summary2 = "";
4034
4062
  let finalText2 = "";
4063
+ let stopReason;
4035
4064
  let usage;
4036
4065
  let rateLimits;
4037
- const stream = provider.stream({
4066
+ const stream = streamWithRetry(provider, {
4038
4067
  model,
4039
4068
  system: systemPrompt,
4040
4069
  messages: [{ role: "user", content: [{ type: "text", text: source }] }],
4041
4070
  tools: [],
4042
4071
  maxTokens: Math.max(1, Math.floor(Math.min(maxOutputTokens, maxTokens))),
4043
- thinking: false,
4044
- signal
4045
- });
4072
+ compaction: true
4073
+ }, signal);
4046
4074
  for await (const event of stream) {
4047
4075
  if (event.type === "text_delta") summary2 += event.text;
4048
4076
  else if (event.type === "usage") usage = event.usage;
4049
4077
  else if (event.type === "rate_limits") rateLimits = event.limits;
4050
4078
  else if (event.type === "done") {
4079
+ stopReason = event.stopReason;
4051
4080
  finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
4052
4081
  }
4053
4082
  }
4054
4083
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
4084
+ if (stopReason !== "end_turn") {
4085
+ throw new Error(`Context summary is incomplete (${stopReason ?? "stream ended without a final response"}); original history was kept.`);
4086
+ }
4087
+ if (!(finalText2 || summary2).trim()) throw new Error("The provider returned an empty context summary.");
4055
4088
  return { text: (finalText2 || summary2).trim(), usage, rateLimits };
4056
4089
  }
4057
4090
  function mergeUsage(usages) {
@@ -4174,7 +4207,11 @@ function createSubagentRunner(makeConfig, tools) {
4174
4207
  const permissionLane = createExecutionLane();
4175
4208
  return {
4176
4209
  async run(request) {
4177
- if (request.signal.aborted) return SUBAGENT_CANCELLED;
4210
+ if (request.signal.aborted) {
4211
+ request.onComplete?.("cancelled", "Cancelled by the user.");
4212
+ return SUBAGENT_CANCELLED;
4213
+ }
4214
+ let stopReason = "";
4178
4215
  let steps = 0;
4179
4216
  let finalCall = false;
4180
4217
  const executionLane = createExecutionLane();
@@ -4227,6 +4264,7 @@ Working directory: ${base.cwd}`
4227
4264
  const signal = AbortSignal.any([request.signal, limit.signal]);
4228
4265
  const hooks = {
4229
4266
  onEvent(event) {
4267
+ if (event.type === "turn_end") stopReason = event.stopReason;
4230
4268
  if (event.type === "turn_start") {
4231
4269
  ++steps;
4232
4270
  if (steps > MAX_SUBAGENT_STEPS + 1) limit.abort();
@@ -4251,11 +4289,22 @@ Working directory: ${base.cwd}`
4251
4289
  { role: "user", content: [{ type: "text", text: request.prompt }] }
4252
4290
  ];
4253
4291
  const text = finalText(await runTurn(cfg, history, hooks, signal));
4254
- if (!limit.signal.aborted) return text;
4255
- if (request.signal.aborted && text === SUBAGENT_NO_ANSWER) return SUBAGENT_CANCELLED;
4256
- return text === SUBAGENT_NO_ANSWER ? STEP_LIMIT_NOTE : `${text}
4292
+ if (request.signal.aborted || stopReason === "aborted" && !limit.signal.aborted) {
4293
+ request.onComplete?.("cancelled", "Cancelled before the task finished; any output is partial.");
4294
+ return text === SUBAGENT_NO_ANSWER ? SUBAGENT_CANCELLED : text;
4295
+ }
4296
+ if (limit.signal.aborted || finalCall && (stopReason === "tool_use" || text === SUBAGENT_NO_ANSWER)) {
4297
+ request.onComplete?.("incomplete", STEP_LIMIT_NOTE);
4298
+ return text === SUBAGENT_NO_ANSWER ? STEP_LIMIT_NOTE : `${text}
4257
4299
 
4258
4300
  ${STEP_LIMIT_NOTE}`;
4301
+ }
4302
+ if (text === SUBAGENT_NO_ANSWER || stopReason === "max_tokens" || stopReason === "refusal" || stopReason === "pause_turn") {
4303
+ request.onComplete?.("incomplete", text === SUBAGENT_NO_ANSWER ? SUBAGENT_NO_ANSWER : `Task stopped (${stopReason}); output may be partial.`);
4304
+ } else {
4305
+ request.onComplete?.("done");
4306
+ }
4307
+ return text;
4259
4308
  }
4260
4309
  };
4261
4310
  }
@@ -4446,7 +4495,7 @@ function clip(value) {
4446
4495
  // package.json
4447
4496
  var package_default = {
4448
4497
  name: "@kernelonpanic/kitcode",
4449
- version: "1.4.5",
4498
+ version: "1.4.6",
4450
4499
  description: "Terminal coding agent with a config you never have to write by hand",
4451
4500
  type: "module",
4452
4501
  license: "MIT",
@@ -4514,7 +4563,7 @@ var package_default = {
4514
4563
 
4515
4564
  // src/version.ts
4516
4565
  var KITCODE_VERSION = package_default.version;
4517
- var KITCODE_COMMIT = true ? "aac7d92efa29456e6757bdef12abafb489064dbe" : "development";
4566
+ var KITCODE_COMMIT = true ? "98093a63cea2a1e99b9477191baf72c1839c9c27" : "development";
4518
4567
  function kitcodeUserAgent(sdkUserAgent2) {
4519
4568
  return `KitCode/${KITCODE_VERSION} ${sdkUserAgent2}`.trim();
4520
4569
  }
@@ -5160,12 +5209,12 @@ async function* streamOnce(client, providerId, apiKey2, req, thinkingOn) {
5160
5209
  messages: toMessageParams(req.messages),
5161
5210
  tools: toToolParams(req.tools)
5162
5211
  };
5163
- if (thinkingOn) {
5212
+ if (thinkingOn && !req.compaction) {
5164
5213
  const thinking2 = thinkingParam(req.model, req.maxTokens);
5165
5214
  if (thinking2) params.thinking = thinking2;
5166
5215
  }
5167
5216
  const effort = effectiveEffort("anthropic", req.model, req.effort, req.messages);
5168
- if (effort) params.output_config = { effort };
5217
+ if (effort && !req.compaction) params.output_config = { effort };
5169
5218
  let thinking = "";
5170
5219
  let text = "";
5171
5220
  let events = 0;
@@ -5431,8 +5480,8 @@ async function* streamOnce2(client, providerId, apiKey2, req, effort, thinkingOf
5431
5480
  {
5432
5481
  model: req.model,
5433
5482
  [tokenField]: req.maxTokens,
5434
- ...effort ? { reasoning_effort: effort } : {},
5435
- ...thinkingOff,
5483
+ ...effort && !req.compaction ? { reasoning_effort: effort } : {},
5484
+ ...req.compaction ? {} : thinkingOff,
5436
5485
  messages: toChatMessages(req.system, req.messages),
5437
5486
  ...req.tools.length > 0 ? { tools: toChatTools(req.tools) } : {},
5438
5487
  stream: true,
@@ -7094,8 +7143,14 @@ function createTaskTool(runner) {
7094
7143
  const subagentId = `sa${randomBytes3(4).toString("hex")}`;
7095
7144
  const description = field(input, "description") || "subagent";
7096
7145
  ctx.onEvent?.({ type: "subagent_start", id: subagentId, description });
7146
+ let status = "done";
7147
+ let reason;
7097
7148
  try {
7098
- const content = await runner.run({
7149
+ let content = await runner.run({
7150
+ onComplete: (outcome, detail) => {
7151
+ status = outcome;
7152
+ reason = detail;
7153
+ },
7099
7154
  prompt: instruction,
7100
7155
  description,
7101
7156
  signal: ctx.signal,
@@ -7104,10 +7159,19 @@ function createTaskTool(runner) {
7104
7159
  confirm: ctx.confirm,
7105
7160
  requestPermission: ctx.requestPermission
7106
7161
  });
7107
- ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: content });
7108
- if (progress.length === 0) return { content };
7162
+ if (ctx.signal.aborted) {
7163
+ status = "cancelled";
7164
+ reason = "Cancelled by the user; any output is partial.";
7165
+ }
7166
+ if (status !== "done" && reason) content = `${reason}
7167
+
7168
+ ${content}`;
7169
+ ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: content, status, reason });
7170
+ const isError = status !== "done";
7171
+ if (progress.length === 0) return { content, isError };
7109
7172
  return {
7110
7173
  content,
7174
+ isError,
7111
7175
  display: {
7112
7176
  kind: "list",
7113
7177
  title: `task ${brief(field(input, "description"), 40)}`,
@@ -7115,9 +7179,11 @@ function createTaskTool(runner) {
7115
7179
  }
7116
7180
  };
7117
7181
  } catch (error) {
7118
- const reason = error instanceof Error ? error.message : String(error);
7119
- ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: `The subagent failed: ${reason}` });
7120
- return { content: `The subagent failed: ${reason}`, isError: true };
7182
+ const reason2 = error instanceof Error ? error.message : String(error);
7183
+ const cancelled = ctx.signal.aborted;
7184
+ const content = cancelled ? `The subagent was cancelled: ${reason2}` : `The subagent failed: ${reason2}`;
7185
+ ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: content, status: cancelled ? "cancelled" : "error", reason: reason2 });
7186
+ return { content, isError: true };
7121
7187
  }
7122
7188
  }
7123
7189
  };
@@ -7781,6 +7847,9 @@ async function boot(options) {
7781
7847
  isSystemPromptEnabled: () => config.systemPromptEnabled && config.systemPrompt !== void 0 && config.systemPrompt.trim() !== "",
7782
7848
  async setSystemPrompt(text) {
7783
7849
  const trimmed = text.trim();
7850
+ if (trimmed.length > MAX_SYSTEM_PROMPT_CHARS) {
7851
+ throw new Error(`System prompt must be at most ${MAX_SYSTEM_PROMPT_CHARS.toLocaleString("en-US")} characters.`);
7852
+ }
7784
7853
  await persistConfig((draft) => {
7785
7854
  if (trimmed === "") delete draft.systemPrompt;
7786
7855
  else draft.systemPrompt = trimmed;
@@ -8382,7 +8451,7 @@ function validateKey(value) {
8382
8451
  import { render } from "ink";
8383
8452
 
8384
8453
  // src/ui/App.tsx
8385
- import { Box as Box14, Text as Text13, useApp, useInput as useInput4, useStdout as useStdout2 } from "ink";
8454
+ import { Box as Box14, Text as Text13, useApp, useInput as useInput3, useStdout as useStdout2 } from "ink";
8386
8455
  import { useCallback, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef5, useState as useState8 } from "react";
8387
8456
 
8388
8457
  // src/ui/terminal-size.ts
@@ -8578,7 +8647,7 @@ var COMMANDS = [
8578
8647
  { name: "resume" },
8579
8648
  { name: "undo" },
8580
8649
  { name: "clear" },
8581
- { name: "subagents", ...BUSY_SAFE },
8650
+ { name: "subagents", args: "[clear]", ...BUSY_SAFE },
8582
8651
  { name: "help", ...BUSY_SAFE },
8583
8652
  { name: "exit", ...BUSY_SAFE }
8584
8653
  ];
@@ -8865,6 +8934,13 @@ var en = {
8865
8934
  subagentsTitle: "Open a sub-agent chat",
8866
8935
  subagentRunning: "running",
8867
8936
  subagentDone: "done",
8937
+ subagentCancelled: "cancelled",
8938
+ subagentError: "error",
8939
+ subagentIncomplete: "incomplete",
8940
+ subagentsCleared: "Finished subagents cleared from the list.",
8941
+ noFinishedSubagents: "No finished subagents to clear.",
8942
+ subagentsClear: "Clear finished subagents",
8943
+ confirmExit: "Press Ctrl+C again within 3 seconds to exit. Any other key cancels.",
8868
8944
  subagentWaiting: "waiting for the first step\u2026",
8869
8945
  subagentBack: "esc back",
8870
8946
  attachUsage: "Usage: /attach <image-or-text-file> \xB7 /attach clipboard \xB7 /attach clear",
@@ -8950,6 +9026,7 @@ ${url}`,
8950
9026
  systemPromptCleared: "System prompt addition cleared.",
8951
9027
  systemPromptEnabled: "System prompt enabled.",
8952
9028
  systemPromptDisabled: "System prompt disabled (kept in config).",
9029
+ systemPromptTooLong: (max) => `System prompt must be at most ${max} characters.`,
8953
9030
  stepsCurrent: (n) => n === 0 ? "Model calls per turn: unlimited" : `Model calls per turn: ${n}`,
8954
9031
  stepsSet: (n) => `Model calls per turn set to ${n}`,
8955
9032
  stepsUnlimited: "Model calls per turn: unlimited (0). The turn budget still applies.",
@@ -9115,6 +9192,13 @@ var ru = {
9115
9192
  subagentsTitle: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442 \u0441\u0443\u0431-\u0430\u0433\u0435\u043D\u0442\u0430",
9116
9193
  subagentRunning: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442",
9117
9194
  subagentDone: "\u0433\u043E\u0442\u043E\u0432\u043E",
9195
+ subagentCancelled: "\u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E",
9196
+ subagentError: "\u043E\u0448\u0438\u0431\u043A\u0430",
9197
+ subagentIncomplete: "\u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E",
9198
+ subagentsCleared: "\u0417\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0435 \u0441\u0443\u0431\u0430\u0433\u0435\u043D\u0442\u044B \u0443\u0434\u0430\u043B\u0435\u043D\u044B \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430.",
9199
+ noFinishedSubagents: "\u041D\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0445 \u0441\u0443\u0431\u0430\u0433\u0435\u043D\u0442\u043E\u0432 \u0434\u043B\u044F \u043E\u0447\u0438\u0441\u0442\u043A\u0438.",
9200
+ subagentsClear: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0445 \u0441\u0443\u0431\u0430\u0433\u0435\u043D\u0442\u043E\u0432",
9201
+ confirmExit: "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 Ctrl+C \u0435\u0449\u0451 \u0440\u0430\u0437 \u0432 \u0442\u0435\u0447\u0435\u043D\u0438\u0435 3 \u0441\u0435\u043A\u0443\u043D\u0434 \u0434\u043B\u044F \u0432\u044B\u0445\u043E\u0434\u0430. \u041B\u044E\u0431\u0430\u044F \u0434\u0440\u0443\u0433\u0430\u044F \u043A\u043B\u0430\u0432\u0438\u0448\u0430 \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442 \u0432\u044B\u0445\u043E\u0434.",
9118
9202
  subagentWaiting: "\u0436\u0434\u0451\u043C \u043F\u0435\u0440\u0432\u044B\u0439 \u0448\u0430\u0433\u2026",
9119
9203
  subagentBack: "esc \u2014 \u043D\u0430\u0437\u0430\u0434",
9120
9204
  attachUsage: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /attach <\u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0430-\u0438\u043B\u0438-\u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439-\u0444\u0430\u0439\u043B> \xB7 /attach clipboard \xB7 /attach clear",
@@ -9200,6 +9284,7 @@ ${url}`,
9200
9284
  systemPromptCleared: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0443\u0434\u0430\u043B\u0435\u043D\u043E.",
9201
9285
  systemPromptEnabled: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E.",
9202
9286
  systemPromptDisabled: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E (\u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E \u0432 \u043A\u043E\u043D\u0444\u0438\u0433\u0435).",
9287
+ systemPromptTooLong: (max) => `\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0434\u043B\u0438\u043D\u043D\u0435\u0435 ${max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432.`,
9203
9288
  stepsCurrent: (n) => n === 0 ? "\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430" : `\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: ${n}`,
9204
9289
  stepsSet: (n) => `\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E: ${n}`,
9205
9290
  stepsUnlimited: "\u0412\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434: \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430 (0). \u0411\u044E\u0434\u0436\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u043E\u0432 \u0432\u0441\u0451 \u0435\u0449\u0451 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442.",
@@ -9293,6 +9378,7 @@ function Confirm({ title, body, danger, onAnswer }) {
9293
9378
  const theme = useTheme();
9294
9379
  const strings = useStrings();
9295
9380
  useTerminalInput((char, key) => {
9381
+ if (key.ctrl || key.meta || key.super) return;
9296
9382
  const c = normalizeHotkey(char);
9297
9383
  if (c === "y") onAnswer(true);
9298
9384
  else if (c === "n" || key.escape || key.return) onAnswer(false);
@@ -9675,6 +9761,7 @@ function PermissionPrompt({ request, onDecide }) {
9675
9761
  setSelected(dangerous ? options.length - 1 : 0);
9676
9762
  }, [request]);
9677
9763
  useTerminalInput((char, key) => {
9764
+ if (key.ctrl || key.meta || key.super) return;
9678
9765
  const c = normalizeHotkey(char);
9679
9766
  if (key.upArrow) setSelected((index) => (index - 1 + options.length) % options.length);
9680
9767
  else if (key.downArrow || key.tab) setSelected((index) => (index + 1) % options.length);
@@ -10455,7 +10542,25 @@ function ContextMeter({
10455
10542
  }
10456
10543
 
10457
10544
  // src/ui/components/SubagentChat.tsx
10458
- import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
10545
+ import { Box as Box12, Text as Text12 } from "ink";
10546
+
10547
+ // src/ui/subagent-status.ts
10548
+ function subagentStatusLabel(state, strings) {
10549
+ switch (state) {
10550
+ case "running":
10551
+ return strings.subagentRunning;
10552
+ case "done":
10553
+ return strings.subagentDone;
10554
+ case "cancelled":
10555
+ return strings.subagentCancelled;
10556
+ case "error":
10557
+ return strings.subagentError;
10558
+ case "incomplete":
10559
+ return strings.subagentIncomplete;
10560
+ }
10561
+ }
10562
+
10563
+ // src/ui/components/SubagentChat.tsx
10459
10564
  import { useState as useState7 } from "react";
10460
10565
 
10461
10566
  // src/ui/components/Transcript.tsx
@@ -11356,8 +11461,11 @@ function AssistantView({ bubble: source, maxRows }) {
11356
11461
  function clipTextToRows(text, maxRows, columns) {
11357
11462
  if (text === "" || maxRows <= 0) return "";
11358
11463
  const width = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 80;
11359
- const rows = text.replace(/\r\n?/g, "\n").split("\n").flatMap((line) => wrapVisualLine(line, width));
11360
- if (rows.length <= maxRows) return rows.join("\n");
11464
+ const bound = (maxRows + 1) * width;
11465
+ const sliced = text.length > bound;
11466
+ const source = sliced ? text.slice(text.length - bound) : text;
11467
+ const rows = source.replace(/\r\n?/g, "\n").split("\n").flatMap((line) => wrapVisualLine(line, width));
11468
+ if (!sliced && rows.length <= maxRows) return rows.join("\n");
11361
11469
  const tail2 = rows.slice(-Math.max(1, maxRows - 1));
11362
11470
  return maxRows === 1 ? tail2.at(-1) ?? "" : ["\u2026", ...tail2].join("\n");
11363
11471
  }
@@ -11460,6 +11568,13 @@ function previewLines(content) {
11460
11568
  return [...lines.slice(0, 6), `\u2026 ${lines.length - 6} more lines`];
11461
11569
  }
11462
11570
  var VISIBLE_SUBAGENTS = 3;
11571
+ var PROGRESS_TAIL_CHARS = 4e3;
11572
+ function progressTail(text) {
11573
+ if (text.length <= PROGRESS_TAIL_CHARS) return text;
11574
+ const cut = text.slice(text.length - PROGRESS_TAIL_CHARS);
11575
+ const newline = cut.indexOf("\n");
11576
+ return newline === -1 ? cut : cut.slice(newline + 1);
11577
+ }
11463
11578
  function SubagentSummary({ agents }) {
11464
11579
  const theme = useTheme();
11465
11580
  const running = agents.filter((agent) => agent.state === "running");
@@ -11468,12 +11583,14 @@ function SubagentSummary({ agents }) {
11468
11583
  "Subagents: ",
11469
11584
  running.length,
11470
11585
  " running \xB7 ",
11471
- agents.length - running.length,
11472
- " done"
11586
+ agents.filter((agent) => agent.state === "done").length,
11587
+ " done / ",
11588
+ agents.filter((agent) => agent.state !== "running" && agent.state !== "done").length,
11589
+ " stopped"
11473
11590
  ] }),
11474
11591
  running.slice(0, VISIBLE_SUBAGENTS).map((agent) => {
11475
11592
  const latest = agent.bubbles.at(-1);
11476
- const progress = latest?.kind === "tool" ? latest.summary : latest?.kind === "assistant" ? sanitizeThinkingText(latest.text || latest.thinking, latest.streaming) : "waiting";
11593
+ const progress = latest?.kind === "tool" ? latest.summary : latest?.kind === "assistant" ? sanitizeThinkingText(progressTail(latest.text || latest.thinking), latest.streaming) : "waiting";
11477
11594
  return /* @__PURE__ */ jsxs11(Text11, { dimColor: true, wrap: "truncate-end", children: [
11478
11595
  agent.description,
11479
11596
  " \xB7 ",
@@ -11485,23 +11602,28 @@ function SubagentSummary({ agents }) {
11485
11602
  }
11486
11603
  function SubagentView({ bubble, maxRows }) {
11487
11604
  const theme = useTheme();
11605
+ const strings = useStrings();
11488
11606
  const { columns } = useTerminalSize();
11489
11607
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
11490
- const color = bubble.state === "running" ? theme.warn : theme.ok;
11608
+ const color = bubble.state === "error" ? theme.error : bubble.state === "done" ? theme.ok : theme.warn;
11491
11609
  const latest = bubble.bubbles.at(-1);
11492
- const progress = latest?.kind === "assistant" ? sanitizeThinkingText(latest.text || latest.thinking, latest.streaming) : latest?.kind === "tool" ? latest.summary : "";
11610
+ const progress = latest?.kind === "assistant" ? sanitizeThinkingText(progressTail(latest.text || latest.thinking), latest.streaming) : latest?.kind === "tool" ? latest.summary : "";
11493
11611
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, minWidth: 0, children: [
11494
11612
  /* @__PURE__ */ jsxs11(Text11, { color, bold: true, wrap: "truncate-end", children: [
11495
11613
  mark,
11496
11614
  " subagent: ",
11497
- bubble.description
11615
+ bubble.description,
11616
+ " - ",
11617
+ subagentStatusLabel(bubble.state, strings)
11498
11618
  ] }),
11619
+ bubble.reason && /* @__PURE__ */ jsx11(Text11, { color: theme.warn, children: bubble.reason }),
11499
11620
  /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, flexDirection: "column", minWidth: 0, children: bubble.state === "running" ? /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", maxHeight: Math.max(1, (maxRows ?? 6) - 2), overflowY: "hidden", children: /* @__PURE__ */ jsx11(Markdown, { children: clipTextToRows(progress, Math.max(1, (maxRows ?? 6) - 2), Math.max(1, columns - 2)) }) }) : bubble.result ? /* @__PURE__ */ jsx11(Markdown, { children: sanitizeThinkingText(bubble.result) }) : null })
11500
11621
  ] });
11501
11622
  }
11502
11623
 
11503
11624
  // src/ui/components/SubagentChat.tsx
11504
11625
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
11626
+ var MAX_SHOWN_BUBBLES = 24;
11505
11627
  function SubagentChat({
11506
11628
  bubble,
11507
11629
  rows,
@@ -11511,7 +11633,7 @@ function SubagentChat({
11511
11633
  const strings = useStrings();
11512
11634
  const [offset, setOffset] = useState7(0);
11513
11635
  const total = bubble?.bubbles.length ?? 0;
11514
- useInput3((_input, key) => {
11636
+ useTerminalInput((_input, key) => {
11515
11637
  if (key.escape) onClose();
11516
11638
  else if (key.upArrow) setOffset((value) => Math.min(Math.max(0, total - 1), value + 1));
11517
11639
  else if (key.downArrow) setOffset((value) => Math.max(0, value - 1));
@@ -11523,18 +11645,19 @@ function SubagentChat({
11523
11645
  ] });
11524
11646
  }
11525
11647
  const running = bubble.state === "running";
11526
- const shown = bubble.bubbles.slice(0, total - offset);
11527
- const bodyRows = Math.max(3, rows - 2);
11648
+ const shown = bubble.bubbles.slice(Math.max(0, total - offset - MAX_SHOWN_BUBBLES), total - offset);
11649
+ const bodyRows = Math.max(3, rows - (bubble.reason ? 3 : 2));
11528
11650
  return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", minWidth: 0, children: [
11529
- /* @__PURE__ */ jsxs12(Text12, { color: running ? theme.warn : theme.ok, bold: true, wrap: "truncate-end", children: [
11651
+ /* @__PURE__ */ jsxs12(Text12, { color: running || bubble.state === "cancelled" || bubble.state === "incomplete" ? theme.warn : bubble.state === "error" ? theme.error : theme.ok, bold: true, wrap: "truncate-end", children: [
11530
11652
  running ? "\u25CC" : "\u25CF",
11531
11653
  " ",
11532
11654
  bubble.description,
11533
11655
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, bold: false, children: [
11534
11656
  " \xB7 ",
11535
- running ? strings.subagentRunning : strings.subagentDone
11657
+ subagentStatusLabel(bubble.state, strings)
11536
11658
  ] })
11537
11659
  ] }),
11660
+ bubble.reason && /* @__PURE__ */ jsx12(Text12, { color: theme.warn, wrap: "truncate-end", children: bubble.reason }),
11538
11661
  /* @__PURE__ */ jsxs12(
11539
11662
  Box12,
11540
11663
  {
@@ -11686,7 +11809,7 @@ function applyEvent(state, event) {
11686
11809
  return {
11687
11810
  ...state,
11688
11811
  bubbles: state.bubbles.map(
11689
- (bubble) => bubble.kind === "subagent" && bubble.id === event.id ? { ...bubble, state: "done", result: sanitizeTerminalText(event.result) } : bubble
11812
+ (bubble) => bubble.kind === "subagent" && bubble.id === event.id ? { ...bubble, state: event.status ?? "done", reason: event.reason ? sanitizeTerminalText(event.reason) : void 0, result: sanitizeTerminalText(event.result) } : bubble
11690
11813
  )
11691
11814
  };
11692
11815
  case "subagent_event": {
@@ -11718,6 +11841,10 @@ function applyEvent(state, event) {
11718
11841
  function applyEvents(state, events) {
11719
11842
  return coalesceDeltas(events).reduce(applyEvent, state);
11720
11843
  }
11844
+ function isStreamDelta(event) {
11845
+ if (event.type === "text_delta" || event.type === "thinking_delta") return true;
11846
+ return event.type === "subagent_event" && isStreamDelta(event.event);
11847
+ }
11721
11848
  function coalesceDeltas(events) {
11722
11849
  const out = [];
11723
11850
  for (const event of events) {
@@ -11912,6 +12039,8 @@ function App({
11912
12039
  const [, forceRender] = useState8(0);
11913
12040
  const history = useRef5(initialHistory);
11914
12041
  const abort = useRef5(null);
12042
+ const exitArmedAt = useRef5(null);
12043
+ const [hiddenSubagents, setHiddenSubagents] = useState8(() => /* @__PURE__ */ new Set());
11915
12044
  const sessionStart = useRef5(Date.now());
11916
12045
  const turns = useRef5(0);
11917
12046
  const [turnStart, setTurnStart] = useState8(null);
@@ -11963,7 +12092,7 @@ function App({
11963
12092
  (event) => {
11964
12093
  recordSpeed(speedMeter.current, event);
11965
12094
  transcriptEvents.current.push(event);
11966
- if (event.type !== "text_delta" && event.type !== "thinking_delta") {
12095
+ if (!isStreamDelta(event)) {
11967
12096
  flushTranscriptEvents();
11968
12097
  return;
11969
12098
  }
@@ -12041,6 +12170,9 @@ function App({
12041
12170
  turns.current += 1;
12042
12171
  const hooks = {
12043
12172
  onEvent: queueTranscriptEvent,
12173
+ onHistory: (messages) => {
12174
+ history.current = messages;
12175
+ },
12044
12176
  requestPermission: (request) => new Promise((resolve3) => {
12045
12177
  let settled = false;
12046
12178
  const finish = (decision) => {
@@ -12074,10 +12206,12 @@ function App({
12074
12206
  notice("error", error instanceof Error ? error.message : String(error));
12075
12207
  }
12076
12208
  history.current = await runtime.run(history.current, hooks, controller.signal);
12077
- void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
12078
12209
  } catch (error) {
12079
12210
  notice("error", error instanceof Error ? error.message : String(error));
12080
12211
  } finally {
12212
+ await runtime.persist(history.current).catch(
12213
+ (error) => notice("error", error instanceof Error ? error.message : String(error))
12214
+ );
12081
12215
  if (!controller.signal.aborted && Date.now() - startedAt >= BELL_AFTER_MS) bell();
12082
12216
  flushTranscriptEvents();
12083
12217
  abort.current = null;
@@ -12781,8 +12915,24 @@ ${strings.mcpAddUsage}`
12781
12915
  return;
12782
12916
  }
12783
12917
  case "subagents": {
12918
+ flushTranscriptEvents();
12919
+ const clearFinished = () => {
12920
+ const finished = transcriptRef.current.bubbles.filter(
12921
+ (bubble) => bubble.kind === "subagent" && bubble.state !== "running" && !hiddenSubagents.has(bubble.id)
12922
+ );
12923
+ if (finished.length === 0) {
12924
+ notice("info", strings.noFinishedSubagents);
12925
+ return;
12926
+ }
12927
+ setHiddenSubagents((previous) => /* @__PURE__ */ new Set([...previous, ...finished.map((bubble) => bubble.id)]));
12928
+ notice("info", strings.subagentsCleared);
12929
+ };
12930
+ if (rawRest === "clear") {
12931
+ clearFinished();
12932
+ return;
12933
+ }
12784
12934
  const agents = transcriptRef.current.bubbles.filter(
12785
- (bubble) => bubble.kind === "subagent"
12935
+ (bubble) => bubble.kind === "subagent" && !hiddenSubagents.has(bubble.id)
12786
12936
  );
12787
12937
  if (agents.length === 0) {
12788
12938
  notice("info", strings.noSubagents);
@@ -12795,13 +12945,14 @@ ${strings.mcpAddUsage}`
12795
12945
  const running = ordered.filter((agent) => agent.state === "running");
12796
12946
  const id = running.length === 1 && rawRest === "" ? running[0].id : await pick(
12797
12947
  strings.subagentsTitle,
12798
- ordered.slice(0, 20).map((agent) => ({
12948
+ [...ordered.slice(0, 20).map((agent) => ({
12799
12949
  key: agent.id,
12800
12950
  label: agent.description || "subagent",
12801
- hint: agent.state === "running" ? strings.subagentRunning : strings.subagentDone
12802
- }))
12951
+ hint: subagentStatusLabel(agent.state, strings)
12952
+ })), ...ordered.some((agent) => agent.state !== "running") ? [{ key: "__clear_finished__", label: strings.subagentsClear, hint: "/subagents clear" }] : []]
12803
12953
  );
12804
- if (id) setOverlay({ kind: "subagent", id });
12954
+ if (id === "__clear_finished__") clearFinished();
12955
+ else if (id) setOverlay({ kind: "subagent", id });
12805
12956
  return;
12806
12957
  }
12807
12958
  case "checker":
@@ -12876,27 +13027,20 @@ ${strings.mcpAddUsage}`
12876
13027
  }
12877
13028
  case "systemp": {
12878
13029
  const sub = rest[0]?.toLowerCase();
12879
- if (sub === "set") {
12880
- const text = rawRest.replace(/^\S+\s*/, "").trim();
12881
- if (!text) {
12882
- const typed = await promptInput(strings.systemPromptTitle);
12883
- if (!typed || typed.trim() === "") return;
12884
- await runtime.setSystemPrompt(typed);
12885
- notice("info", strings.systemPromptSet);
12886
- return;
12887
- }
12888
- await runtime.setSystemPrompt(text);
12889
- notice("info", strings.systemPromptSet);
12890
- return;
12891
- }
12892
- if (sub === "load") {
12893
- const filePath = rawRest.replace(/^\S+\s*/, "").trim();
12894
- if (!filePath) {
13030
+ if (sub === "set" || sub === "load") {
13031
+ const argument = rawRest.replace(/^\S+\s*/, "").trim();
13032
+ if (!argument && sub === "load") {
12895
13033
  notice("warn", strings.systemPromptUsage);
12896
13034
  return;
12897
13035
  }
12898
13036
  try {
12899
- const text = await runtime.loadSystemPromptFile(filePath);
13037
+ const text = sub === "set" ? argument || // No inline text: fall back to a full-screen input prompt.
13038
+ (await promptInput(strings.systemPromptTitle) ?? "").trim() : await runtime.loadSystemPromptFile(argument);
13039
+ if (!text) return;
13040
+ if (text.length > MAX_SYSTEM_PROMPT_CHARS) {
13041
+ notice("error", strings.systemPromptTooLong(MAX_SYSTEM_PROMPT_CHARS.toLocaleString(lang ?? "en")));
13042
+ return;
13043
+ }
12900
13044
  await runtime.setSystemPrompt(text);
12901
13045
  notice("info", strings.systemPromptSet);
12902
13046
  } catch (error) {
@@ -13147,7 +13291,7 @@ ${strings.mcpAddUsage}`
13147
13291
  }
13148
13292
  }
13149
13293
  },
13150
- [applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
13294
+ [applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings, hiddenSubagents, flushTranscriptEvents]
13151
13295
  );
13152
13296
  const runSlash = useCallback(
13153
13297
  (line) => {
@@ -13316,6 +13460,7 @@ ${strings.mcpAddUsage}`
13316
13460
  }
13317
13461
  const text = raw.trim();
13318
13462
  const submitSlash = () => {
13463
+ setPromptHistory((state) => appendInputHistory(state, text));
13319
13464
  if (!detachedInput) setInput("");
13320
13465
  const command = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase();
13321
13466
  if (busyRef.current && !commandRunsWhileBusy(command) || slashPendingRef.current > 0) {
@@ -13377,6 +13522,18 @@ ${strings.mcpAddUsage}`
13377
13522
  [replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
13378
13523
  );
13379
13524
  useTerminalInput((_char, key) => {
13525
+ if (key.ctrl && _char === "c") {
13526
+ const now = Date.now();
13527
+ if (exitArmedAt.current !== null && now - exitArmedAt.current < 3e3) {
13528
+ abort.current?.abort();
13529
+ exit();
13530
+ } else {
13531
+ exitArmedAt.current = now;
13532
+ notice("warn", strings.confirmExit);
13533
+ }
13534
+ return;
13535
+ }
13536
+ exitArmedAt.current = null;
13380
13537
  if (key.tab && key.shift) {
13381
13538
  const next = runtime.cycleMode();
13382
13539
  notice("info", strings.modeChanged(strings.modeLabel[next] ?? next));
@@ -13384,7 +13541,7 @@ ${strings.mcpAddUsage}`
13384
13541
  return;
13385
13542
  }
13386
13543
  if (!key.escape) return;
13387
- if (overlay.kind === "subagent") return;
13544
+ if (overlay.kind !== "none") return;
13388
13545
  if (checkedOut.current && overlay.kind === "none") {
13389
13546
  if (busyRef.current || slashPendingRef.current > 0) {
13390
13547
  putBackCheckedOut(input);
@@ -13410,21 +13567,6 @@ ${strings.mcpAddUsage}`
13410
13567
  if (clipboardPasteTask.current) clipboardPasteTask.current = null;
13411
13568
  return;
13412
13569
  }
13413
- if (overlay.kind === "permission") {
13414
- overlay.resolve("deny");
13415
- setOverlay({ kind: "none" });
13416
- return;
13417
- }
13418
- if (overlay.kind === "confirm") {
13419
- overlay.resolve(false);
13420
- setOverlay({ kind: "none" });
13421
- return;
13422
- }
13423
- if (overlay.kind === "picker") {
13424
- overlay.select("");
13425
- setOverlay({ kind: "none" });
13426
- return;
13427
- }
13428
13570
  setInput("");
13429
13571
  });
13430
13572
  const shell = (children) => /* @__PURE__ */ jsx14(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx14(StringsContext.Provider, { value: strings, children }) });
@@ -13591,7 +13733,7 @@ function TextInputOverlay({
13591
13733
  const submit = useCallback(() => {
13592
13734
  onAnswer(value || null);
13593
13735
  }, [value, onAnswer]);
13594
- useInput4((_input, key) => {
13736
+ useInput3((_input, key) => {
13595
13737
  if (key.escape) onCancel();
13596
13738
  });
13597
13739
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
@@ -13719,6 +13861,7 @@ var TUI_RENDER_OPTIONS = {
13719
13861
  // twice). Standard rendering clears the whole frame each time — a little
13720
13862
  // more work, but no stale lines.
13721
13863
  incrementalRendering: false,
13864
+ exitOnCtrlC: false,
13722
13865
  // Throttling defers writes; when a bubble closes into <Static> mid-stream,
13723
13866
  // the deferred frame and the static write race and leave stale rows behind.
13724
13867
  // Unthrottled renders cost a little CPU but keep every frame consistent.