@kernelonpanic/kitcode 1.2.6 → 1.2.9

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
@@ -62,7 +62,7 @@ var themeSchema = z.object({
62
62
  var langSchema = z.enum(["en", "ru"]);
63
63
  var budgetSchema = z.object({
64
64
  maxTokensPerTurn: z.number().int().min(0).max(1e7).default(1e7),
65
- maxCostUsdPerTurn: z.number().positive().max(1e3).default(5),
65
+ maxCostUsdPerTurn: z.number().nonnegative().max(1e3).default(0),
66
66
  maxSubagentsPerTurn: z.number().int().min(0).max(16).default(3)
67
67
  });
68
68
  var diagnosticsSchema = z.object({
@@ -79,7 +79,7 @@ var configSchema = z.object({
79
79
  maxTokens: z.number().int().positive().max(2e5).default(64e3),
80
80
  budget: budgetSchema.default({
81
81
  maxTokensPerTurn: 1e7,
82
- maxCostUsdPerTurn: 5,
82
+ maxCostUsdPerTurn: 0,
83
83
  maxSubagentsPerTurn: 3
84
84
  }),
85
85
  diagnostics: diagnosticsSchema.default({ autoRun: true, commands: [] }),
@@ -999,6 +999,14 @@ function sanitizeTerminalText(value) {
999
999
  }
1000
1000
  return output;
1001
1001
  }
1002
+ function sanitizeThinkingText(text, streaming = false) {
1003
+ const clean = sanitizeTerminalText(text).replace(/<\/?think(?:ing)?>/gi, "");
1004
+ if (!streaming) return clean;
1005
+ const start = clean.lastIndexOf("<");
1006
+ if (start === -1) return clean;
1007
+ const suffix = clean.slice(start).toLowerCase();
1008
+ return ["<think>", "</think>", "<thinking>", "</thinking>"].some((tag) => tag.startsWith(suffix)) ? clean.slice(0, start) : clean;
1009
+ }
1002
1010
  function skipCsi(text, from) {
1003
1011
  for (let index = from; index < text.length; index += 1) {
1004
1012
  const code = text.charCodeAt(index);
@@ -1422,6 +1430,10 @@ async function runTurn(cfg, history, hooks, signal) {
1422
1430
  let pauses = 0;
1423
1431
  let steps = 0;
1424
1432
  for (; ; ) {
1433
+ if (signal.aborted) {
1434
+ hooks.onEvent({ type: "turn_end", stopReason: "aborted" });
1435
+ return sanitizeHistory(messages);
1436
+ }
1425
1437
  if (++steps > MAX_TURN_STEPS) {
1426
1438
  hooks.onEvent({
1427
1439
  type: "notice",
@@ -1508,12 +1520,16 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
1508
1520
  signal
1509
1521
  };
1510
1522
  let outcome;
1523
+ let text = "";
1524
+ let thinking = "";
1511
1525
  for await (const event of streamWithRetry(cfg.provider, request, cfg.modelRef, signal)) {
1512
1526
  switch (event.type) {
1513
1527
  case "text_delta":
1528
+ text += event.text;
1514
1529
  hooks.onEvent({ type: "text_delta", text: event.text });
1515
1530
  break;
1516
1531
  case "thinking_delta":
1532
+ thinking += event.text;
1517
1533
  hooks.onEvent({ type: "thinking_delta", text: event.text });
1518
1534
  break;
1519
1535
  case "usage":
@@ -1532,7 +1548,12 @@ async function consumeStream(cfg, messages, hooks, signal, maxTokens) {
1532
1548
  }
1533
1549
  }
1534
1550
  if (outcome) return outcome;
1535
- if (signal.aborted) return { content: [], stopReason: "aborted" };
1551
+ if (signal.aborted) {
1552
+ const content = [];
1553
+ if (thinking) content.push({ type: "thinking", text: thinking });
1554
+ if (text) content.push({ type: "text", text });
1555
+ return { content, stopReason: "aborted" };
1556
+ }
1536
1557
  throw new Error(
1537
1558
  `"${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.`
1538
1559
  );
@@ -1542,8 +1563,29 @@ async function* streamWithRetry(provider, request, modelRef, signal) {
1542
1563
  for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
1543
1564
  if (signal.aborted) return;
1544
1565
  try {
1545
- for await (const event of provider.stream(request)) {
1546
- yield event;
1566
+ const stream = provider.stream(request)[Symbol.asyncIterator]();
1567
+ let cancel;
1568
+ let cancelTimer;
1569
+ const cancelled = new Promise((resolve3) => {
1570
+ cancel = () => {
1571
+ cancelTimer ??= setTimeout(() => resolve3({ done: true, value: void 0 }), 100);
1572
+ };
1573
+ signal.addEventListener("abort", cancel, { once: true });
1574
+ if (signal.aborted) cancel();
1575
+ });
1576
+ try {
1577
+ for (; ; ) {
1578
+ const next = await Promise.race([stream.next(), cancelled]);
1579
+ if (next.done) break;
1580
+ if (!signal.aborted || next.value.type === "usage" || next.value.type === "done") {
1581
+ yield next.value;
1582
+ }
1583
+ if (next.value.type === "done") break;
1584
+ }
1585
+ } finally {
1586
+ signal.removeEventListener("abort", cancel);
1587
+ if (cancelTimer !== void 0) clearTimeout(cancelTimer);
1588
+ void stream.return?.().catch(() => void 0);
1547
1589
  }
1548
1590
  return;
1549
1591
  } catch (error) {
@@ -1551,7 +1593,12 @@ async function* streamWithRetry(provider, request, modelRef, signal) {
1551
1593
  if (attempt >= MAX_RETRIES2) break;
1552
1594
  const waitMs = retryBackoffMs(error, attempt);
1553
1595
  if (waitMs > 0) {
1554
- await sleep(waitMs, signal);
1596
+ try {
1597
+ await sleep(waitMs, signal);
1598
+ } catch (error2) {
1599
+ if (signal.aborted) return;
1600
+ throw error2;
1601
+ }
1555
1602
  continue;
1556
1603
  }
1557
1604
  break;
@@ -1762,7 +1809,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1762
1809
  reason: `Turn stopped at the token budget (${limits.maxTokensPerTurn.toLocaleString()} tokens). This is a safety limit to prevent runaway costs. To disable it, run: /budget 0`
1763
1810
  };
1764
1811
  }
1765
- if (current.costUsd !== null && current.costUsd >= limits.maxCostUsdPerTurn) {
1812
+ if (limits.maxCostUsdPerTurn > 0 && current.costUsd !== null && current.costUsd >= limits.maxCostUsdPerTurn) {
1766
1813
  return {
1767
1814
  allowed: false,
1768
1815
  reason: `Turn stopped at the cost budget ($${limits.maxCostUsdPerTurn.toFixed(2)}). Send another message to continue.`
@@ -1781,7 +1828,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1781
1828
  };
1782
1829
  }
1783
1830
  const pricing = resolvePricing(request.modelRef);
1784
- if (current.costUsd !== null) {
1831
+ if (limits.maxCostUsdPerTurn > 0 && current.costUsd !== null) {
1785
1832
  const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
1786
1833
  const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
1787
1834
  const inputUsd = estimatedInput * effectivePricing.input / 1e6;
@@ -3258,11 +3305,25 @@ function createSubagentRunner(makeConfig, tools) {
3258
3305
  return {
3259
3306
  async run(request) {
3260
3307
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
3308
+ let steps = 0;
3261
3309
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
3262
3310
  const cfg = {
3263
3311
  ...base,
3312
+ provider: {
3313
+ id: base.provider.id,
3314
+ kind: base.provider.kind,
3315
+ listModels: () => base.provider.listModels(),
3316
+ knownModels: () => base.provider.knownModels(),
3317
+ stream: (chat) => base.provider.stream(steps === MAX_SUBAGENT_STEPS ? {
3318
+ ...chat,
3319
+ tools: [],
3320
+ system: `${chat.system}
3321
+
3322
+ This is your last model call. Do not call tools. Return your findings now, clearly stating any unfinished work.`
3323
+ } : chat)
3324
+ },
3264
3325
  tools: {
3265
- get: (name) => name === TASK_TOOL_NAME ? void 0 : base.tools.get(name),
3326
+ get: (name) => name === TASK_TOOL_NAME || steps >= MAX_SUBAGENT_STEPS ? void 0 : base.tools.get(name),
3266
3327
  schemas: () => base.tools.schemas().filter((schema) => schema.name !== TASK_TOOL_NAME)
3267
3328
  },
3268
3329
  system: `${SUBAGENT_SYSTEM}
@@ -3271,7 +3332,6 @@ Working directory: ${base.cwd}`
3271
3332
  };
3272
3333
  const limit = new AbortController();
3273
3334
  const signal = AbortSignal.any([request.signal, limit.signal]);
3274
- let steps = 0;
3275
3335
  const hooks = {
3276
3336
  onEvent(event) {
3277
3337
  if (event.type === "turn_start" && ++steps > MAX_SUBAGENT_STEPS) limit.abort();
@@ -3295,6 +3355,7 @@ Working directory: ${base.cwd}`
3295
3355
  ];
3296
3356
  const text = finalText(await runTurn(cfg, history, hooks, signal));
3297
3357
  if (!limit.signal.aborted) return text;
3358
+ if (request.signal.aborted && text === SUBAGENT_NO_ANSWER) return SUBAGENT_CANCELLED;
3298
3359
  return text === SUBAGENT_NO_ANSWER ? STEP_LIMIT_NOTE : `${text}
3299
3360
 
3300
3361
  ${STEP_LIMIT_NOTE}`;
@@ -3472,7 +3533,7 @@ function clip(value) {
3472
3533
  // package.json
3473
3534
  var package_default = {
3474
3535
  name: "@kernelonpanic/kitcode",
3475
- version: "1.2.6",
3536
+ version: "1.2.9",
3476
3537
  description: "Terminal coding agent with a config you never have to write by hand",
3477
3538
  type: "module",
3478
3539
  license: "MIT",
@@ -3540,7 +3601,7 @@ var package_default = {
3540
3601
 
3541
3602
  // src/version.ts
3542
3603
  var KITCODE_VERSION = package_default.version;
3543
- var KITCODE_COMMIT = true ? "f40cc2cb3cebf7fa8b3c20dbbff5b14047d1e983" : "development";
3604
+ var KITCODE_COMMIT = true ? "cc499fc443e73408f49b928f204b77c48e9d2d05" : "development";
3544
3605
 
3545
3606
  // src/mcp/client.ts
3546
3607
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3928,7 +3989,7 @@ async function limitedResponseText(response, limit) {
3928
3989
  import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3929
3990
  import path8 from "path";
3930
3991
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3931
- var CACHE_VERSION = 2;
3992
+ var CACHE_VERSION = 3;
3932
3993
  async function loadModels(provider, refresh = false) {
3933
3994
  const file = cacheFile(provider.id);
3934
3995
  const cached = await readCache(file);
@@ -4155,13 +4216,15 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4155
4216
  final = await stream.finalMessage();
4156
4217
  } catch (error) {
4157
4218
  if (isUserAbort(error, req.signal)) {
4158
- const partial = stream.currentMessage;
4159
- if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4219
+ const partial2 = stream.currentMessage;
4220
+ if (partial2) yield { type: "usage", usage: toUsage(partial2.usage) };
4160
4221
  const limits3 = parseRateLimits(capture.headers());
4161
4222
  if (limits3) yield { type: "rate_limits", limits: limits3 };
4162
4223
  yield { type: "done", stopReason: "aborted", content: partialContent(thinking, text) };
4163
4224
  return;
4164
4225
  }
4226
+ const partial = stream.currentMessage;
4227
+ if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4165
4228
  const limits2 = parseRateLimits(capture.headers());
4166
4229
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4167
4230
  if (events === 0 && capture.succeeded() && !isConnectionFailure(error)) {
@@ -4195,7 +4258,7 @@ async function listModels(client, providerId, apiKey2) {
4195
4258
  name: model.display_name,
4196
4259
  contextWindow: model.max_input_tokens ?? void 0,
4197
4260
  maxOutput: model.max_tokens ?? void 0,
4198
- pricing: pricingFor(model.id)
4261
+ pricing: modelInfoFromRaw(model)?.pricing ?? pricingFor(model.id)
4199
4262
  });
4200
4263
  }
4201
4264
  return models;
@@ -4355,6 +4418,7 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4355
4418
  }
4356
4419
  } catch (error) {
4357
4420
  if (!isUserAbort(error, req.signal)) {
4421
+ if (sawUsage) yield { type: "usage", usage };
4358
4422
  const limits2 = parseRateLimits(capture.headers());
4359
4423
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4360
4424
  throw toProviderError(error, providerId, [apiKey2]);
@@ -4370,13 +4434,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4370
4434
  return;
4371
4435
  }
4372
4436
  if (recognised === 0) throw await invalidStreamError(providerId, capture, void 0, [apiKey2]);
4437
+ if (sawUsage) yield { type: "usage", usage };
4373
4438
  const content = toContentBlocks2(providerId, thinking, text, calls);
4374
4439
  for (const block of content) {
4375
4440
  if (block.type === "tool_use") {
4376
4441
  yield { type: "tool_call", id: block.id, name: block.name, input: block.input };
4377
4442
  }
4378
4443
  }
4379
- if (sawUsage) yield { type: "usage", usage };
4380
4444
  const limits = parseRateLimits(capture.headers());
4381
4445
  if (limits) yield { type: "rate_limits", limits };
4382
4446
  yield { type: "done", stopReason: resolveStop(finishReason, calls.size > 0), content };
@@ -6505,6 +6569,7 @@ async function boot(options) {
6505
6569
  async setMaxTokensPerTurn(tokens3) {
6506
6570
  await persistConfig((draft) => {
6507
6571
  draft.budget.maxTokensPerTurn = tokens3;
6572
+ draft.budget.maxCostUsdPerTurn = 0;
6508
6573
  });
6509
6574
  },
6510
6575
  getLang: () => config.lang,
@@ -7037,8 +7102,48 @@ function validateKey(value) {
7037
7102
  import { render } from "ink";
7038
7103
 
7039
7104
  // src/ui/App.tsx
7040
- import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
7041
- import { useCallback, useEffect as useEffect2, useMemo as useMemo4, useRef as useRef4, useState as useState5 } from "react";
7105
+ import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useStdout as useStdout2 } from "ink";
7106
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef4, useState as useState5 } from "react";
7107
+
7108
+ // src/ui/terminal-size.ts
7109
+ import { useStdout } from "ink";
7110
+ import { useMemo, useSyncExternalStore } from "react";
7111
+ var stores = /* @__PURE__ */ new WeakMap();
7112
+ function createStore(stdout) {
7113
+ let size = { columns: stdout.columns || 80, rows: stdout.rows || 24 };
7114
+ const listeners = /* @__PURE__ */ new Set();
7115
+ const update = () => {
7116
+ const columns = stdout.columns || 80;
7117
+ const rows = stdout.rows || 24;
7118
+ if (columns === size.columns && rows === size.rows) return;
7119
+ size = { columns, rows };
7120
+ for (const listener of listeners) listener();
7121
+ };
7122
+ return {
7123
+ getSnapshot: () => size,
7124
+ subscribe(listener) {
7125
+ if (listeners.size === 0) stdout.on("resize", update);
7126
+ listeners.add(listener);
7127
+ update();
7128
+ return () => {
7129
+ listeners.delete(listener);
7130
+ if (listeners.size === 0) stdout.off("resize", update);
7131
+ };
7132
+ }
7133
+ };
7134
+ }
7135
+ function useTerminalSize() {
7136
+ const { stdout } = useStdout();
7137
+ const store = useMemo(() => {
7138
+ let current = stores.get(stdout);
7139
+ if (!current) {
7140
+ current = createStore(stdout);
7141
+ stores.set(stdout, current);
7142
+ }
7143
+ return current;
7144
+ }, [stdout]);
7145
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
7146
+ }
7042
7147
 
7043
7148
  // src/mcp/add.ts
7044
7149
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
@@ -7665,7 +7770,7 @@ import { Box as Box3, Text as Text3 } from "ink";
7665
7770
  import { useState } from "react";
7666
7771
 
7667
7772
  // src/ui/components/Logo.tsx
7668
- import { Box as Box2, Text as Text2, useWindowSize } from "ink";
7773
+ import { Box as Box2, Text as Text2 } from "ink";
7669
7774
 
7670
7775
  // src/ui/workspace.ts
7671
7776
  import { homedir } from "os";
@@ -7712,7 +7817,7 @@ var CAT = [" \u2571\\_\u2571\\ ", " ( o.o )", " > ^ < "];
7712
7817
  function Logo({ subtitle, workspace }) {
7713
7818
  const theme = useTheme();
7714
7819
  const strings = useStrings();
7715
- const { columns } = useWindowSize();
7820
+ const { columns } = useTerminalSize();
7716
7821
  const location = workspace ? formatWorkspacePath(workspace, Math.max(6, Math.min(72, columns - 15))) : "";
7717
7822
  return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
7718
7823
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
@@ -7970,15 +8075,16 @@ function previewText(value, limit = MAX_PREVIEW_CHARS) {
7970
8075
 
7971
8076
  // src/ui/components/Picker.tsx
7972
8077
  import { Box as Box7, Text as Text7 } from "ink";
7973
- import { useMemo, useState as useState3 } from "react";
8078
+ import { useMemo as useMemo2, useState as useState3 } from "react";
7974
8079
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7975
8080
  var WINDOW = 10;
7976
8081
  function Picker({ title, items, onSelect, onCancel }) {
7977
8082
  const theme = useTheme();
8083
+ const { columns } = useTerminalSize();
7978
8084
  const strings = useStrings();
7979
8085
  const [query, setQuery] = useState3("");
7980
8086
  const [cursor, setCursor] = useState3(0);
7981
- const matches = useMemo(() => {
8087
+ const matches = useMemo2(() => {
7982
8088
  const needle = query.toLowerCase();
7983
8089
  if (needle === "") return items;
7984
8090
  return items.filter(
@@ -8005,7 +8111,7 @@ function Picker({ title, items, onSelect, onCancel }) {
8005
8111
  });
8006
8112
  const start = Math.max(0, Math.min(active2 - WINDOW + 2, matches.length - WINDOW));
8007
8113
  const visible = matches.slice(start, start + WINDOW);
8008
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
8114
+ return /* @__PURE__ */ jsxs7(Box7, { width: columns, maxWidth: "100%", flexShrink: 0, flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
8009
8115
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accent, children: [
8010
8116
  sanitizeTerminalText(title),
8011
8117
  query !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -8015,7 +8121,7 @@ function Picker({ title, items, onSelect, onCancel }) {
8015
8121
  ] }),
8016
8122
  matches.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: strings.noMatches }) : visible.map((item, index) => {
8017
8123
  const selected = start + index === active2;
8018
- return /* @__PURE__ */ jsxs7(Text7, { color: selected ? theme.accent : void 0, inverse: selected, children: [
8124
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate-end", color: selected ? theme.accent : void 0, inverse: selected, children: [
8019
8125
  truncate2(sanitizeTerminalText(item.label), 70),
8020
8126
  item.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
8021
8127
  " ",
@@ -8071,6 +8177,7 @@ var PromptInput = memo(function PromptInput2({
8071
8177
  attachments = []
8072
8178
  }) {
8073
8179
  const theme = useTheme();
8180
+ const { columns } = useTerminalSize();
8074
8181
  const strings = useStrings();
8075
8182
  const safeValue = sanitizeTerminalText(value);
8076
8183
  const [selectionCursor, setSelectionCursor] = useState4(0);
@@ -8206,7 +8313,7 @@ var PromptInput = memo(function PromptInput2({
8206
8313
  });
8207
8314
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8208
8315
  const visible = suggestions.slice(start, start + WINDOW2);
8209
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8316
+ return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8210
8317
  /* @__PURE__ */ jsxs8(
8211
8318
  Box8,
8212
8319
  {
@@ -8217,7 +8324,7 @@ var PromptInput = memo(function PromptInput2({
8217
8324
  paddingX: 1,
8218
8325
  children: [
8219
8326
  /* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
8220
- /* @__PURE__ */ jsx8(EditableText, { value: safeValue, cursor: inputCursor, placeholder: strings.placeholder }),
8327
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx8(EditableText, { value: safeValue, cursor: inputCursor, placeholder: strings.placeholder }) }),
8221
8328
  pending && pending > 0 ? /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
8222
8329
  " \xB7 ",
8223
8330
  strings.queued(pending)
@@ -8510,9 +8617,12 @@ function liveTranscriptRows(rows) {
8510
8617
  return Math.max(1, interactiveViewportRows(rows) - INTERACTIVE_CHROME_ROWS);
8511
8618
  }
8512
8619
  function TerminalViewport({ children, rows }) {
8620
+ const { columns } = useTerminalSize();
8513
8621
  return /* @__PURE__ */ jsx10(
8514
8622
  Box10,
8515
8623
  {
8624
+ width: columns,
8625
+ maxWidth: "100%",
8516
8626
  flexDirection: "column",
8517
8627
  maxHeight: interactiveViewportRows(rows),
8518
8628
  overflowY: "hidden",
@@ -8522,18 +8632,18 @@ function TerminalViewport({ children, rows }) {
8522
8632
  }
8523
8633
 
8524
8634
  // src/ui/components/Transcript.tsx
8525
- import { Box as Box12, Static, Text as Text11, useWindowSize as useWindowSize3 } from "ink";
8526
- import { memo as memo2, useMemo as useMemo3, useRef as useRef3 } from "react";
8635
+ import { Box as Box12, Static, Text as Text11 } from "ink";
8636
+ import { memo as memo2, useMemo as useMemo4, useRef as useRef3 } from "react";
8527
8637
  import Spinner2 from "ink-spinner";
8528
8638
  import stringWidth2 from "string-width";
8529
8639
 
8530
8640
  // src/ui/markdown.tsx
8531
- import { Box as Box11, Text as Text10, useWindowSize as useWindowSize2 } from "ink";
8532
- import { Fragment as Fragment4, useMemo as useMemo2 } from "react";
8641
+ import { Box as Box11, Text as Text10 } from "ink";
8642
+ import { Fragment as Fragment4, useMemo as useMemo3 } from "react";
8533
8643
  import stringWidth from "string-width";
8534
8644
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8535
8645
  function Markdown({ children }) {
8536
- const blocks = useMemo2(() => extractBlocks(children), [children]);
8646
+ const blocks = useMemo3(() => extractBlocks(children), [children]);
8537
8647
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
8538
8648
  }
8539
8649
  function BlockView({ block }) {
@@ -8567,7 +8677,7 @@ function BlockView({ block }) {
8567
8677
  case "code":
8568
8678
  return /* @__PURE__ */ jsx11(CodeBlock, { code: block.text ?? "" });
8569
8679
  case "hr":
8570
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2500".repeat(60) }) });
8680
+ return /* @__PURE__ */ jsx11(Box11, { height: 1 });
8571
8681
  }
8572
8682
  }
8573
8683
  function CodeBlock({ code }) {
@@ -8575,7 +8685,7 @@ function CodeBlock({ code }) {
8575
8685
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) });
8576
8686
  }
8577
8687
  function TableView({ block }) {
8578
- const { columns } = useWindowSize2();
8688
+ const { columns } = useTerminalSize();
8579
8689
  const headers = block.headers ?? [];
8580
8690
  const rows = block.rows ?? [];
8581
8691
  if (headers.length === 0 && rows.length === 0) {
@@ -9148,26 +9258,41 @@ var BubbleView = memo2(function BubbleView2({
9148
9258
  return /* @__PURE__ */ jsx12(AssistantView, { bubble, maxRows });
9149
9259
  }
9150
9260
  if (bubble.kind === "subagent") {
9151
- return /* @__PURE__ */ jsx12(SubagentView, { bubble });
9261
+ return /* @__PURE__ */ jsx12(SubagentView, { bubble, maxRows });
9152
9262
  }
9153
9263
  return /* @__PURE__ */ jsx12(ToolView, { bubble });
9154
9264
  });
9155
- function AssistantView({ bubble, maxRows }) {
9156
- const { columns } = useWindowSize3();
9265
+ function splitThinkingPrefix(text, streaming) {
9266
+ const opening = /^\s*<think(?:ing)?>/i.exec(text);
9267
+ if (!opening) {
9268
+ const prefix = text.trimStart().toLowerCase();
9269
+ return { thinking: "", text: streaming && prefix !== "" && ["<think>", "<thinking>"].some((tag) => tag.startsWith(prefix)) ? "" : text };
9270
+ }
9271
+ const rest = text.slice(opening[0].length);
9272
+ const closing = /<\/think(?:ing)?>/i.exec(rest);
9273
+ return closing ? { thinking: sanitizeThinkingText(rest.slice(0, closing.index)), text: rest.slice(closing.index + closing[0].length) } : { thinking: sanitizeThinkingText(rest, streaming), text: "" };
9274
+ }
9275
+ function AssistantView({ bubble: source, maxRows }) {
9276
+ const tagged = splitThinkingPrefix(source.text, source.streaming);
9277
+ const bubble = { ...source, text: tagged.text, thinking: [source.thinking, tagged.thinking].filter(Boolean).join("\n") };
9278
+ const { columns } = useTerminalSize();
9157
9279
  const frozenThinking = useRef3(void 0);
9158
9280
  const answering = bubble.streaming && bubble.text !== "";
9159
9281
  if (!answering) frozenThinking.current = void 0;
9160
9282
  if (answering && frozenThinking.current === void 0) {
9161
9283
  frozenThinking.current = bubble.thinking;
9162
9284
  }
9163
- const visibleThinking = assistantThinkingForFrame(bubble, frozenThinking.current);
9285
+ const visibleThinking = sanitizeThinkingText(
9286
+ assistantThinkingForFrame(bubble, frozenThinking.current),
9287
+ bubble.streaming
9288
+ );
9164
9289
  const liveBudget = bubble.streaming && maxRows !== void 0 ? Math.max(1, maxRows - 1) : void 0;
9165
9290
  const thinkingBudget = liveBudget === void 0 ? void 0 : bubble.text !== "" ? Math.min(3, Math.max(0, liveBudget - 1)) : Math.max(0, liveBudget - 1);
9166
9291
  const frameThinking = thinkingBudget === void 0 ? visibleThinking.trim() : clipTextToRows(visibleThinking.trim(), thinkingBudget, columns);
9167
9292
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9168
9293
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9169
9294
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9170
- frameThinking !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: frameThinking }),
9295
+ frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { children: frameThinking }) }),
9171
9296
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9172
9297
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9173
9298
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9206,7 +9331,7 @@ function ToolView({ bubble }) {
9206
9331
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
9207
9332
  const color = bubble.state === "error" ? theme.error : bubble.state === "ok" ? theme.ok : theme.warn;
9208
9333
  const display = bubble.display;
9209
- const diff = useMemo3(
9334
+ const diff = useMemo4(
9210
9335
  () => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
9211
9336
  [display]
9212
9337
  );
@@ -9244,20 +9369,20 @@ function previewLines(content) {
9244
9369
  if (lines.length <= 6) return lines;
9245
9370
  return [...lines.slice(0, 6), `\u2026 ${lines.length - 6} more lines`];
9246
9371
  }
9247
- function SubagentView({ bubble }) {
9372
+ function SubagentView({ bubble, maxRows }) {
9248
9373
  const theme = useTheme();
9374
+ const { columns } = useTerminalSize();
9249
9375
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
9250
9376
  const color = bubble.state === "running" ? theme.warn : theme.ok;
9251
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9252
- /* @__PURE__ */ jsxs11(Text11, { color, bold: true, children: [
9377
+ const latest = bubble.bubbles.at(-1);
9378
+ const progress = latest?.kind === "assistant" ? sanitizeThinkingText(latest.text || latest.thinking, latest.streaming) : latest?.kind === "tool" ? latest.summary : "";
9379
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, minWidth: 0, children: [
9380
+ /* @__PURE__ */ jsxs11(Text11, { color, bold: true, wrap: "truncate-end", children: [
9253
9381
  mark,
9254
9382
  " subagent: ",
9255
9383
  bubble.description
9256
9384
  ] }),
9257
- /* @__PURE__ */ jsxs11(Box12, { marginLeft: 2, flexDirection: "column", children: [
9258
- bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx12(BubbleView, { bubble: inner }, index)),
9259
- bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
9260
- ] })
9385
+ /* @__PURE__ */ jsx12(Box12, { marginLeft: 2, flexDirection: "column", minWidth: 0, children: bubble.state === "running" ? /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: Math.max(1, (maxRows ?? 6) - 2), overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { children: clipTextToRows(progress, Math.max(1, (maxRows ?? 6) - 2), Math.max(1, columns - 2)) }) }) : bubble.result ? /* @__PURE__ */ jsx12(Markdown, { children: sanitizeThinkingText(bubble.result) }) : null })
9261
9386
  ] });
9262
9387
  }
9263
9388
 
@@ -9507,8 +9632,9 @@ function App({
9507
9632
  initialHistory,
9508
9633
  warnings = []
9509
9634
  }) {
9510
- const { exit } = useApp();
9511
- const { rows } = useWindowSize4();
9635
+ const { exit, suspendTerminal } = useApp();
9636
+ const { stdout } = useStdout2();
9637
+ const { rows } = useTerminalSize();
9512
9638
  const [transcript, setTranscript] = useState5(
9513
9639
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
9514
9640
  );
@@ -9526,6 +9652,7 @@ function App({
9526
9652
  const drainQueueRef = useRef4(() => void 0);
9527
9653
  const [attachments, setAttachments] = useState5([]);
9528
9654
  const attachmentsRef = useRef4([]);
9655
+ const attachmentGeneration = useRef4(0);
9529
9656
  const automaticAttachmentTask = useRef4(null);
9530
9657
  const clipboardPasteTask = useRef4(null);
9531
9658
  const [overlay, setOverlay] = useState5({ kind: "none" });
@@ -9542,6 +9669,9 @@ function App({
9542
9669
  const [context, setContext] = useState5(() => runtime.modelContext());
9543
9670
  const transcriptEvents = useRef4([]);
9544
9671
  const transcriptTimer = useRef4(null);
9672
+ const clearScreen = useCallback(() => {
9673
+ if (stdout.isTTY) stdout.write("\x1B[2J\x1B[3J\x1B[H");
9674
+ }, [stdout]);
9545
9675
  const replaceAttachments = useCallback((next) => {
9546
9676
  attachmentsRef.current = next;
9547
9677
  setAttachments(next);
@@ -9590,8 +9720,8 @@ function App({
9590
9720
  const timer = setInterval(() => tick((n) => n + 1), 1e3);
9591
9721
  return () => clearInterval(timer);
9592
9722
  }, [turnStart]);
9593
- const theme = useMemo4(() => makeTheme(accent), [accent]);
9594
- const strings = useMemo4(() => stringsFor(lang), [lang]);
9723
+ const theme = useMemo5(() => makeTheme(accent), [accent]);
9724
+ const strings = useMemo5(() => stringsFor(lang), [lang]);
9595
9725
  const notice = useCallback(
9596
9726
  (level, text) => {
9597
9727
  flushTranscriptEvents();
@@ -9709,12 +9839,20 @@ function App({
9709
9839
  exit();
9710
9840
  return;
9711
9841
  case "clear":
9842
+ if (busyRef.current && abort.current) {
9843
+ abort.current.abort();
9844
+ }
9845
+ queueRef.current = [];
9846
+ setPendingCount(0);
9712
9847
  await runtime.newSession();
9713
9848
  history.current = [];
9714
9849
  replaceAttachments([]);
9715
9850
  setPromptHistory([]);
9716
- setTranscript(emptyTranscript());
9717
- setTranscriptRevision((revision) => revision + 1);
9851
+ await suspendTerminal(async () => {
9852
+ setTranscript(emptyTranscript());
9853
+ setTranscriptRevision((revision) => revision + 1);
9854
+ clearScreen();
9855
+ });
9718
9856
  sessionStart.current = Date.now();
9719
9857
  turns.current = 0;
9720
9858
  void runtime.persist([]).catch(
@@ -10212,6 +10350,9 @@ ${strings.mcpAddUsage}`
10212
10350
  }
10213
10351
  case "attach": {
10214
10352
  if (rawRest.toLowerCase() === "clear") {
10353
+ attachmentGeneration.current += 1;
10354
+ automaticAttachmentTask.current = null;
10355
+ clipboardPasteTask.current = null;
10215
10356
  replaceAttachments([]);
10216
10357
  notice("info", strings.attachmentsCleared);
10217
10358
  return;
@@ -10223,15 +10364,18 @@ ${strings.mcpAddUsage}`
10223
10364
  }
10224
10365
  if (clipboardPasteTask.current) return;
10225
10366
  const task = runtime.loadClipboardImage().then((block) => {
10367
+ if (clipboardPasteTask.current !== task) return;
10226
10368
  if (appendAttachment(block)) {
10227
10369
  notice(
10228
10370
  "info",
10229
10371
  strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image")
10230
10372
  );
10231
10373
  }
10232
- }).catch(
10233
- (error) => notice("error", error instanceof Error ? error.message : String(error))
10234
- );
10374
+ }).catch((error) => {
10375
+ if (clipboardPasteTask.current === task) {
10376
+ notice("error", error instanceof Error ? error.message : String(error));
10377
+ }
10378
+ });
10235
10379
  clipboardPasteTask.current = task;
10236
10380
  try {
10237
10381
  await task;
@@ -10540,6 +10684,7 @@ Rename the file if you want a different name.`);
10540
10684
  return Promise.resolve(true);
10541
10685
  }
10542
10686
  const task = runtime.loadAutomaticAttachment(requestedPath).then((block) => {
10687
+ if (automaticAttachmentTask.current !== task) return true;
10543
10688
  if (!block) return false;
10544
10689
  if (!appendAttachment(block)) {
10545
10690
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
@@ -10548,6 +10693,7 @@ Rename the file if you want a different name.`);
10548
10693
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "attachment"));
10549
10694
  return true;
10550
10695
  }).catch(() => {
10696
+ if (automaticAttachmentTask.current !== task) return true;
10551
10697
  return false;
10552
10698
  });
10553
10699
  automaticAttachmentTask.current = task;
@@ -10565,12 +10711,17 @@ Rename the file if you want a different name.`);
10565
10711
  return;
10566
10712
  }
10567
10713
  const task = runtime.loadClipboardImage().then((block) => {
10714
+ if (clipboardPasteTask.current !== task) return;
10568
10715
  if (!appendAttachment(block)) {
10569
10716
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
10570
10717
  return;
10571
10718
  }
10572
10719
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
10573
- }).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
10720
+ }).catch((error) => {
10721
+ if (clipboardPasteTask.current === task) {
10722
+ notice("error", error instanceof Error ? error.message : String(error));
10723
+ }
10724
+ });
10574
10725
  clipboardPasteTask.current = task;
10575
10726
  void task.finally(() => {
10576
10727
  if (clipboardPasteTask.current === task) clipboardPasteTask.current = null;
@@ -10578,6 +10729,7 @@ Rename the file if you want a different name.`);
10578
10729
  }, [appendAttachment, notice, runtime, strings]);
10579
10730
  const submit = useCallback(
10580
10731
  async (raw) => {
10732
+ const generation = attachmentGeneration.current;
10581
10733
  const pendingAttachments = [automaticAttachmentTask.current, clipboardPasteTask.current].filter(
10582
10734
  (task) => task !== null
10583
10735
  );
@@ -10585,6 +10737,7 @@ Rename the file if you want a different name.`);
10585
10737
  if (detachedInput) {
10586
10738
  setInput("");
10587
10739
  await Promise.all(pendingAttachments);
10740
+ if (generation !== attachmentGeneration.current) return;
10588
10741
  }
10589
10742
  const text = raw.trim();
10590
10743
  const submitSlash = () => {
@@ -10602,6 +10755,7 @@ Rename the file if you want a different name.`);
10602
10755
  }
10603
10756
  if (text && looksLikeAttachmentPath(text)) {
10604
10757
  const attached = await tryQueueAutomaticAttachment(text);
10758
+ if (generation !== attachmentGeneration.current) return;
10605
10759
  if (attached) {
10606
10760
  if (!detachedInput) setInput("");
10607
10761
  return;
@@ -10640,20 +10794,21 @@ Rename the file if you want a different name.`);
10640
10794
  return;
10641
10795
  }
10642
10796
  if (!key.escape) return;
10643
- if (input !== "" || attachmentsRef.current.length > 0) {
10797
+ if (busyRef.current && abort.current) {
10798
+ if (!abort.current.signal.aborted) {
10799
+ abort.current.abort();
10800
+ notice("warn", strings.cancelled);
10801
+ }
10802
+ return;
10803
+ }
10804
+ if (input !== "" || attachmentsRef.current.length > 0 || automaticAttachmentTask.current || clipboardPasteTask.current) {
10805
+ attachmentGeneration.current += 1;
10644
10806
  setInput("");
10645
10807
  replaceAttachments([]);
10646
10808
  if (automaticAttachmentTask.current) automaticAttachmentTask.current = null;
10647
10809
  if (clipboardPasteTask.current) clipboardPasteTask.current = null;
10648
10810
  return;
10649
10811
  }
10650
- if (busy && abort.current) {
10651
- abort.current.abort();
10652
- queueRef.current = [];
10653
- setPendingCount(0);
10654
- notice("warn", strings.cancelled);
10655
- return;
10656
- }
10657
10812
  if (overlay.kind === "permission") {
10658
10813
  overlay.resolve("deny");
10659
10814
  setOverlay({ kind: "none" });