@kernelonpanic/kitcode 1.4.3 → 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(),
@@ -81,6 +82,14 @@ var configSchema = z.object({
81
82
  // Manual override for the model's context window. Unset means the window
82
83
  // comes from the provider catalogue or the endpoint's model list.
83
84
  contextWindow: z.number().int().min(1e3).max(1e7).optional(),
85
+ // Extra instructions appended to the built-in system prompt on every turn.
86
+ systemPrompt: z.string().max(MAX_SYSTEM_PROMPT_CHARS).optional(),
87
+ // When false, systemPrompt is kept in the config but not sent to the model.
88
+ systemPromptEnabled: z.boolean().default(true),
89
+ // Hard cap on model calls in one turn. Higher values let large refactors
90
+ // finish; 0 disables the cap entirely (use with care — a runaway loop will
91
+ // keep burning tokens until you press Esc).
92
+ maxTurnSteps: z.number().int().min(0).max(1e3).default(256),
84
93
  maxTokens: z.number().int().positive().max(2e5).default(64e3),
85
94
  budget: budgetSchema.default({
86
95
  maxTokensPerTurn: 1e7,
@@ -1268,8 +1277,40 @@ function isSubsequence(needle, haystack) {
1268
1277
  return needle.length === 0;
1269
1278
  }
1270
1279
 
1271
- // src/tools/jobs.ts
1280
+ // src/tools/kill-tree.ts
1272
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";
1273
1314
  import { StringDecoder } from "string_decoder";
1274
1315
  var MAX_JOBS = 8;
1275
1316
  var MAX_JOB_OUTPUT = 1e5;
@@ -1285,7 +1326,7 @@ function startJob(command, cwd) {
1285
1326
  installExitCleanup();
1286
1327
  counter += 1;
1287
1328
  const id = `bg-${counter}`;
1288
- const child = spawn(command, {
1329
+ const child = spawn2(command, {
1289
1330
  shell: true,
1290
1331
  cwd,
1291
1332
  detached: process.platform !== "win32",
@@ -1391,22 +1432,6 @@ function summary(job) {
1391
1432
  signal: job.signal
1392
1433
  };
1393
1434
  }
1394
- function killTree(child) {
1395
- if (child.pid == null) return;
1396
- if (process.platform === "win32") {
1397
- try {
1398
- spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
1399
- } catch {
1400
- child.kill("SIGKILL");
1401
- }
1402
- return;
1403
- }
1404
- try {
1405
- process.kill(-child.pid, "SIGKILL");
1406
- } catch {
1407
- child.kill("SIGKILL");
1408
- }
1409
- }
1410
1435
 
1411
1436
  // src/core/memory.ts
1412
1437
  import { createHash } from "crypto";
@@ -1884,11 +1909,116 @@ function validSessionId(id) {
1884
1909
  return /^[A-Za-z0-9_-]{1,240}$/.test(id);
1885
1910
  }
1886
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
+
1887
2019
  // src/core/agent.ts
1888
2020
  var MAX_PAUSE_RESUMES = 5;
1889
- var MAX_RETRIES2 = 3;
1890
- var INITIAL_BACKOFF_MS = 1e3;
1891
- var MAX_TURN_STEPS = 64;
2021
+ var DEFAULT_MAX_TURN_STEPS = 256;
1892
2022
  function createExecutionLane() {
1893
2023
  let tail2 = Promise.resolve();
1894
2024
  return {
@@ -1900,6 +2030,8 @@ function createExecutionLane() {
1900
2030
  };
1901
2031
  }
1902
2032
  async function runTurn(cfg, history, hooks, signal) {
2033
+ const maxTurnSteps = cfg.maxTurnSteps ?? DEFAULT_MAX_TURN_STEPS;
2034
+ const hasStepCap = maxTurnSteps > 0;
1903
2035
  const permissionLane = cfg.permissionLane ?? createExecutionLane();
1904
2036
  const originalHooks = hooks;
1905
2037
  hooks = {
@@ -1914,11 +2046,12 @@ async function runTurn(cfg, history, hooks, signal) {
1914
2046
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1915
2047
  return sanitizeHistory(messages);
1916
2048
  }
1917
- if (++steps > MAX_TURN_STEPS) {
2049
+ steps += 1;
2050
+ if (hasStepCap && steps > maxTurnSteps) {
1918
2051
  hooks.onEvent({
1919
2052
  type: "notice",
1920
2053
  level: "warn",
1921
- text: `Stopped after ${MAX_TURN_STEPS} model calls without finishing, to avoid burning tokens in a loop. Ask again with a narrower request.`
2054
+ text: `Stopped after ${maxTurnSteps} model calls without finishing, to avoid burning tokens in a loop. Ask again with a narrower request.`
1922
2055
  });
1923
2056
  hooks.onEvent({ type: "turn_end", stopReason: "max_tokens" });
1924
2057
  return messages;
@@ -1935,6 +2068,7 @@ async function runTurn(cfg, history, hooks, signal) {
1935
2068
  }
1936
2069
  }
1937
2070
  const request = async () => {
2071
+ hooks.onHistory?.(sanitizeHistory(messages));
1938
2072
  if (signal.aborted) return { content: [], stopReason: "aborted" };
1939
2073
  const budgetDecision = cfg.budget?.beforeRequest({
1940
2074
  modelRef: cfg.modelRef,
@@ -1958,6 +2092,7 @@ async function runTurn(cfg, history, hooks, signal) {
1958
2092
  const outcome = await (cfg.modelRequestLane?.run(request) ?? request());
1959
2093
  if (!outcome) return messages;
1960
2094
  messages.push({ role: "assistant", content: outcome.content });
2095
+ hooks.onHistory?.(sanitizeHistory(messages));
1961
2096
  if (outcome.stopReason === "aborted") {
1962
2097
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1963
2098
  return sanitizeHistory(messages);
@@ -1965,6 +2100,7 @@ async function runTurn(cfg, history, hooks, signal) {
1965
2100
  if (outcome.stopReason === "tool_use") {
1966
2101
  const results = await runToolCalls(cfg, outcome.content, hooks, signal);
1967
2102
  if (results.length > 0) messages.push({ role: "user", content: results });
2103
+ hooks.onHistory?.(sanitizeHistory(messages));
1968
2104
  if (signal.aborted) {
1969
2105
  hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1970
2106
  return sanitizeHistory(messages);
@@ -2011,7 +2147,7 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2011
2147
  let outputTokens = 0;
2012
2148
  const startedAt = Date.now();
2013
2149
  let firstTokenAt;
2014
- for await (const event of streamWithRetry(cfg.provider, request, cfg.modelRef, signal)) {
2150
+ for await (const event of streamWithRetry(cfg.provider, request, signal)) {
2015
2151
  switch (event.type) {
2016
2152
  case "text_delta":
2017
2153
  firstTokenAt ??= Date.now();
@@ -2033,6 +2169,13 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2033
2169
  case "rate_limits":
2034
2170
  hooks.onEvent({ type: "rate_limits", model: cfg.modelRef, limits: event.limits });
2035
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;
2036
2179
  case "tool_call":
2037
2180
  break;
2038
2181
  case "done":
@@ -2057,96 +2200,6 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
2057
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.`
2058
2201
  );
2059
2202
  }
2060
- async function* streamWithRetry(provider, request, modelRef, signal) {
2061
- let lastError;
2062
- for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
2063
- if (signal.aborted) return;
2064
- try {
2065
- const stream = provider.stream(request)[Symbol.asyncIterator]();
2066
- let cancel;
2067
- let cancelTimer;
2068
- const cancelled = new Promise((resolve3) => {
2069
- cancel = () => {
2070
- cancelTimer ??= setTimeout(() => resolve3({ done: true, value: void 0 }), 100);
2071
- };
2072
- signal.addEventListener("abort", cancel, { once: true });
2073
- if (signal.aborted) cancel();
2074
- });
2075
- try {
2076
- for (; ; ) {
2077
- const next = await Promise.race([stream.next(), cancelled]);
2078
- if (next.done) break;
2079
- if (!signal.aborted || next.value.type === "usage" || next.value.type === "done") {
2080
- yield next.value;
2081
- }
2082
- if (next.value.type === "done") break;
2083
- }
2084
- } finally {
2085
- signal.removeEventListener("abort", cancel);
2086
- if (cancelTimer !== void 0) clearTimeout(cancelTimer);
2087
- void stream.return?.().catch(() => void 0);
2088
- }
2089
- return;
2090
- } catch (error) {
2091
- lastError = error;
2092
- if (attempt >= MAX_RETRIES2) break;
2093
- const waitMs = retryBackoffMs(error, attempt);
2094
- if (waitMs > 0) {
2095
- try {
2096
- await sleep(waitMs, signal);
2097
- } catch (error2) {
2098
- if (signal.aborted) return;
2099
- throw error2;
2100
- }
2101
- continue;
2102
- }
2103
- break;
2104
- }
2105
- }
2106
- if (lastError) throw lastError;
2107
- }
2108
- function retryBackoffMs(error, attempt) {
2109
- if (!isRateLimitError(error)) return 0;
2110
- const serverWait = retryAfterFromError(error);
2111
- if (serverWait > 0) return Math.min(serverWait, 6e4);
2112
- return Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, 3e4);
2113
- }
2114
- function isRateLimitError(error) {
2115
- if (!(error instanceof ProviderError)) return false;
2116
- return error.status === 429;
2117
- }
2118
- function retryAfterFromError(error) {
2119
- const cause = error?.cause;
2120
- const headers = cause?.headers;
2121
- if (headers && typeof headers.get === "function") {
2122
- const ms = Number(headers.get("retry-after-ms"));
2123
- if (Number.isFinite(ms) && ms > 0) return ms;
2124
- const raw = headers.get("retry-after");
2125
- if (raw) {
2126
- const s = Number(raw);
2127
- if (Number.isFinite(s) && s > 0) return s * 1e3;
2128
- }
2129
- }
2130
- return 0;
2131
- }
2132
- function sleep(ms, signal) {
2133
- return new Promise((resolve3, reject) => {
2134
- if (signal.aborted) {
2135
- reject(signal.reason ?? new Error("Aborted"));
2136
- return;
2137
- }
2138
- const finish = () => {
2139
- signal.removeEventListener("abort", onAbort);
2140
- resolve3();
2141
- };
2142
- const onAbort = () => {
2143
- clearTimeout(id);
2144
- reject(signal.reason ?? new Error("Aborted"));
2145
- };
2146
- const id = setTimeout(finish, ms);
2147
- signal.addEventListener("abort", onAbort, { once: true });
2148
- });
2149
- }
2150
2203
  var CHARS_PER_TOKEN = 4;
2151
2204
  function estimateRequestTokens(cfg, messages) {
2152
2205
  let characters3 = cfg.system.length;
@@ -2173,8 +2226,10 @@ async function runToolCalls(cfg, content, hooks, signal) {
2173
2226
  for (const call of calls) if (call.name !== "task") out.push(await run(call));
2174
2227
  return out;
2175
2228
  })();
2176
- const [settledOthers] = await Promise.allSettled([others]);
2177
- const settledTasks = await Promise.allSettled(tasks);
2229
+ const [[settledOthers], settledTasks] = await Promise.all([
2230
+ Promise.allSettled([others]),
2231
+ Promise.allSettled(tasks)
2232
+ ]);
2178
2233
  for (const result of [settledOthers, ...settledTasks]) {
2179
2234
  if (result.status === "rejected") throw result.reason;
2180
2235
  }
@@ -2752,7 +2807,7 @@ import { readFile as readFile6, stat as stat4 } from "fs/promises";
2752
2807
  import path7 from "path";
2753
2808
 
2754
2809
  // src/tools/bash.ts
2755
- import { spawn as spawn2 } from "child_process";
2810
+ import { spawn as spawn3 } from "child_process";
2756
2811
  import { StringDecoder as StringDecoder2 } from "string_decoder";
2757
2812
  var DEFAULT_TIMEOUT = 12e4;
2758
2813
  var MAX_TIMEOUT = 6e5;
@@ -2814,7 +2869,7 @@ var bashTool = {
2814
2869
  });
2815
2870
  }
2816
2871
  return new Promise((settle) => {
2817
- const child = spawn2(command, {
2872
+ const child = spawn3(command, {
2818
2873
  shell: true,
2819
2874
  cwd: ctx.cwd,
2820
2875
  detached: process.platform !== "win32",
@@ -2854,31 +2909,15 @@ var bashTool = {
2854
2909
  };
2855
2910
  child.stdout.on("data", collector());
2856
2911
  child.stderr.on("data", collector());
2857
- const killTree2 = () => {
2858
- if (child.pid == null) return;
2859
- if (process.platform === "win32") {
2860
- try {
2861
- spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
2862
- } catch {
2863
- child.kill("SIGKILL");
2864
- }
2865
- return;
2866
- }
2867
- try {
2868
- process.kill(-child.pid, "SIGKILL");
2869
- } catch {
2870
- child.kill("SIGKILL");
2871
- }
2872
- };
2873
2912
  let timedOut = false;
2874
2913
  let aborted = false;
2875
2914
  const timer = setTimeout(() => {
2876
2915
  timedOut = true;
2877
- killTree2();
2916
+ killTree(child);
2878
2917
  }, timeout);
2879
2918
  const onAbort = () => {
2880
2919
  aborted = true;
2881
- killTree2();
2920
+ killTree(child);
2882
2921
  };
2883
2922
  ctx.signal.addEventListener("abort", onAbort, { once: true });
2884
2923
  const finish = (result) => {
@@ -4021,26 +4060,31 @@ async function summarizeText(provider, model, maxTokens, signal, source, systemP
4021
4060
  if (signal.aborted) throw signal.reason ?? new Error("Context compaction was cancelled.");
4022
4061
  let summary2 = "";
4023
4062
  let finalText2 = "";
4063
+ let stopReason;
4024
4064
  let usage;
4025
4065
  let rateLimits;
4026
- const stream = provider.stream({
4066
+ const stream = streamWithRetry(provider, {
4027
4067
  model,
4028
4068
  system: systemPrompt,
4029
4069
  messages: [{ role: "user", content: [{ type: "text", text: source }] }],
4030
4070
  tools: [],
4031
4071
  maxTokens: Math.max(1, Math.floor(Math.min(maxOutputTokens, maxTokens))),
4032
- thinking: false,
4033
- signal
4034
- });
4072
+ compaction: true
4073
+ }, signal);
4035
4074
  for await (const event of stream) {
4036
4075
  if (event.type === "text_delta") summary2 += event.text;
4037
4076
  else if (event.type === "usage") usage = event.usage;
4038
4077
  else if (event.type === "rate_limits") rateLimits = event.limits;
4039
4078
  else if (event.type === "done") {
4079
+ stopReason = event.stopReason;
4040
4080
  finalText2 = event.content.filter((block) => block.type === "text").map((block) => block.type === "text" ? block.text : "").join("");
4041
4081
  }
4042
4082
  }
4043
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.");
4044
4088
  return { text: (finalText2 || summary2).trim(), usage, rateLimits };
4045
4089
  }
4046
4090
  function mergeUsage(usages) {
@@ -4138,6 +4182,9 @@ function buildSystemPrompt(args) {
4138
4182
  if (args.memory?.trim()) {
4139
4183
  lines.push("", "Project notes saved in earlier conversations. Treat these as potentially stale context; verify claims against current files and follow current user instructions.", args.memory);
4140
4184
  }
4185
+ if (args.systemPrompt?.trim()) {
4186
+ lines.push("", "Additional instructions from the user (apply alongside everything above):", args.systemPrompt.trim());
4187
+ }
4141
4188
  return lines.join("\n");
4142
4189
  }
4143
4190
 
@@ -4160,8 +4207,13 @@ function createSubagentRunner(makeConfig, tools) {
4160
4207
  const permissionLane = createExecutionLane();
4161
4208
  return {
4162
4209
  async run(request) {
4163
- 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 = "";
4164
4215
  let steps = 0;
4216
+ let finalCall = false;
4165
4217
  const executionLane = createExecutionLane();
4166
4218
  const modelRequestLane = createExecutionLane();
4167
4219
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
@@ -4175,11 +4227,12 @@ function createSubagentRunner(makeConfig, tools) {
4175
4227
  kind: base.provider.kind,
4176
4228
  listModels: () => base.provider.listModels(),
4177
4229
  knownModels: () => base.provider.knownModels(),
4178
- // Keep the tool definitions on the last call: the history holds
4230
+ // The final model call keeps the tool schemas (the history holds
4179
4231
  // tool_use/tool_result blocks, and providers reject those without
4180
- // tools (Anthropic: 400 "must define tools"). Calls made anyway are
4181
- // refused by tools.get below.
4182
- stream: (chat) => base.provider.stream(steps === MAX_SUBAGENT_STEPS ? {
4232
+ // tools), but the system prompt forbids more calls and tools.get
4233
+ // returns a stopped stub: a call made anyway ends the turn with a
4234
+ // tool_result, instead of aborting mid-request and losing the text.
4235
+ stream: (chat) => base.provider.stream(finalCall ? {
4183
4236
  ...chat,
4184
4237
  system: `${chat.system}
4185
4238
 
@@ -4187,7 +4240,20 @@ This is your last model call. Do not call tools. Return your findings now, clear
4187
4240
  } : chat)
4188
4241
  },
4189
4242
  tools: {
4190
- get: (name) => name === TASK_TOOL_NAME || steps >= MAX_SUBAGENT_STEPS ? void 0 : base.tools.get(name),
4243
+ get: (name) => {
4244
+ if (name === TASK_TOOL_NAME) return void 0;
4245
+ const tool = base.tools.get(name);
4246
+ if (!tool) return void 0;
4247
+ if (!finalCall) return tool;
4248
+ return {
4249
+ ...tool,
4250
+ permission: () => "allow",
4251
+ execute: () => Promise.resolve({
4252
+ content: "Tool budget exhausted: this was your last model call. Write the final answer now from what you already gathered.",
4253
+ isError: true
4254
+ })
4255
+ };
4256
+ },
4191
4257
  schemas: () => base.tools.schemas().filter((schema) => schema.name !== TASK_TOOL_NAME)
4192
4258
  },
4193
4259
  system: `${SUBAGENT_SYSTEM}
@@ -4198,7 +4264,12 @@ Working directory: ${base.cwd}`
4198
4264
  const signal = AbortSignal.any([request.signal, limit.signal]);
4199
4265
  const hooks = {
4200
4266
  onEvent(event) {
4201
- if (event.type === "turn_start" && ++steps > MAX_SUBAGENT_STEPS) limit.abort();
4267
+ if (event.type === "turn_end") stopReason = event.stopReason;
4268
+ if (event.type === "turn_start") {
4269
+ ++steps;
4270
+ if (steps > MAX_SUBAGENT_STEPS + 1) limit.abort();
4271
+ else if (steps >= MAX_SUBAGENT_STEPS) finalCall = true;
4272
+ }
4202
4273
  if (event.type === "tool_start") request.onProgress(brief(event.summary, 80));
4203
4274
  if (event.type === "subagent_start" || event.type === "subagent_end" || event.type === "subagent_event") return;
4204
4275
  request.onEvent?.(event);
@@ -4218,11 +4289,22 @@ Working directory: ${base.cwd}`
4218
4289
  { role: "user", content: [{ type: "text", text: request.prompt }] }
4219
4290
  ];
4220
4291
  const text = finalText(await runTurn(cfg, history, hooks, signal));
4221
- if (!limit.signal.aborted) return text;
4222
- if (request.signal.aborted && text === SUBAGENT_NO_ANSWER) return SUBAGENT_CANCELLED;
4223
- 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}
4224
4299
 
4225
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;
4226
4308
  }
4227
4309
  };
4228
4310
  }
@@ -4413,7 +4495,7 @@ function clip(value) {
4413
4495
  // package.json
4414
4496
  var package_default = {
4415
4497
  name: "@kernelonpanic/kitcode",
4416
- version: "1.4.3",
4498
+ version: "1.4.6",
4417
4499
  description: "Terminal coding agent with a config you never have to write by hand",
4418
4500
  type: "module",
4419
4501
  license: "MIT",
@@ -4481,7 +4563,7 @@ var package_default = {
4481
4563
 
4482
4564
  // src/version.ts
4483
4565
  var KITCODE_VERSION = package_default.version;
4484
- var KITCODE_COMMIT = true ? "586eb4def64a8ef824bf2fe2f58a9f71e18090f0" : "development";
4566
+ var KITCODE_COMMIT = true ? "98093a63cea2a1e99b9477191baf72c1839c9c27" : "development";
4485
4567
  function kitcodeUserAgent(sdkUserAgent2) {
4486
4568
  return `KitCode/${KITCODE_VERSION} ${sdkUserAgent2}`.trim();
4487
4569
  }
@@ -5127,12 +5209,12 @@ async function* streamOnce(client, providerId, apiKey2, req, thinkingOn) {
5127
5209
  messages: toMessageParams(req.messages),
5128
5210
  tools: toToolParams(req.tools)
5129
5211
  };
5130
- if (thinkingOn) {
5212
+ if (thinkingOn && !req.compaction) {
5131
5213
  const thinking2 = thinkingParam(req.model, req.maxTokens);
5132
5214
  if (thinking2) params.thinking = thinking2;
5133
5215
  }
5134
5216
  const effort = effectiveEffort("anthropic", req.model, req.effort, req.messages);
5135
- if (effort) params.output_config = { effort };
5217
+ if (effort && !req.compaction) params.output_config = { effort };
5136
5218
  let thinking = "";
5137
5219
  let text = "";
5138
5220
  let events = 0;
@@ -5398,8 +5480,8 @@ async function* streamOnce2(client, providerId, apiKey2, req, effort, thinkingOf
5398
5480
  {
5399
5481
  model: req.model,
5400
5482
  [tokenField]: req.maxTokens,
5401
- ...effort ? { reasoning_effort: effort } : {},
5402
- ...thinkingOff,
5483
+ ...effort && !req.compaction ? { reasoning_effort: effort } : {},
5484
+ ...req.compaction ? {} : thinkingOff,
5403
5485
  messages: toChatMessages(req.system, req.messages),
5404
5486
  ...req.tools.length > 0 ? { tools: toChatTools(req.tools) } : {},
5405
5487
  stream: true,
@@ -7061,8 +7143,14 @@ function createTaskTool(runner) {
7061
7143
  const subagentId = `sa${randomBytes3(4).toString("hex")}`;
7062
7144
  const description = field(input, "description") || "subagent";
7063
7145
  ctx.onEvent?.({ type: "subagent_start", id: subagentId, description });
7146
+ let status = "done";
7147
+ let reason;
7064
7148
  try {
7065
- const content = await runner.run({
7149
+ let content = await runner.run({
7150
+ onComplete: (outcome, detail) => {
7151
+ status = outcome;
7152
+ reason = detail;
7153
+ },
7066
7154
  prompt: instruction,
7067
7155
  description,
7068
7156
  signal: ctx.signal,
@@ -7071,10 +7159,19 @@ function createTaskTool(runner) {
7071
7159
  confirm: ctx.confirm,
7072
7160
  requestPermission: ctx.requestPermission
7073
7161
  });
7074
- ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: content });
7075
- 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 };
7076
7172
  return {
7077
7173
  content,
7174
+ isError,
7078
7175
  display: {
7079
7176
  kind: "list",
7080
7177
  title: `task ${brief(field(input, "description"), 40)}`,
@@ -7082,9 +7179,11 @@ function createTaskTool(runner) {
7082
7179
  }
7083
7180
  };
7084
7181
  } catch (error) {
7085
- const reason = error instanceof Error ? error.message : String(error);
7086
- ctx.onEvent?.({ type: "subagent_end", id: subagentId, result: `The subagent failed: ${reason}` });
7087
- 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 };
7088
7187
  }
7089
7188
  }
7090
7189
  };
@@ -7292,7 +7391,8 @@ async function boot(options) {
7292
7391
  cwd: options.cwd,
7293
7392
  toolNames: tools.list().map((tool) => tool.name),
7294
7393
  skills: skillCatalogue,
7295
- memory: projectMemory
7394
+ memory: projectMemory,
7395
+ systemPrompt: config.systemPromptEnabled ? config.systemPrompt : void 0
7296
7396
  });
7297
7397
  let modelRef = session.model || config.model || "";
7298
7398
  if (options.modelRef) modelRef = options.modelRef;
@@ -7362,6 +7462,7 @@ async function boot(options) {
7362
7462
  maxTokens: config.maxTokens,
7363
7463
  effort: config.effort,
7364
7464
  thinking: config.thinking,
7465
+ maxTurnSteps: config.maxTurnSteps,
7365
7466
  budget: activeBudget,
7366
7467
  checkpoint: activeCheckpoint,
7367
7468
  trimContext: (messages) => trimForWindow(resolved.provider.kind, messages)
@@ -7742,6 +7843,34 @@ async function boot(options) {
7742
7843
  draft.lang = lang;
7743
7844
  });
7744
7845
  },
7846
+ getSystemPrompt: () => config.systemPrompt ?? "",
7847
+ isSystemPromptEnabled: () => config.systemPromptEnabled && config.systemPrompt !== void 0 && config.systemPrompt.trim() !== "",
7848
+ async setSystemPrompt(text) {
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
+ }
7853
+ await persistConfig((draft) => {
7854
+ if (trimmed === "") delete draft.systemPrompt;
7855
+ else draft.systemPrompt = trimmed;
7856
+ });
7857
+ },
7858
+ async setSystemPromptEnabled(enabled) {
7859
+ await persistConfig((draft) => {
7860
+ draft.systemPromptEnabled = enabled;
7861
+ });
7862
+ },
7863
+ async loadSystemPromptFile(requestedPath) {
7864
+ const { readFile: readFile15 } = await import("fs/promises");
7865
+ const resolved = path15.resolve(options.cwd, requestedPath);
7866
+ return readFile15(resolved, "utf8");
7867
+ },
7868
+ getMaxTurnSteps: () => config.maxTurnSteps,
7869
+ async setMaxTurnSteps(steps) {
7870
+ await persistConfig((draft) => {
7871
+ draft.maxTurnSteps = steps;
7872
+ });
7873
+ },
7745
7874
  getAccent: () => config.theme.accent,
7746
7875
  async setAccent(value) {
7747
7876
  await persistConfig((draft) => {
@@ -8508,6 +8637,8 @@ var COMMANDS = [
8508
8637
  { name: "mcp", args: "[add|list|delete|enable|disable]" },
8509
8638
  { name: "attach", args: "<path|clipboard|clear>" },
8510
8639
  { name: "memory", args: "[show|set <text>|clear]" },
8640
+ { name: "systemp", args: "[set <text>|clear]", ...BUSY_SAFE },
8641
+ { name: "steps", args: "[count]", ...BUSY_SAFE },
8511
8642
  { name: "compact" },
8512
8643
  { name: "update" },
8513
8644
  { name: "checker", ...BUSY_SAFE },
@@ -8516,7 +8647,7 @@ var COMMANDS = [
8516
8647
  { name: "resume" },
8517
8648
  { name: "undo" },
8518
8649
  { name: "clear" },
8519
- { name: "subagents", ...BUSY_SAFE },
8650
+ { name: "subagents", args: "[clear]", ...BUSY_SAFE },
8520
8651
  { name: "help", ...BUSY_SAFE },
8521
8652
  { name: "exit", ...BUSY_SAFE }
8522
8653
  ];
@@ -8803,6 +8934,13 @@ var en = {
8803
8934
  subagentsTitle: "Open a sub-agent chat",
8804
8935
  subagentRunning: "running",
8805
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.",
8806
8944
  subagentWaiting: "waiting for the first step\u2026",
8807
8945
  subagentBack: "esc back",
8808
8946
  attachUsage: "Usage: /attach <image-or-text-file> \xB7 /attach clipboard \xB7 /attach clear",
@@ -8881,6 +9019,18 @@ ${url}`,
8881
9019
  memoryEmpty: "Project memory is empty.",
8882
9020
  memoryClearAsk: "Clear project memory?",
8883
9021
  memoryReplaceAsk: "Replace all project memory notes with this text?",
9022
+ systemPromptTitle: "System prompt addition",
9023
+ systemPromptUsage: "/systemp [show] \xB7 /systemp set <text> \xB7 /systemp clear",
9024
+ systemPromptEmpty: "No extra system prompt. Set one with /systemp set <text>.",
9025
+ systemPromptSet: "System prompt addition saved.",
9026
+ systemPromptCleared: "System prompt addition cleared.",
9027
+ systemPromptEnabled: "System prompt enabled.",
9028
+ systemPromptDisabled: "System prompt disabled (kept in config).",
9029
+ systemPromptTooLong: (max) => `System prompt must be at most ${max} characters.`,
9030
+ stepsCurrent: (n) => n === 0 ? "Model calls per turn: unlimited" : `Model calls per turn: ${n}`,
9031
+ stepsSet: (n) => `Model calls per turn set to ${n}`,
9032
+ stepsUnlimited: "Model calls per turn: unlimited (0). The turn budget still applies.",
9033
+ stepsInvalid: "Usage: /steps <0-1000> (0 = unlimited)",
8884
9034
  promptDeleted: (name) => `Deleted prompt "${name}"`,
8885
9035
  promptNotFound: (name) => `Prompt "${name}" not found.`,
8886
9036
  promptsEmpty: "No saved prompts. Use /prompt save <name>.",
@@ -8923,6 +9073,8 @@ ${url}`,
8923
9073
  theme: "change the accent colour",
8924
9074
  lang: "change the interface language",
8925
9075
  memory: "view, replace or clear project memory",
9076
+ systemp: "view, set, load from file, or toggle the extra system prompt",
9077
+ steps: "cap model calls per turn",
8926
9078
  prompt: "insert or save a prompt",
8927
9079
  "prompt save": "save the current or recent text as a prompt",
8928
9080
  "prompt delete": "delete a saved prompt",
@@ -9040,6 +9192,13 @@ var ru = {
9040
9192
  subagentsTitle: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0447\u0430\u0442 \u0441\u0443\u0431-\u0430\u0433\u0435\u043D\u0442\u0430",
9041
9193
  subagentRunning: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442",
9042
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.",
9043
9202
  subagentWaiting: "\u0436\u0434\u0451\u043C \u043F\u0435\u0440\u0432\u044B\u0439 \u0448\u0430\u0433\u2026",
9044
9203
  subagentBack: "esc \u2014 \u043D\u0430\u0437\u0430\u0434",
9045
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",
@@ -9118,6 +9277,18 @@ ${url}`,
9118
9277
  memoryEmpty: "\u041F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0443\u0441\u0442\u0430.",
9119
9278
  memoryClearAsk: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430?",
9120
9279
  memoryReplaceAsk: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435 \u0437\u0430\u043C\u0435\u0442\u043A\u0438 \u043F\u0430\u043C\u044F\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u044D\u0442\u0438\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C?",
9280
+ systemPromptTitle: "\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",
9281
+ systemPromptUsage: "/systemp [show] \xB7 /systemp set <\u0442\u0435\u043A\u0441\u0442> \xB7 /systemp clear",
9282
+ systemPromptEmpty: "\u0414\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u043A \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u043C\u0443 \u043F\u0440\u043E\u043C\u043F\u0442\u0443 \u043D\u0435\u0442. \u0417\u0430\u0434\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 /systemp set <\u0442\u0435\u043A\u0441\u0442>.",
9283
+ systemPromptSet: "\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 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E.",
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.",
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.",
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.`,
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}`,
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}`,
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.",
9291
+ stepsInvalid: "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435: /steps <0-1000> (0 = \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430)",
9121
9292
  promptDeleted: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u0443\u0434\u0430\u043B\u0451\u043D`,
9122
9293
  promptNotFound: (name) => `\u041F\u0440\u043E\u043C\u0442 \xAB${name}\xBB \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D.`,
9123
9294
  promptsEmpty: "\u0421\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0445 \u043F\u0440\u043E\u043C\u0442\u043E\u0432 \u043D\u0435\u0442. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 /prompt save <\u0438\u043C\u044F>.",
@@ -9160,6 +9331,8 @@ ${url}`,
9160
9331
  theme: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0430\u043A\u0446\u0435\u043D\u0442\u0430",
9161
9332
  lang: "\u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430",
9162
9333
  memory: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442\u0430",
9334
+ systemp: "\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C, \u0437\u0430\u0434\u0430\u0442\u044C, \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0438\u0437 \u0444\u0430\u0439\u043B\u0430 \u0438\u043B\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u0432\u044B\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0434\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",
9335
+ steps: "\u043B\u0438\u043C\u0438\u0442 \u0432\u044B\u0437\u043E\u0432\u043E\u0432 \u043C\u043E\u0434\u0435\u043B\u0438 \u0437\u0430 \u0445\u043E\u0434",
9163
9336
  prompt: "\u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u0442",
9164
9337
  skills: "\u0441\u043F\u0438\u0441\u043E\u043A \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u043A\u0438\u043B\u043B\u043E\u0432",
9165
9338
  "prompt delete": "\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u043F\u0440\u043E\u043C\u0442",
@@ -9205,6 +9378,7 @@ function Confirm({ title, body, danger, onAnswer }) {
9205
9378
  const theme = useTheme();
9206
9379
  const strings = useStrings();
9207
9380
  useTerminalInput((char, key) => {
9381
+ if (key.ctrl || key.meta || key.super) return;
9208
9382
  const c = normalizeHotkey(char);
9209
9383
  if (c === "y") onAnswer(true);
9210
9384
  else if (c === "n" || key.escape || key.return) onAnswer(false);
@@ -9338,18 +9512,30 @@ function LanguagePicker({ onPick }) {
9338
9512
  }
9339
9513
 
9340
9514
  // src/ui/components/Onboarding.tsx
9341
- import { Box as Box4, Text as Text4 } from "ink";
9515
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
9342
9516
  import Spinner from "ink-spinner";
9343
9517
  import TextInput from "ink-text-input";
9344
9518
  import { useState as useState2 } from "react";
9345
9519
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
9346
- function Onboarding({ onSubmit }) {
9520
+ function Onboarding({
9521
+ onSubmit,
9522
+ onCancel
9523
+ }) {
9347
9524
  const theme = useTheme();
9348
9525
  const strings = useStrings();
9349
9526
  const [step, setStep] = useState2("url");
9350
9527
  const [url, setUrl] = useState2("");
9351
9528
  const [key, setKey] = useState2("");
9352
9529
  const [error, setError] = useState2("");
9530
+ useInput2((_input, inputKey) => {
9531
+ if (!inputKey.escape || step === "working") return;
9532
+ if (step === "key") {
9533
+ setError("");
9534
+ setStep("url");
9535
+ } else {
9536
+ onCancel?.();
9537
+ }
9538
+ });
9353
9539
  const submitKey = async (value) => {
9354
9540
  if (value.trim() === "") return;
9355
9541
  setStep("working");
@@ -9575,6 +9761,7 @@ function PermissionPrompt({ request, onDecide }) {
9575
9761
  setSelected(dangerous ? options.length - 1 : 0);
9576
9762
  }, [request]);
9577
9763
  useTerminalInput((char, key) => {
9764
+ if (key.ctrl || key.meta || key.super) return;
9578
9765
  const c = normalizeHotkey(char);
9579
9766
  if (key.upArrow) setSelected((index) => (index - 1 + options.length) % options.length);
9580
9767
  else if (key.downArrow || key.tab) setSelected((index) => (index + 1) % options.length);
@@ -10355,13 +10542,30 @@ function ContextMeter({
10355
10542
  }
10356
10543
 
10357
10544
  // src/ui/components/SubagentChat.tsx
10358
- import { Box as Box12, Text as Text12, useInput as useInput2 } 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
10359
10564
  import { useState as useState7 } from "react";
10360
10565
 
10361
10566
  // src/ui/components/Transcript.tsx
10362
10567
  import { Box as Box11, Static, Text as Text11 } from "ink";
10363
10568
  import { memo as memo2, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
10364
- import Spinner2 from "ink-spinner";
10365
10569
  import stringWidth4 from "string-width";
10366
10570
 
10367
10571
  // src/ui/markdown.tsx
@@ -11251,17 +11455,17 @@ function AssistantView({ bubble: source, maxRows }) {
11251
11455
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, children: [
11252
11456
  frameThinking !== "" && /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx11(Markdown, { dim: true, children: frameThinking }) }),
11253
11457
  bubble.streaming ? /* @__PURE__ */ jsx11(Text11, { children: frameText }) : /* @__PURE__ */ jsx11(Markdown, { children: bubble.text }),
11254
- bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
11255
- /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
11256
- " thinking"
11257
- ] })
11458
+ bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "thinking\u2026" })
11258
11459
  ] });
11259
11460
  }
11260
11461
  function clipTextToRows(text, maxRows, columns) {
11261
11462
  if (text === "" || maxRows <= 0) return "";
11262
11463
  const width = Number.isFinite(columns) ? Math.max(1, Math.floor(columns)) : 80;
11263
- const rows = text.replace(/\r\n?/g, "\n").split("\n").flatMap((line) => wrapVisualLine(line, width));
11264
- 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");
11265
11469
  const tail2 = rows.slice(-Math.max(1, maxRows - 1));
11266
11470
  return maxRows === 1 ? tail2.at(-1) ?? "" : ["\u2026", ...tail2].join("\n");
11267
11471
  }
@@ -11364,6 +11568,13 @@ function previewLines(content) {
11364
11568
  return [...lines.slice(0, 6), `\u2026 ${lines.length - 6} more lines`];
11365
11569
  }
11366
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
+ }
11367
11578
  function SubagentSummary({ agents }) {
11368
11579
  const theme = useTheme();
11369
11580
  const running = agents.filter((agent) => agent.state === "running");
@@ -11372,12 +11583,14 @@ function SubagentSummary({ agents }) {
11372
11583
  "Subagents: ",
11373
11584
  running.length,
11374
11585
  " running \xB7 ",
11375
- agents.length - running.length,
11376
- " done"
11586
+ agents.filter((agent) => agent.state === "done").length,
11587
+ " done / ",
11588
+ agents.filter((agent) => agent.state !== "running" && agent.state !== "done").length,
11589
+ " stopped"
11377
11590
  ] }),
11378
11591
  running.slice(0, VISIBLE_SUBAGENTS).map((agent) => {
11379
11592
  const latest = agent.bubbles.at(-1);
11380
- 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";
11381
11594
  return /* @__PURE__ */ jsxs11(Text11, { dimColor: true, wrap: "truncate-end", children: [
11382
11595
  agent.description,
11383
11596
  " \xB7 ",
@@ -11389,23 +11602,28 @@ function SubagentSummary({ agents }) {
11389
11602
  }
11390
11603
  function SubagentView({ bubble, maxRows }) {
11391
11604
  const theme = useTheme();
11605
+ const strings = useStrings();
11392
11606
  const { columns } = useTerminalSize();
11393
11607
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
11394
- const color = bubble.state === "running" ? theme.warn : theme.ok;
11608
+ const color = bubble.state === "error" ? theme.error : bubble.state === "done" ? theme.ok : theme.warn;
11395
11609
  const latest = bubble.bubbles.at(-1);
11396
- 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 : "";
11397
11611
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginTop: 1, minWidth: 0, children: [
11398
11612
  /* @__PURE__ */ jsxs11(Text11, { color, bold: true, wrap: "truncate-end", children: [
11399
11613
  mark,
11400
11614
  " subagent: ",
11401
- bubble.description
11615
+ bubble.description,
11616
+ " - ",
11617
+ subagentStatusLabel(bubble.state, strings)
11402
11618
  ] }),
11619
+ bubble.reason && /* @__PURE__ */ jsx11(Text11, { color: theme.warn, children: bubble.reason }),
11403
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 })
11404
11621
  ] });
11405
11622
  }
11406
11623
 
11407
11624
  // src/ui/components/SubagentChat.tsx
11408
11625
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
11626
+ var MAX_SHOWN_BUBBLES = 24;
11409
11627
  function SubagentChat({
11410
11628
  bubble,
11411
11629
  rows,
@@ -11415,7 +11633,7 @@ function SubagentChat({
11415
11633
  const strings = useStrings();
11416
11634
  const [offset, setOffset] = useState7(0);
11417
11635
  const total = bubble?.bubbles.length ?? 0;
11418
- useInput2((_input, key) => {
11636
+ useTerminalInput((_input, key) => {
11419
11637
  if (key.escape) onClose();
11420
11638
  else if (key.upArrow) setOffset((value) => Math.min(Math.max(0, total - 1), value + 1));
11421
11639
  else if (key.downArrow) setOffset((value) => Math.max(0, value - 1));
@@ -11427,18 +11645,19 @@ function SubagentChat({
11427
11645
  ] });
11428
11646
  }
11429
11647
  const running = bubble.state === "running";
11430
- const shown = bubble.bubbles.slice(0, total - offset);
11431
- 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));
11432
11650
  return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", minWidth: 0, children: [
11433
- /* @__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: [
11434
11652
  running ? "\u25CC" : "\u25CF",
11435
11653
  " ",
11436
11654
  bubble.description,
11437
11655
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, bold: false, children: [
11438
11656
  " \xB7 ",
11439
- running ? strings.subagentRunning : strings.subagentDone
11657
+ subagentStatusLabel(bubble.state, strings)
11440
11658
  ] })
11441
11659
  ] }),
11660
+ bubble.reason && /* @__PURE__ */ jsx12(Text12, { color: theme.warn, wrap: "truncate-end", children: bubble.reason }),
11442
11661
  /* @__PURE__ */ jsxs12(
11443
11662
  Box12,
11444
11663
  {
@@ -11464,25 +11683,44 @@ function SubagentChat({
11464
11683
 
11465
11684
  // src/ui/components/TerminalViewport.tsx
11466
11685
  import { Box as Box13 } from "ink";
11467
- import { jsx as jsx13 } from "react/jsx-runtime";
11468
- var INTERACTIVE_CHROME_ROWS = 11;
11686
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
11687
+ var CHROME_ROWS = 16;
11469
11688
  var MAX_LIVE_ROWS = 11;
11470
11689
  var MIN_LIVE_ROWS = 4;
11471
- function liveTranscriptRows(rows) {
11690
+ var BOTTOM_SLACK_ROWS = 3;
11691
+ function liveTranscriptRows(rows, chromeRows) {
11472
11692
  if (!Number.isFinite(rows)) return MAX_LIVE_ROWS;
11473
- return Math.max(MIN_LIVE_ROWS, Math.min(MAX_LIVE_ROWS, Math.floor(rows) - INTERACTIVE_CHROME_ROWS - 2));
11693
+ const chrome = chromeRows ?? CHROME_ROWS;
11694
+ const budget = Math.floor(rows) - chrome - BOTTOM_SLACK_ROWS - 1;
11695
+ return Math.max(MIN_LIVE_ROWS, Math.min(MAX_LIVE_ROWS, budget));
11474
11696
  }
11475
11697
  function TerminalViewport({ children, rows }) {
11476
11698
  const { columns } = useTerminalSize();
11477
- return /* @__PURE__ */ jsx13(
11699
+ const liveRows = liveTranscriptRows(rows);
11700
+ const items = Array.isArray(children) ? children : [children];
11701
+ const [transcript, ...chromeChildren] = items;
11702
+ return /* @__PURE__ */ jsxs13(
11478
11703
  Box13,
11479
11704
  {
11480
- width: columns,
11705
+ width: columns - 1,
11481
11706
  maxWidth: "100%",
11482
11707
  flexDirection: "column",
11483
- maxHeight: liveTranscriptRows(rows) + INTERACTIVE_CHROME_ROWS,
11708
+ maxHeight: liveRows + CHROME_ROWS,
11484
11709
  overflowY: "hidden",
11485
- children
11710
+ children: [
11711
+ /* @__PURE__ */ jsx13(
11712
+ Box13,
11713
+ {
11714
+ flexDirection: "column",
11715
+ flexShrink: 1,
11716
+ minHeight: 0,
11717
+ maxHeight: liveRows,
11718
+ overflowY: "hidden",
11719
+ children: transcript
11720
+ }
11721
+ ),
11722
+ /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexShrink: 0, children: chromeChildren })
11723
+ ]
11486
11724
  }
11487
11725
  );
11488
11726
  }
@@ -11571,7 +11809,7 @@ function applyEvent(state, event) {
11571
11809
  return {
11572
11810
  ...state,
11573
11811
  bubbles: state.bubbles.map(
11574
- (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
11575
11813
  )
11576
11814
  };
11577
11815
  case "subagent_event": {
@@ -11603,6 +11841,10 @@ function applyEvent(state, event) {
11603
11841
  function applyEvents(state, events) {
11604
11842
  return coalesceDeltas(events).reduce(applyEvent, state);
11605
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
+ }
11606
11848
  function coalesceDeltas(events) {
11607
11849
  const out = [];
11608
11850
  for (const event of events) {
@@ -11740,7 +11982,7 @@ function safeSummary(summarize, name, input) {
11740
11982
  }
11741
11983
 
11742
11984
  // src/ui/App.tsx
11743
- import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
11985
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
11744
11986
  var EFFORTS = ["auto", "low", "medium", "high", "xhigh", "max"];
11745
11987
  var STREAM_FRAME_MS = 50;
11746
11988
  var CUSTOM_ACCENT = "__custom__";
@@ -11797,6 +12039,8 @@ function App({
11797
12039
  const [, forceRender] = useState8(0);
11798
12040
  const history = useRef5(initialHistory);
11799
12041
  const abort = useRef5(null);
12042
+ const exitArmedAt = useRef5(null);
12043
+ const [hiddenSubagents, setHiddenSubagents] = useState8(() => /* @__PURE__ */ new Set());
11800
12044
  const sessionStart = useRef5(Date.now());
11801
12045
  const turns = useRef5(0);
11802
12046
  const [turnStart, setTurnStart] = useState8(null);
@@ -11848,7 +12092,7 @@ function App({
11848
12092
  (event) => {
11849
12093
  recordSpeed(speedMeter.current, event);
11850
12094
  transcriptEvents.current.push(event);
11851
- if (event.type !== "text_delta" && event.type !== "thinking_delta") {
12095
+ if (!isStreamDelta(event)) {
11852
12096
  flushTranscriptEvents();
11853
12097
  return;
11854
12098
  }
@@ -11926,6 +12170,9 @@ function App({
11926
12170
  turns.current += 1;
11927
12171
  const hooks = {
11928
12172
  onEvent: queueTranscriptEvent,
12173
+ onHistory: (messages) => {
12174
+ history.current = messages;
12175
+ },
11929
12176
  requestPermission: (request) => new Promise((resolve3) => {
11930
12177
  let settled = false;
11931
12178
  const finish = (decision) => {
@@ -11959,10 +12206,12 @@ function App({
11959
12206
  notice("error", error instanceof Error ? error.message : String(error));
11960
12207
  }
11961
12208
  history.current = await runtime.run(history.current, hooks, controller.signal);
11962
- void runtime.persist(history.current).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
11963
12209
  } catch (error) {
11964
12210
  notice("error", error instanceof Error ? error.message : String(error));
11965
12211
  } finally {
12212
+ await runtime.persist(history.current).catch(
12213
+ (error) => notice("error", error instanceof Error ? error.message : String(error))
12214
+ );
11966
12215
  if (!controller.signal.aborted && Date.now() - startedAt >= BELL_AFTER_MS) bell();
11967
12216
  flushTranscriptEvents();
11968
12217
  abort.current = null;
@@ -12022,6 +12271,7 @@ function App({
12022
12271
  });
12023
12272
  sessionStart.current = Date.now();
12024
12273
  turns.current = 0;
12274
+ speedMeter.current = createSpeedMeter();
12025
12275
  void runtime.persist([]).catch(
12026
12276
  (error) => notice("error", error instanceof Error ? error.message : String(error))
12027
12277
  );
@@ -12665,8 +12915,24 @@ ${strings.mcpAddUsage}`
12665
12915
  return;
12666
12916
  }
12667
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
+ }
12668
12934
  const agents = transcriptRef.current.bubbles.filter(
12669
- (bubble) => bubble.kind === "subagent"
12935
+ (bubble) => bubble.kind === "subagent" && !hiddenSubagents.has(bubble.id)
12670
12936
  );
12671
12937
  if (agents.length === 0) {
12672
12938
  notice("info", strings.noSubagents);
@@ -12679,13 +12945,14 @@ ${strings.mcpAddUsage}`
12679
12945
  const running = ordered.filter((agent) => agent.state === "running");
12680
12946
  const id = running.length === 1 && rawRest === "" ? running[0].id : await pick(
12681
12947
  strings.subagentsTitle,
12682
- ordered.slice(0, 20).map((agent) => ({
12948
+ [...ordered.slice(0, 20).map((agent) => ({
12683
12949
  key: agent.id,
12684
12950
  label: agent.description || "subagent",
12685
- hint: agent.state === "running" ? strings.subagentRunning : strings.subagentDone
12686
- }))
12951
+ hint: subagentStatusLabel(agent.state, strings)
12952
+ })), ...ordered.some((agent) => agent.state !== "running") ? [{ key: "__clear_finished__", label: strings.subagentsClear, hint: "/subagents clear" }] : []]
12687
12953
  );
12688
- if (id) setOverlay({ kind: "subagent", id });
12954
+ if (id === "__clear_finished__") clearFinished();
12955
+ else if (id) setOverlay({ kind: "subagent", id });
12689
12956
  return;
12690
12957
  }
12691
12958
  case "checker":
@@ -12758,6 +13025,61 @@ ${strings.mcpAddUsage}`
12758
13025
  notice("info", runtime.readMemory() || strings.memoryEmpty);
12759
13026
  return;
12760
13027
  }
13028
+ case "systemp": {
13029
+ const sub = rest[0]?.toLowerCase();
13030
+ if (sub === "set" || sub === "load") {
13031
+ const argument = rawRest.replace(/^\S+\s*/, "").trim();
13032
+ if (!argument && sub === "load") {
13033
+ notice("warn", strings.systemPromptUsage);
13034
+ return;
13035
+ }
13036
+ try {
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
+ }
13044
+ await runtime.setSystemPrompt(text);
13045
+ notice("info", strings.systemPromptSet);
13046
+ } catch (error) {
13047
+ notice("error", error instanceof Error ? error.message : String(error));
13048
+ }
13049
+ return;
13050
+ }
13051
+ if (sub === "clear") {
13052
+ if (!runtime.getSystemPrompt()) {
13053
+ notice("info", strings.systemPromptEmpty);
13054
+ return;
13055
+ }
13056
+ await runtime.setSystemPrompt("");
13057
+ notice("info", strings.systemPromptCleared);
13058
+ return;
13059
+ }
13060
+ if (sub === "on") {
13061
+ await runtime.setSystemPromptEnabled(true);
13062
+ notice("info", strings.systemPromptEnabled);
13063
+ return;
13064
+ }
13065
+ if (sub === "off") {
13066
+ await runtime.setSystemPromptEnabled(false);
13067
+ notice("info", strings.systemPromptDisabled);
13068
+ return;
13069
+ }
13070
+ if (sub !== void 0 && sub !== "" && sub !== "show") {
13071
+ notice("warn", strings.systemPromptUsage);
13072
+ return;
13073
+ }
13074
+ const current = runtime.getSystemPrompt();
13075
+ if (!current) {
13076
+ notice("info", strings.systemPromptEmpty);
13077
+ return;
13078
+ }
13079
+ const enabled = runtime.isSystemPromptEnabled();
13080
+ notice("info", (enabled ? "" : "[disabled] ") + current);
13081
+ return;
13082
+ }
12761
13083
  case "effort": {
12762
13084
  notice("info", runtime.effortDescription(history.current));
12763
13085
  const choice = EFFORTS.includes(rawRest) ? rawRest : await pick(
@@ -12770,6 +13092,21 @@ ${strings.mcpAddUsage}`
12770
13092
  forceRender((n) => n + 1);
12771
13093
  return;
12772
13094
  }
13095
+ case "steps": {
13096
+ const current = runtime.getMaxTurnSteps();
13097
+ if (!rest[0]) {
13098
+ notice("info", strings.stepsCurrent(current));
13099
+ return;
13100
+ }
13101
+ const parsed = Number(rest[0]);
13102
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1e3) {
13103
+ notice("warn", strings.stepsInvalid);
13104
+ return;
13105
+ }
13106
+ await runtime.setMaxTurnSteps(parsed);
13107
+ notice("info", parsed === 0 ? strings.stepsUnlimited : strings.stepsSet(parsed));
13108
+ return;
13109
+ }
12773
13110
  case "budget": {
12774
13111
  const current = runtime.getMaxTokensPerTurn();
12775
13112
  if (rest[0]) {
@@ -12954,7 +13291,7 @@ ${strings.mcpAddUsage}`
12954
13291
  }
12955
13292
  }
12956
13293
  },
12957
- [applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings]
13294
+ [applyAccent, appendAttachment, ask2, exit, notice, pick, replaceAttachments, runtime, strings, hiddenSubagents, flushTranscriptEvents]
12958
13295
  );
12959
13296
  const runSlash = useCallback(
12960
13297
  (line) => {
@@ -13123,6 +13460,7 @@ ${strings.mcpAddUsage}`
13123
13460
  }
13124
13461
  const text = raw.trim();
13125
13462
  const submitSlash = () => {
13463
+ setPromptHistory((state) => appendInputHistory(state, text));
13126
13464
  if (!detachedInput) setInput("");
13127
13465
  const command = text.slice(1).trim().split(/\s+/)[0]?.toLowerCase();
13128
13466
  if (busyRef.current && !commandRunsWhileBusy(command) || slashPendingRef.current > 0) {
@@ -13184,6 +13522,18 @@ ${strings.mcpAddUsage}`
13184
13522
  [replaceAttachments, runAgent, runSlash, tryQueueAutomaticAttachment]
13185
13523
  );
13186
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;
13187
13537
  if (key.tab && key.shift) {
13188
13538
  const next = runtime.cycleMode();
13189
13539
  notice("info", strings.modeChanged(strings.modeLabel[next] ?? next));
@@ -13191,7 +13541,7 @@ ${strings.mcpAddUsage}`
13191
13541
  return;
13192
13542
  }
13193
13543
  if (!key.escape) return;
13194
- if (overlay.kind === "subagent") return;
13544
+ if (overlay.kind !== "none") return;
13195
13545
  if (checkedOut.current && overlay.kind === "none") {
13196
13546
  if (busyRef.current || slashPendingRef.current > 0) {
13197
13547
  putBackCheckedOut(input);
@@ -13217,21 +13567,6 @@ ${strings.mcpAddUsage}`
13217
13567
  if (clipboardPasteTask.current) clipboardPasteTask.current = null;
13218
13568
  return;
13219
13569
  }
13220
- if (overlay.kind === "permission") {
13221
- overlay.resolve("deny");
13222
- setOverlay({ kind: "none" });
13223
- return;
13224
- }
13225
- if (overlay.kind === "confirm") {
13226
- overlay.resolve(false);
13227
- setOverlay({ kind: "none" });
13228
- return;
13229
- }
13230
- if (overlay.kind === "picker") {
13231
- overlay.select("");
13232
- setOverlay({ kind: "none" });
13233
- return;
13234
- }
13235
13570
  setInput("");
13236
13571
  });
13237
13572
  const shell = (children) => /* @__PURE__ */ jsx14(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx14(StringsContext.Provider, { value: strings, children }) });
@@ -13259,13 +13594,16 @@ ${strings.mcpAddUsage}`
13259
13594
  notice("info", `${summary2}
13260
13595
  ${strings.configAt(runtime.configPath())}`);
13261
13596
  return summary2;
13597
+ },
13598
+ onCancel: () => {
13599
+ if (!runtime.needsSetup()) setSetup(false);
13262
13600
  }
13263
13601
  }
13264
13602
  )
13265
13603
  );
13266
13604
  }
13267
13605
  return shell(
13268
- /* @__PURE__ */ jsxs13(TerminalViewport, { rows, children: [
13606
+ /* @__PURE__ */ jsxs14(TerminalViewport, { rows, children: [
13269
13607
  /* @__PURE__ */ jsx14(
13270
13608
  Transcript,
13271
13609
  {
@@ -13398,7 +13736,7 @@ function TextInputOverlay({
13398
13736
  useInput3((_input, key) => {
13399
13737
  if (key.escape) onCancel();
13400
13738
  });
13401
- return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", children: [
13739
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
13402
13740
  /* @__PURE__ */ jsx14(Text13, { dimColor: true, children: title }),
13403
13741
  /* @__PURE__ */ jsx14(
13404
13742
  TextInput2,
@@ -13518,10 +13856,16 @@ function clearTerminal() {
13518
13856
  if (process.stdout.isTTY) process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
13519
13857
  }
13520
13858
  var TUI_RENDER_OPTIONS = {
13521
- // Keep unchanged streamed lines in place instead of clearing and redrawing
13522
- // the whole live region on every frame.
13523
- incrementalRendering: true,
13524
- maxFps: 30
13859
+ // Incremental rendering is off: Ink's incremental diff races with its own
13860
+ // <Static> commits and leaves stale rows behind (the prompt frame appears
13861
+ // twice). Standard rendering clears the whole frame each time — a little
13862
+ // more work, but no stale lines.
13863
+ incrementalRendering: false,
13864
+ exitOnCtrlC: false,
13865
+ // Throttling defers writes; when a bubble closes into <Static> mid-stream,
13866
+ // the deferred frame and the static write race and leave stale rows behind.
13867
+ // Unthrottled renders cost a little CPU but keep every frame consistent.
13868
+ maxFps: 0
13525
13869
  };
13526
13870
  async function startTui(options) {
13527
13871
  const { runtime, history, warnings, shutdown } = await boot({