@kernelonpanic/kitcode 1.2.6 → 1.2.8

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);
@@ -1551,7 +1559,12 @@ async function* streamWithRetry(provider, request, modelRef, signal) {
1551
1559
  if (attempt >= MAX_RETRIES2) break;
1552
1560
  const waitMs = retryBackoffMs(error, attempt);
1553
1561
  if (waitMs > 0) {
1554
- await sleep(waitMs, signal);
1562
+ try {
1563
+ await sleep(waitMs, signal);
1564
+ } catch (error2) {
1565
+ if (signal.aborted) return;
1566
+ throw error2;
1567
+ }
1555
1568
  continue;
1556
1569
  }
1557
1570
  break;
@@ -1762,7 +1775,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1762
1775
  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
1776
  };
1764
1777
  }
1765
- if (current.costUsd !== null && current.costUsd >= limits.maxCostUsdPerTurn) {
1778
+ if (limits.maxCostUsdPerTurn > 0 && current.costUsd !== null && current.costUsd >= limits.maxCostUsdPerTurn) {
1766
1779
  return {
1767
1780
  allowed: false,
1768
1781
  reason: `Turn stopped at the cost budget ($${limits.maxCostUsdPerTurn.toFixed(2)}). Send another message to continue.`
@@ -1781,7 +1794,7 @@ function createTurnBudget(limits, resolvePricing = pricingFor) {
1781
1794
  };
1782
1795
  }
1783
1796
  const pricing = resolvePricing(request.modelRef);
1784
- if (current.costUsd !== null) {
1797
+ if (limits.maxCostUsdPerTurn > 0 && current.costUsd !== null) {
1785
1798
  const remainingUsd = limits.maxCostUsdPerTurn - current.costUsd;
1786
1799
  const effectivePricing = pricing ?? UNKNOWN_MODEL_PRICING;
1787
1800
  const inputUsd = estimatedInput * effectivePricing.input / 1e6;
@@ -3258,11 +3271,25 @@ function createSubagentRunner(makeConfig, tools) {
3258
3271
  return {
3259
3272
  async run(request) {
3260
3273
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
3274
+ let steps = 0;
3261
3275
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
3262
3276
  const cfg = {
3263
3277
  ...base,
3278
+ provider: {
3279
+ id: base.provider.id,
3280
+ kind: base.provider.kind,
3281
+ listModels: () => base.provider.listModels(),
3282
+ knownModels: () => base.provider.knownModels(),
3283
+ stream: (chat) => base.provider.stream(steps === MAX_SUBAGENT_STEPS ? {
3284
+ ...chat,
3285
+ tools: [],
3286
+ system: `${chat.system}
3287
+
3288
+ This is your last model call. Do not call tools. Return your findings now, clearly stating any unfinished work.`
3289
+ } : chat)
3290
+ },
3264
3291
  tools: {
3265
- get: (name) => name === TASK_TOOL_NAME ? void 0 : base.tools.get(name),
3292
+ get: (name) => name === TASK_TOOL_NAME || steps >= MAX_SUBAGENT_STEPS ? void 0 : base.tools.get(name),
3266
3293
  schemas: () => base.tools.schemas().filter((schema) => schema.name !== TASK_TOOL_NAME)
3267
3294
  },
3268
3295
  system: `${SUBAGENT_SYSTEM}
@@ -3271,7 +3298,6 @@ Working directory: ${base.cwd}`
3271
3298
  };
3272
3299
  const limit = new AbortController();
3273
3300
  const signal = AbortSignal.any([request.signal, limit.signal]);
3274
- let steps = 0;
3275
3301
  const hooks = {
3276
3302
  onEvent(event) {
3277
3303
  if (event.type === "turn_start" && ++steps > MAX_SUBAGENT_STEPS) limit.abort();
@@ -3295,6 +3321,7 @@ Working directory: ${base.cwd}`
3295
3321
  ];
3296
3322
  const text = finalText(await runTurn(cfg, history, hooks, signal));
3297
3323
  if (!limit.signal.aborted) return text;
3324
+ if (request.signal.aborted && text === SUBAGENT_NO_ANSWER) return SUBAGENT_CANCELLED;
3298
3325
  return text === SUBAGENT_NO_ANSWER ? STEP_LIMIT_NOTE : `${text}
3299
3326
 
3300
3327
  ${STEP_LIMIT_NOTE}`;
@@ -3472,7 +3499,7 @@ function clip(value) {
3472
3499
  // package.json
3473
3500
  var package_default = {
3474
3501
  name: "@kernelonpanic/kitcode",
3475
- version: "1.2.6",
3502
+ version: "1.2.8",
3476
3503
  description: "Terminal coding agent with a config you never have to write by hand",
3477
3504
  type: "module",
3478
3505
  license: "MIT",
@@ -3540,7 +3567,7 @@ var package_default = {
3540
3567
 
3541
3568
  // src/version.ts
3542
3569
  var KITCODE_VERSION = package_default.version;
3543
- var KITCODE_COMMIT = true ? "f40cc2cb3cebf7fa8b3c20dbbff5b14047d1e983" : "development";
3570
+ var KITCODE_COMMIT = true ? "422e80dd7cd81a1c20f186b1e3ab36d6e0779b89" : "development";
3544
3571
 
3545
3572
  // src/mcp/client.ts
3546
3573
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3928,7 +3955,7 @@ async function limitedResponseText(response, limit) {
3928
3955
  import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3929
3956
  import path8 from "path";
3930
3957
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3931
- var CACHE_VERSION = 2;
3958
+ var CACHE_VERSION = 3;
3932
3959
  async function loadModels(provider, refresh = false) {
3933
3960
  const file = cacheFile(provider.id);
3934
3961
  const cached = await readCache(file);
@@ -4155,13 +4182,15 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4155
4182
  final = await stream.finalMessage();
4156
4183
  } catch (error) {
4157
4184
  if (isUserAbort(error, req.signal)) {
4158
- const partial = stream.currentMessage;
4159
- if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4185
+ const partial2 = stream.currentMessage;
4186
+ if (partial2) yield { type: "usage", usage: toUsage(partial2.usage) };
4160
4187
  const limits3 = parseRateLimits(capture.headers());
4161
4188
  if (limits3) yield { type: "rate_limits", limits: limits3 };
4162
4189
  yield { type: "done", stopReason: "aborted", content: partialContent(thinking, text) };
4163
4190
  return;
4164
4191
  }
4192
+ const partial = stream.currentMessage;
4193
+ if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4165
4194
  const limits2 = parseRateLimits(capture.headers());
4166
4195
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4167
4196
  if (events === 0 && capture.succeeded() && !isConnectionFailure(error)) {
@@ -4195,7 +4224,7 @@ async function listModels(client, providerId, apiKey2) {
4195
4224
  name: model.display_name,
4196
4225
  contextWindow: model.max_input_tokens ?? void 0,
4197
4226
  maxOutput: model.max_tokens ?? void 0,
4198
- pricing: pricingFor(model.id)
4227
+ pricing: modelInfoFromRaw(model)?.pricing ?? pricingFor(model.id)
4199
4228
  });
4200
4229
  }
4201
4230
  return models;
@@ -4355,6 +4384,7 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4355
4384
  }
4356
4385
  } catch (error) {
4357
4386
  if (!isUserAbort(error, req.signal)) {
4387
+ if (sawUsage) yield { type: "usage", usage };
4358
4388
  const limits2 = parseRateLimits(capture.headers());
4359
4389
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4360
4390
  throw toProviderError(error, providerId, [apiKey2]);
@@ -4370,13 +4400,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4370
4400
  return;
4371
4401
  }
4372
4402
  if (recognised === 0) throw await invalidStreamError(providerId, capture, void 0, [apiKey2]);
4403
+ if (sawUsage) yield { type: "usage", usage };
4373
4404
  const content = toContentBlocks2(providerId, thinking, text, calls);
4374
4405
  for (const block of content) {
4375
4406
  if (block.type === "tool_use") {
4376
4407
  yield { type: "tool_call", id: block.id, name: block.name, input: block.input };
4377
4408
  }
4378
4409
  }
4379
- if (sawUsage) yield { type: "usage", usage };
4380
4410
  const limits = parseRateLimits(capture.headers());
4381
4411
  if (limits) yield { type: "rate_limits", limits };
4382
4412
  yield { type: "done", stopReason: resolveStop(finishReason, calls.size > 0), content };
@@ -6505,6 +6535,7 @@ async function boot(options) {
6505
6535
  async setMaxTokensPerTurn(tokens3) {
6506
6536
  await persistConfig((draft) => {
6507
6537
  draft.budget.maxTokensPerTurn = tokens3;
6538
+ draft.budget.maxCostUsdPerTurn = 0;
6508
6539
  });
6509
6540
  },
6510
6541
  getLang: () => config.lang,
@@ -7036,9 +7067,49 @@ function validateKey(value) {
7036
7067
  // src/app/tui.tsx
7037
7068
  import { render } from "ink";
7038
7069
 
7070
+ // src/ui/terminal-size.ts
7071
+ import { useStdout } from "ink";
7072
+ import { useMemo, useSyncExternalStore } from "react";
7073
+ var stores = /* @__PURE__ */ new WeakMap();
7074
+ function createStore(stdout) {
7075
+ let size = { columns: stdout.columns || 80, rows: stdout.rows || 24 };
7076
+ const listeners = /* @__PURE__ */ new Set();
7077
+ const update = () => {
7078
+ const columns = stdout.columns || 80;
7079
+ const rows = stdout.rows || 24;
7080
+ if (columns === size.columns && rows === size.rows) return;
7081
+ size = { columns, rows };
7082
+ for (const listener of listeners) listener();
7083
+ };
7084
+ return {
7085
+ getSnapshot: () => size,
7086
+ subscribe(listener) {
7087
+ if (listeners.size === 0) stdout.on("resize", update);
7088
+ listeners.add(listener);
7089
+ update();
7090
+ return () => {
7091
+ listeners.delete(listener);
7092
+ if (listeners.size === 0) stdout.off("resize", update);
7093
+ };
7094
+ }
7095
+ };
7096
+ }
7097
+ function useTerminalSize() {
7098
+ const { stdout } = useStdout();
7099
+ const store = useMemo(() => {
7100
+ let current = stores.get(stdout);
7101
+ if (!current) {
7102
+ current = createStore(stdout);
7103
+ stores.set(stdout, current);
7104
+ }
7105
+ return current;
7106
+ }, [stdout]);
7107
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
7108
+ }
7109
+
7039
7110
  // 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";
7111
+ import { Box as Box13, Text as Text12, useApp, useInput as useInput2 } from "ink";
7112
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo5, useRef as useRef4, useState as useState5 } from "react";
7042
7113
 
7043
7114
  // src/mcp/add.ts
7044
7115
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
@@ -7665,7 +7736,7 @@ import { Box as Box3, Text as Text3 } from "ink";
7665
7736
  import { useState } from "react";
7666
7737
 
7667
7738
  // src/ui/components/Logo.tsx
7668
- import { Box as Box2, Text as Text2, useWindowSize } from "ink";
7739
+ import { Box as Box2, Text as Text2 } from "ink";
7669
7740
 
7670
7741
  // src/ui/workspace.ts
7671
7742
  import { homedir } from "os";
@@ -7712,7 +7783,7 @@ var CAT = [" \u2571\\_\u2571\\ ", " ( o.o )", " > ^ < "];
7712
7783
  function Logo({ subtitle, workspace }) {
7713
7784
  const theme = useTheme();
7714
7785
  const strings = useStrings();
7715
- const { columns } = useWindowSize();
7786
+ const { columns } = useTerminalSize();
7716
7787
  const location = workspace ? formatWorkspacePath(workspace, Math.max(6, Math.min(72, columns - 15))) : "";
7717
7788
  return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
7718
7789
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
@@ -7970,15 +8041,16 @@ function previewText(value, limit = MAX_PREVIEW_CHARS) {
7970
8041
 
7971
8042
  // src/ui/components/Picker.tsx
7972
8043
  import { Box as Box7, Text as Text7 } from "ink";
7973
- import { useMemo, useState as useState3 } from "react";
8044
+ import { useMemo as useMemo2, useState as useState3 } from "react";
7974
8045
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7975
8046
  var WINDOW = 10;
7976
8047
  function Picker({ title, items, onSelect, onCancel }) {
7977
8048
  const theme = useTheme();
8049
+ const { columns } = useTerminalSize();
7978
8050
  const strings = useStrings();
7979
8051
  const [query, setQuery] = useState3("");
7980
8052
  const [cursor, setCursor] = useState3(0);
7981
- const matches = useMemo(() => {
8053
+ const matches = useMemo2(() => {
7982
8054
  const needle = query.toLowerCase();
7983
8055
  if (needle === "") return items;
7984
8056
  return items.filter(
@@ -8005,7 +8077,7 @@ function Picker({ title, items, onSelect, onCancel }) {
8005
8077
  });
8006
8078
  const start = Math.max(0, Math.min(active2 - WINDOW + 2, matches.length - WINDOW));
8007
8079
  const visible = matches.slice(start, start + WINDOW);
8008
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
8080
+ return /* @__PURE__ */ jsxs7(Box7, { width: columns, maxWidth: "100%", flexShrink: 0, flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [
8009
8081
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accent, children: [
8010
8082
  sanitizeTerminalText(title),
8011
8083
  query !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -8015,7 +8087,7 @@ function Picker({ title, items, onSelect, onCancel }) {
8015
8087
  ] }),
8016
8088
  matches.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: strings.noMatches }) : visible.map((item, index) => {
8017
8089
  const selected = start + index === active2;
8018
- return /* @__PURE__ */ jsxs7(Text7, { color: selected ? theme.accent : void 0, inverse: selected, children: [
8090
+ return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate-end", color: selected ? theme.accent : void 0, inverse: selected, children: [
8019
8091
  truncate2(sanitizeTerminalText(item.label), 70),
8020
8092
  item.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
8021
8093
  " ",
@@ -8071,6 +8143,7 @@ var PromptInput = memo(function PromptInput2({
8071
8143
  attachments = []
8072
8144
  }) {
8073
8145
  const theme = useTheme();
8146
+ const { columns } = useTerminalSize();
8074
8147
  const strings = useStrings();
8075
8148
  const safeValue = sanitizeTerminalText(value);
8076
8149
  const [selectionCursor, setSelectionCursor] = useState4(0);
@@ -8206,7 +8279,7 @@ var PromptInput = memo(function PromptInput2({
8206
8279
  });
8207
8280
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8208
8281
  const visible = suggestions.slice(start, start + WINDOW2);
8209
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8282
+ return /* @__PURE__ */ jsxs8(Box8, { width: columns, maxWidth: "100%", flexDirection: "column", marginTop: 1, flexShrink: 0, children: [
8210
8283
  /* @__PURE__ */ jsxs8(
8211
8284
  Box8,
8212
8285
  {
@@ -8217,7 +8290,7 @@ var PromptInput = memo(function PromptInput2({
8217
8290
  paddingX: 1,
8218
8291
  children: [
8219
8292
  /* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
8220
- /* @__PURE__ */ jsx8(EditableText, { value: safeValue, cursor: inputCursor, placeholder: strings.placeholder }),
8293
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: /* @__PURE__ */ jsx8(EditableText, { value: safeValue, cursor: inputCursor, placeholder: strings.placeholder }) }),
8221
8294
  pending && pending > 0 ? /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
8222
8295
  " \xB7 ",
8223
8296
  strings.queued(pending)
@@ -8510,9 +8583,12 @@ function liveTranscriptRows(rows) {
8510
8583
  return Math.max(1, interactiveViewportRows(rows) - INTERACTIVE_CHROME_ROWS);
8511
8584
  }
8512
8585
  function TerminalViewport({ children, rows }) {
8586
+ const { columns } = useTerminalSize();
8513
8587
  return /* @__PURE__ */ jsx10(
8514
8588
  Box10,
8515
8589
  {
8590
+ width: columns,
8591
+ maxWidth: "100%",
8516
8592
  flexDirection: "column",
8517
8593
  maxHeight: interactiveViewportRows(rows),
8518
8594
  overflowY: "hidden",
@@ -8522,18 +8598,18 @@ function TerminalViewport({ children, rows }) {
8522
8598
  }
8523
8599
 
8524
8600
  // 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";
8601
+ import { Box as Box12, Static, Text as Text11 } from "ink";
8602
+ import { memo as memo2, useMemo as useMemo4, useRef as useRef3 } from "react";
8527
8603
  import Spinner2 from "ink-spinner";
8528
8604
  import stringWidth2 from "string-width";
8529
8605
 
8530
8606
  // 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";
8607
+ import { Box as Box11, Text as Text10 } from "ink";
8608
+ import { Fragment as Fragment4, useMemo as useMemo3 } from "react";
8533
8609
  import stringWidth from "string-width";
8534
8610
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8535
8611
  function Markdown({ children }) {
8536
- const blocks = useMemo2(() => extractBlocks(children), [children]);
8612
+ const blocks = useMemo3(() => extractBlocks(children), [children]);
8537
8613
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
8538
8614
  }
8539
8615
  function BlockView({ block }) {
@@ -8567,7 +8643,7 @@ function BlockView({ block }) {
8567
8643
  case "code":
8568
8644
  return /* @__PURE__ */ jsx11(CodeBlock, { code: block.text ?? "" });
8569
8645
  case "hr":
8570
- return /* @__PURE__ */ jsx11(Box11, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx11(Text10, { dimColor: true, wrap: "truncate-end", children: "\u2500".repeat(60) }) });
8646
+ return /* @__PURE__ */ jsx11(Box11, { height: 1 });
8571
8647
  }
8572
8648
  }
8573
8649
  function CodeBlock({ code }) {
@@ -8575,7 +8651,7 @@ function CodeBlock({ code }) {
8575
8651
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) });
8576
8652
  }
8577
8653
  function TableView({ block }) {
8578
- const { columns } = useWindowSize2();
8654
+ const { columns } = useTerminalSize();
8579
8655
  const headers = block.headers ?? [];
8580
8656
  const rows = block.rows ?? [];
8581
8657
  if (headers.length === 0 && rows.length === 0) {
@@ -9148,26 +9224,41 @@ var BubbleView = memo2(function BubbleView2({
9148
9224
  return /* @__PURE__ */ jsx12(AssistantView, { bubble, maxRows });
9149
9225
  }
9150
9226
  if (bubble.kind === "subagent") {
9151
- return /* @__PURE__ */ jsx12(SubagentView, { bubble });
9227
+ return /* @__PURE__ */ jsx12(SubagentView, { bubble, maxRows });
9152
9228
  }
9153
9229
  return /* @__PURE__ */ jsx12(ToolView, { bubble });
9154
9230
  });
9155
- function AssistantView({ bubble, maxRows }) {
9156
- const { columns } = useWindowSize3();
9231
+ function splitThinkingPrefix(text, streaming) {
9232
+ const opening = /^\s*<think(?:ing)?>/i.exec(text);
9233
+ if (!opening) {
9234
+ const prefix = text.trimStart().toLowerCase();
9235
+ return { thinking: "", text: streaming && prefix !== "" && ["<think>", "<thinking>"].some((tag) => tag.startsWith(prefix)) ? "" : text };
9236
+ }
9237
+ const rest = text.slice(opening[0].length);
9238
+ const closing = /<\/think(?:ing)?>/i.exec(rest);
9239
+ return closing ? { thinking: sanitizeThinkingText(rest.slice(0, closing.index)), text: rest.slice(closing.index + closing[0].length) } : { thinking: sanitizeThinkingText(rest, streaming), text: "" };
9240
+ }
9241
+ function AssistantView({ bubble: source, maxRows }) {
9242
+ const tagged = splitThinkingPrefix(source.text, source.streaming);
9243
+ const bubble = { ...source, text: tagged.text, thinking: [source.thinking, tagged.thinking].filter(Boolean).join("\n") };
9244
+ const { columns } = useTerminalSize();
9157
9245
  const frozenThinking = useRef3(void 0);
9158
9246
  const answering = bubble.streaming && bubble.text !== "";
9159
9247
  if (!answering) frozenThinking.current = void 0;
9160
9248
  if (answering && frozenThinking.current === void 0) {
9161
9249
  frozenThinking.current = bubble.thinking;
9162
9250
  }
9163
- const visibleThinking = assistantThinkingForFrame(bubble, frozenThinking.current);
9251
+ const visibleThinking = sanitizeThinkingText(
9252
+ assistantThinkingForFrame(bubble, frozenThinking.current),
9253
+ bubble.streaming
9254
+ );
9164
9255
  const liveBudget = bubble.streaming && maxRows !== void 0 ? Math.max(1, maxRows - 1) : void 0;
9165
9256
  const thinkingBudget = liveBudget === void 0 ? void 0 : bubble.text !== "" ? Math.min(3, Math.max(0, liveBudget - 1)) : Math.max(0, liveBudget - 1);
9166
9257
  const frameThinking = thinkingBudget === void 0 ? visibleThinking.trim() : clipTextToRows(visibleThinking.trim(), thinkingBudget, columns);
9167
9258
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9168
9259
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9169
9260
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9170
- frameThinking !== "" && /* @__PURE__ */ jsx12(Text11, { dimColor: true, italic: true, children: frameThinking }),
9261
+ frameThinking !== "" && /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", maxHeight: thinkingBudget, overflowY: "hidden", children: /* @__PURE__ */ jsx12(Markdown, { children: frameThinking }) }),
9171
9262
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9172
9263
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9173
9264
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9206,7 +9297,7 @@ function ToolView({ bubble }) {
9206
9297
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
9207
9298
  const color = bubble.state === "error" ? theme.error : bubble.state === "ok" ? theme.ok : theme.warn;
9208
9299
  const display = bubble.display;
9209
- const diff = useMemo3(
9300
+ const diff = useMemo4(
9210
9301
  () => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
9211
9302
  [display]
9212
9303
  );
@@ -9244,20 +9335,20 @@ function previewLines(content) {
9244
9335
  if (lines.length <= 6) return lines;
9245
9336
  return [...lines.slice(0, 6), `\u2026 ${lines.length - 6} more lines`];
9246
9337
  }
9247
- function SubagentView({ bubble }) {
9338
+ function SubagentView({ bubble, maxRows }) {
9248
9339
  const theme = useTheme();
9340
+ const { columns } = useTerminalSize();
9249
9341
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
9250
9342
  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: [
9343
+ const latest = bubble.bubbles.at(-1);
9344
+ const progress = latest?.kind === "assistant" ? sanitizeThinkingText(latest.text || latest.thinking, latest.streaming) : latest?.kind === "tool" ? latest.summary : "";
9345
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, minWidth: 0, children: [
9346
+ /* @__PURE__ */ jsxs11(Text11, { color, bold: true, wrap: "truncate-end", children: [
9253
9347
  mark,
9254
9348
  " subagent: ",
9255
9349
  bubble.description
9256
9350
  ] }),
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
- ] })
9351
+ /* @__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
9352
  ] });
9262
9353
  }
9263
9354
 
@@ -9508,7 +9599,7 @@ function App({
9508
9599
  warnings = []
9509
9600
  }) {
9510
9601
  const { exit } = useApp();
9511
- const { rows } = useWindowSize4();
9602
+ const { rows } = useTerminalSize();
9512
9603
  const [transcript, setTranscript] = useState5(
9513
9604
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
9514
9605
  );
@@ -9526,6 +9617,7 @@ function App({
9526
9617
  const drainQueueRef = useRef4(() => void 0);
9527
9618
  const [attachments, setAttachments] = useState5([]);
9528
9619
  const attachmentsRef = useRef4([]);
9620
+ const attachmentGeneration = useRef4(0);
9529
9621
  const automaticAttachmentTask = useRef4(null);
9530
9622
  const clipboardPasteTask = useRef4(null);
9531
9623
  const [overlay, setOverlay] = useState5({ kind: "none" });
@@ -9590,8 +9682,8 @@ function App({
9590
9682
  const timer = setInterval(() => tick((n) => n + 1), 1e3);
9591
9683
  return () => clearInterval(timer);
9592
9684
  }, [turnStart]);
9593
- const theme = useMemo4(() => makeTheme(accent), [accent]);
9594
- const strings = useMemo4(() => stringsFor(lang), [lang]);
9685
+ const theme = useMemo5(() => makeTheme(accent), [accent]);
9686
+ const strings = useMemo5(() => stringsFor(lang), [lang]);
9595
9687
  const notice = useCallback(
9596
9688
  (level, text) => {
9597
9689
  flushTranscriptEvents();
@@ -10212,6 +10304,9 @@ ${strings.mcpAddUsage}`
10212
10304
  }
10213
10305
  case "attach": {
10214
10306
  if (rawRest.toLowerCase() === "clear") {
10307
+ attachmentGeneration.current += 1;
10308
+ automaticAttachmentTask.current = null;
10309
+ clipboardPasteTask.current = null;
10215
10310
  replaceAttachments([]);
10216
10311
  notice("info", strings.attachmentsCleared);
10217
10312
  return;
@@ -10223,15 +10318,18 @@ ${strings.mcpAddUsage}`
10223
10318
  }
10224
10319
  if (clipboardPasteTask.current) return;
10225
10320
  const task = runtime.loadClipboardImage().then((block) => {
10321
+ if (clipboardPasteTask.current !== task) return;
10226
10322
  if (appendAttachment(block)) {
10227
10323
  notice(
10228
10324
  "info",
10229
10325
  strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image")
10230
10326
  );
10231
10327
  }
10232
- }).catch(
10233
- (error) => notice("error", error instanceof Error ? error.message : String(error))
10234
- );
10328
+ }).catch((error) => {
10329
+ if (clipboardPasteTask.current === task) {
10330
+ notice("error", error instanceof Error ? error.message : String(error));
10331
+ }
10332
+ });
10235
10333
  clipboardPasteTask.current = task;
10236
10334
  try {
10237
10335
  await task;
@@ -10540,6 +10638,7 @@ Rename the file if you want a different name.`);
10540
10638
  return Promise.resolve(true);
10541
10639
  }
10542
10640
  const task = runtime.loadAutomaticAttachment(requestedPath).then((block) => {
10641
+ if (automaticAttachmentTask.current !== task) return true;
10543
10642
  if (!block) return false;
10544
10643
  if (!appendAttachment(block)) {
10545
10644
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
@@ -10548,6 +10647,7 @@ Rename the file if you want a different name.`);
10548
10647
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "attachment"));
10549
10648
  return true;
10550
10649
  }).catch(() => {
10650
+ if (automaticAttachmentTask.current !== task) return true;
10551
10651
  return false;
10552
10652
  });
10553
10653
  automaticAttachmentTask.current = task;
@@ -10565,12 +10665,17 @@ Rename the file if you want a different name.`);
10565
10665
  return;
10566
10666
  }
10567
10667
  const task = runtime.loadClipboardImage().then((block) => {
10668
+ if (clipboardPasteTask.current !== task) return;
10568
10669
  if (!appendAttachment(block)) {
10569
10670
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
10570
10671
  return;
10571
10672
  }
10572
10673
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
10573
- }).catch((error) => notice("error", error instanceof Error ? error.message : String(error)));
10674
+ }).catch((error) => {
10675
+ if (clipboardPasteTask.current === task) {
10676
+ notice("error", error instanceof Error ? error.message : String(error));
10677
+ }
10678
+ });
10574
10679
  clipboardPasteTask.current = task;
10575
10680
  void task.finally(() => {
10576
10681
  if (clipboardPasteTask.current === task) clipboardPasteTask.current = null;
@@ -10578,6 +10683,7 @@ Rename the file if you want a different name.`);
10578
10683
  }, [appendAttachment, notice, runtime, strings]);
10579
10684
  const submit = useCallback(
10580
10685
  async (raw) => {
10686
+ const generation = attachmentGeneration.current;
10581
10687
  const pendingAttachments = [automaticAttachmentTask.current, clipboardPasteTask.current].filter(
10582
10688
  (task) => task !== null
10583
10689
  );
@@ -10585,6 +10691,7 @@ Rename the file if you want a different name.`);
10585
10691
  if (detachedInput) {
10586
10692
  setInput("");
10587
10693
  await Promise.all(pendingAttachments);
10694
+ if (generation !== attachmentGeneration.current) return;
10588
10695
  }
10589
10696
  const text = raw.trim();
10590
10697
  const submitSlash = () => {
@@ -10602,6 +10709,7 @@ Rename the file if you want a different name.`);
10602
10709
  }
10603
10710
  if (text && looksLikeAttachmentPath(text)) {
10604
10711
  const attached = await tryQueueAutomaticAttachment(text);
10712
+ if (generation !== attachmentGeneration.current) return;
10605
10713
  if (attached) {
10606
10714
  if (!detachedInput) setInput("");
10607
10715
  return;
@@ -10640,20 +10748,21 @@ Rename the file if you want a different name.`);
10640
10748
  return;
10641
10749
  }
10642
10750
  if (!key.escape) return;
10643
- if (input !== "" || attachmentsRef.current.length > 0) {
10751
+ if (busyRef.current && abort.current) {
10752
+ if (!abort.current.signal.aborted) {
10753
+ abort.current.abort();
10754
+ notice("warn", strings.cancelled);
10755
+ }
10756
+ return;
10757
+ }
10758
+ if (input !== "" || attachmentsRef.current.length > 0 || automaticAttachmentTask.current || clipboardPasteTask.current) {
10759
+ attachmentGeneration.current += 1;
10644
10760
  setInput("");
10645
10761
  replaceAttachments([]);
10646
10762
  if (automaticAttachmentTask.current) automaticAttachmentTask.current = null;
10647
10763
  if (clipboardPasteTask.current) clipboardPasteTask.current = null;
10648
10764
  return;
10649
10765
  }
10650
- if (busy && abort.current) {
10651
- abort.current.abort();
10652
- queueRef.current = [];
10653
- setPendingCount(0);
10654
- notice("warn", strings.cancelled);
10655
- return;
10656
- }
10657
10766
  if (overlay.kind === "permission") {
10658
10767
  overlay.resolve("deny");
10659
10768
  setOverlay({ kind: "none" });