@kernelonpanic/kitcode 1.2.5 → 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;
@@ -2467,7 +2480,7 @@ async function isFile2(file) {
2467
2480
 
2468
2481
  // src/core/attachments.ts
2469
2482
  import { execFile } from "child_process";
2470
- import { lstat as lstat2, readFile as readFile6, stat as stat5 } from "fs/promises";
2483
+ import { lstat as lstat2, readdir as readdir3, readFile as readFile6, stat as stat5 } from "fs/promises";
2471
2484
  import os2 from "os";
2472
2485
  import path7 from "path";
2473
2486
  import { fileURLToPath } from "url";
@@ -2506,15 +2519,17 @@ async function loadAttachment(cwd, requestedPath) {
2506
2519
  if (error.code === "ENOENT") throw new Error(`Attachment not found: ${resolved}`);
2507
2520
  throw error;
2508
2521
  });
2522
+ if (info.isDirectory()) {
2523
+ const attachment = await loadFirstImageFromDirectory(resolved);
2524
+ if (attachment) return attachment;
2525
+ throw new Error(`No supported images found in directory: ${path7.basename(resolved)}`);
2526
+ }
2509
2527
  if (!info.isFile()) throw new Error(`Attachment is not a regular file: ${resolved}`);
2510
2528
  return loadResolvedAttachment(resolved, info.size);
2511
2529
  }
2512
2530
  async function loadAutomaticAttachment(cwd, requestedPath) {
2513
2531
  if (!looksLikeAttachmentPath(requestedPath)) return null;
2514
- const requested = resolveAttachmentPath(cwd, requestedPath);
2515
- const safe = resolveInside(cwd, requested);
2516
- if (!safe.ok || safe.relative === "") return null;
2517
- const resolved = safe.path;
2532
+ const resolved = resolveAttachmentPath(cwd, requestedPath);
2518
2533
  if (isSensitiveAutomaticPath(resolved)) {
2519
2534
  throw new Error(
2520
2535
  `For safety, sensitive-looking files must be attached explicitly with /attach: ${path7.basename(resolved)}`
@@ -2526,17 +2541,23 @@ async function loadAutomaticAttachment(cwd, requestedPath) {
2526
2541
  if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
2527
2542
  throw error;
2528
2543
  });
2544
+ if (info?.isDirectory()) return loadFirstImageFromDirectory(resolved);
2529
2545
  if (!info?.isFile()) return null;
2530
2546
  return loadResolvedAttachment(resolved, info.size);
2531
2547
  }
2548
+ var MAX_PATH_CHARS = 2048;
2532
2549
  function looksLikeAttachmentPath(value) {
2533
2550
  const trimmed = value.trim();
2534
2551
  if (!trimmed || /[\r\n\0]/.test(trimmed)) return false;
2552
+ if (trimmed.length > MAX_PATH_CHARS) return false;
2535
2553
  const candidate = normalizeInputPath(trimmed);
2536
2554
  if (/^file:\/\//i.test(candidate)) return true;
2537
2555
  if (path7.isAbsolute(candidate)) return true;
2538
2556
  if (/^(?:~|\.{1,2})[\\/]/.test(candidate)) return true;
2539
- if (candidate.includes("/") || candidate.includes("\\")) return true;
2557
+ if (candidate.includes("/") || candidate.includes("\\")) {
2558
+ if (trimmed.length > 200) return false;
2559
+ return true;
2560
+ }
2540
2561
  const basename3 = path7.basename(candidate).toLowerCase();
2541
2562
  return path7.extname(basename3) !== "" || AUTO_PATH_NAMES.has(basename3);
2542
2563
  }
@@ -2600,6 +2621,18 @@ async function loadResolvedAttachment(resolved, size) {
2600
2621
  block: { type: "file", mediaType: textMime(resolved), text, name }
2601
2622
  };
2602
2623
  }
2624
+ async function loadFirstImageFromDirectory(dirPath) {
2625
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
2626
+ const entries = await readdir3(dirPath, { withFileTypes: true }).catch(() => null);
2627
+ if (!entries) return null;
2628
+ const sorted = entries.filter((entry) => entry.isFile() && IMAGE_EXTENSIONS.has(path7.extname(entry.name).toLowerCase())).sort((a, b) => a.name.localeCompare(b.name));
2629
+ if (sorted.length === 0) return null;
2630
+ const first2 = sorted[0];
2631
+ const filePath = path7.join(dirPath, first2.name);
2632
+ const info = await stat5(filePath).catch(() => null);
2633
+ if (!info?.isFile()) return null;
2634
+ return loadResolvedAttachment(filePath, info.size);
2635
+ }
2603
2636
  function resolveAttachmentPath(cwd, requestedPath) {
2604
2637
  const cleaned = normalizeInputPath(requestedPath.trim());
2605
2638
  if (!cleaned) throw new Error("Give a file path: /attach <path>");
@@ -3238,11 +3271,25 @@ function createSubagentRunner(makeConfig, tools) {
3238
3271
  return {
3239
3272
  async run(request) {
3240
3273
  if (request.signal.aborted) return SUBAGENT_CANCELLED;
3274
+ let steps = 0;
3241
3275
  const base = makeConfig(SUBAGENT_SYSTEM, tools);
3242
3276
  const cfg = {
3243
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
+ },
3244
3291
  tools: {
3245
- 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),
3246
3293
  schemas: () => base.tools.schemas().filter((schema) => schema.name !== TASK_TOOL_NAME)
3247
3294
  },
3248
3295
  system: `${SUBAGENT_SYSTEM}
@@ -3251,7 +3298,6 @@ Working directory: ${base.cwd}`
3251
3298
  };
3252
3299
  const limit = new AbortController();
3253
3300
  const signal = AbortSignal.any([request.signal, limit.signal]);
3254
- let steps = 0;
3255
3301
  const hooks = {
3256
3302
  onEvent(event) {
3257
3303
  if (event.type === "turn_start" && ++steps > MAX_SUBAGENT_STEPS) limit.abort();
@@ -3275,6 +3321,7 @@ Working directory: ${base.cwd}`
3275
3321
  ];
3276
3322
  const text = finalText(await runTurn(cfg, history, hooks, signal));
3277
3323
  if (!limit.signal.aborted) return text;
3324
+ if (request.signal.aborted && text === SUBAGENT_NO_ANSWER) return SUBAGENT_CANCELLED;
3278
3325
  return text === SUBAGENT_NO_ANSWER ? STEP_LIMIT_NOTE : `${text}
3279
3326
 
3280
3327
  ${STEP_LIMIT_NOTE}`;
@@ -3452,7 +3499,7 @@ function clip(value) {
3452
3499
  // package.json
3453
3500
  var package_default = {
3454
3501
  name: "@kernelonpanic/kitcode",
3455
- version: "1.2.5",
3502
+ version: "1.2.8",
3456
3503
  description: "Terminal coding agent with a config you never have to write by hand",
3457
3504
  type: "module",
3458
3505
  license: "MIT",
@@ -3520,7 +3567,7 @@ var package_default = {
3520
3567
 
3521
3568
  // src/version.ts
3522
3569
  var KITCODE_VERSION = package_default.version;
3523
- var KITCODE_COMMIT = true ? "e9d5bdd60b06f2ee8d2d28f0d53b13d68298ae6b" : "development";
3570
+ var KITCODE_COMMIT = true ? "422e80dd7cd81a1c20f186b1e3ab36d6e0779b89" : "development";
3524
3571
 
3525
3572
  // src/mcp/client.ts
3526
3573
  var clientInfo = { name: "kitcode", version: KITCODE_VERSION };
@@ -3908,7 +3955,7 @@ async function limitedResponseText(response, limit) {
3908
3955
  import { chmod as chmod4, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
3909
3956
  import path8 from "path";
3910
3957
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
3911
- var CACHE_VERSION = 2;
3958
+ var CACHE_VERSION = 3;
3912
3959
  async function loadModels(provider, refresh = false) {
3913
3960
  const file = cacheFile(provider.id);
3914
3961
  const cached = await readCache(file);
@@ -4135,13 +4182,15 @@ async function* streamTurn(client, providerId, apiKey2, req) {
4135
4182
  final = await stream.finalMessage();
4136
4183
  } catch (error) {
4137
4184
  if (isUserAbort(error, req.signal)) {
4138
- const partial = stream.currentMessage;
4139
- if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4185
+ const partial2 = stream.currentMessage;
4186
+ if (partial2) yield { type: "usage", usage: toUsage(partial2.usage) };
4140
4187
  const limits3 = parseRateLimits(capture.headers());
4141
4188
  if (limits3) yield { type: "rate_limits", limits: limits3 };
4142
4189
  yield { type: "done", stopReason: "aborted", content: partialContent(thinking, text) };
4143
4190
  return;
4144
4191
  }
4192
+ const partial = stream.currentMessage;
4193
+ if (partial) yield { type: "usage", usage: toUsage(partial.usage) };
4145
4194
  const limits2 = parseRateLimits(capture.headers());
4146
4195
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4147
4196
  if (events === 0 && capture.succeeded() && !isConnectionFailure(error)) {
@@ -4175,7 +4224,7 @@ async function listModels(client, providerId, apiKey2) {
4175
4224
  name: model.display_name,
4176
4225
  contextWindow: model.max_input_tokens ?? void 0,
4177
4226
  maxOutput: model.max_tokens ?? void 0,
4178
- pricing: pricingFor(model.id)
4227
+ pricing: modelInfoFromRaw(model)?.pricing ?? pricingFor(model.id)
4179
4228
  });
4180
4229
  }
4181
4230
  return models;
@@ -4335,6 +4384,7 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4335
4384
  }
4336
4385
  } catch (error) {
4337
4386
  if (!isUserAbort(error, req.signal)) {
4387
+ if (sawUsage) yield { type: "usage", usage };
4338
4388
  const limits2 = parseRateLimits(capture.headers());
4339
4389
  if (limits2) yield { type: "rate_limits", limits: limits2 };
4340
4390
  throw toProviderError(error, providerId, [apiKey2]);
@@ -4350,13 +4400,13 @@ async function* streamTurn2(client, providerId, apiKey2, req) {
4350
4400
  return;
4351
4401
  }
4352
4402
  if (recognised === 0) throw await invalidStreamError(providerId, capture, void 0, [apiKey2]);
4403
+ if (sawUsage) yield { type: "usage", usage };
4353
4404
  const content = toContentBlocks2(providerId, thinking, text, calls);
4354
4405
  for (const block of content) {
4355
4406
  if (block.type === "tool_use") {
4356
4407
  yield { type: "tool_call", id: block.id, name: block.name, input: block.input };
4357
4408
  }
4358
4409
  }
4359
- if (sawUsage) yield { type: "usage", usage };
4360
4410
  const limits = parseRateLimits(capture.headers());
4361
4411
  if (limits) yield { type: "rate_limits", limits };
4362
4412
  yield { type: "done", stopReason: resolveStop(finishReason, calls.size > 0), content };
@@ -5313,7 +5363,7 @@ function createToolRegistry(tools) {
5313
5363
  }
5314
5364
 
5315
5365
  // src/prompts/library.ts
5316
- import { chmod as chmod6, readFile as readFile12, readdir as readdir3, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5366
+ import { chmod as chmod6, readFile as readFile12, readdir as readdir4, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
5317
5367
  import path10 from "path";
5318
5368
  async function savePrompt(input) {
5319
5369
  const slug = slugify(input.name);
@@ -5346,7 +5396,7 @@ async function getPrompt(slug) {
5346
5396
  async function listPrompts() {
5347
5397
  let names;
5348
5398
  try {
5349
- names = await readdir3(promptsDir);
5399
+ names = await readdir4(promptsDir);
5350
5400
  } catch (error) {
5351
5401
  if (!isMissing(error)) throw error;
5352
5402
  return [];
@@ -5418,7 +5468,7 @@ function parse(slug, text) {
5418
5468
  }
5419
5469
 
5420
5470
  // src/skills/library.ts
5421
- import { lstat as lstat4, open as open2, readdir as readdir4 } from "fs/promises";
5471
+ import { lstat as lstat4, open as open2, readdir as readdir5 } from "fs/promises";
5422
5472
  import path11 from "path";
5423
5473
  var SKILL_FILE = "SKILL.md";
5424
5474
  var FRONTMATTER_BYTES = 8192;
@@ -5430,7 +5480,7 @@ async function discoverSkills(dirs) {
5430
5480
  for (const root of dirs) {
5431
5481
  const rootInfo = await lstat4(root).catch(() => null);
5432
5482
  if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink()) continue;
5433
- const entries = await readdir4(root, { withFileTypes: true }).catch(() => []);
5483
+ const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
5434
5484
  const found = await Promise.all(
5435
5485
  entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).slice(0, MAX_SKILLS_PER_ROOT).map((entry) => readMeta(root, rootInfo, path11.join(root, entry.name)))
5436
5486
  );
@@ -6485,6 +6535,7 @@ async function boot(options) {
6485
6535
  async setMaxTokensPerTurn(tokens3) {
6486
6536
  await persistConfig((draft) => {
6487
6537
  draft.budget.maxTokensPerTurn = tokens3;
6538
+ draft.budget.maxCostUsdPerTurn = 0;
6488
6539
  });
6489
6540
  },
6490
6541
  getLang: () => config.lang,
@@ -7016,9 +7067,49 @@ function validateKey(value) {
7016
7067
  // src/app/tui.tsx
7017
7068
  import { render } from "ink";
7018
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
+
7019
7110
  // src/ui/App.tsx
7020
- import { Box as Box13, Text as Text12, useApp, useInput as useInput2, useWindowSize as useWindowSize4 } from "ink";
7021
- 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";
7022
7113
 
7023
7114
  // src/mcp/add.ts
7024
7115
  var SERVER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
@@ -7645,7 +7736,7 @@ import { Box as Box3, Text as Text3 } from "ink";
7645
7736
  import { useState } from "react";
7646
7737
 
7647
7738
  // src/ui/components/Logo.tsx
7648
- import { Box as Box2, Text as Text2, useWindowSize } from "ink";
7739
+ import { Box as Box2, Text as Text2 } from "ink";
7649
7740
 
7650
7741
  // src/ui/workspace.ts
7651
7742
  import { homedir } from "os";
@@ -7692,7 +7783,7 @@ var CAT = [" \u2571\\_\u2571\\ ", " ( o.o )", " > ^ < "];
7692
7783
  function Logo({ subtitle, workspace }) {
7693
7784
  const theme = useTheme();
7694
7785
  const strings = useStrings();
7695
- const { columns } = useWindowSize();
7786
+ const { columns } = useTerminalSize();
7696
7787
  const location = workspace ? formatWorkspacePath(workspace, Math.max(6, Math.min(72, columns - 15))) : "";
7697
7788
  return /* @__PURE__ */ jsxs2(Box2, { width: "100%", marginBottom: 1, children: [
7698
7789
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexShrink: 0, children: CAT.map((line) => /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: line }, line)) }),
@@ -7950,15 +8041,16 @@ function previewText(value, limit = MAX_PREVIEW_CHARS) {
7950
8041
 
7951
8042
  // src/ui/components/Picker.tsx
7952
8043
  import { Box as Box7, Text as Text7 } from "ink";
7953
- import { useMemo, useState as useState3 } from "react";
8044
+ import { useMemo as useMemo2, useState as useState3 } from "react";
7954
8045
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
7955
8046
  var WINDOW = 10;
7956
8047
  function Picker({ title, items, onSelect, onCancel }) {
7957
8048
  const theme = useTheme();
8049
+ const { columns } = useTerminalSize();
7958
8050
  const strings = useStrings();
7959
8051
  const [query, setQuery] = useState3("");
7960
8052
  const [cursor, setCursor] = useState3(0);
7961
- const matches = useMemo(() => {
8053
+ const matches = useMemo2(() => {
7962
8054
  const needle = query.toLowerCase();
7963
8055
  if (needle === "") return items;
7964
8056
  return items.filter(
@@ -7985,7 +8077,7 @@ function Picker({ title, items, onSelect, onCancel }) {
7985
8077
  });
7986
8078
  const start = Math.max(0, Math.min(active2 - WINDOW + 2, matches.length - WINDOW));
7987
8079
  const visible = matches.slice(start, start + WINDOW);
7988
- 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: [
7989
8081
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: theme.accent, children: [
7990
8082
  sanitizeTerminalText(title),
7991
8083
  query !== "" && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
@@ -7995,7 +8087,7 @@ function Picker({ title, items, onSelect, onCancel }) {
7995
8087
  ] }),
7996
8088
  matches.length === 0 ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: strings.noMatches }) : visible.map((item, index) => {
7997
8089
  const selected = start + index === active2;
7998
- 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: [
7999
8091
  truncate2(sanitizeTerminalText(item.label), 70),
8000
8092
  item.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
8001
8093
  " ",
@@ -8051,6 +8143,7 @@ var PromptInput = memo(function PromptInput2({
8051
8143
  attachments = []
8052
8144
  }) {
8053
8145
  const theme = useTheme();
8146
+ const { columns } = useTerminalSize();
8054
8147
  const strings = useStrings();
8055
8148
  const safeValue = sanitizeTerminalText(value);
8056
8149
  const [selectionCursor, setSelectionCursor] = useState4(0);
@@ -8186,7 +8279,7 @@ var PromptInput = memo(function PromptInput2({
8186
8279
  });
8187
8280
  const start = Math.max(0, Math.min(active2 - WINDOW2 + 2, suggestions.length - WINDOW2));
8188
8281
  const visible = suggestions.slice(start, start + WINDOW2);
8189
- 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: [
8190
8283
  /* @__PURE__ */ jsxs8(
8191
8284
  Box8,
8192
8285
  {
@@ -8197,7 +8290,7 @@ var PromptInput = memo(function PromptInput2({
8197
8290
  paddingX: 1,
8198
8291
  children: [
8199
8292
  /* @__PURE__ */ jsx8(Text8, { color: disabled ? "gray" : theme.accent, children: "\u203A " }),
8200
- /* @__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 }) }),
8201
8294
  pending && pending > 0 ? /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
8202
8295
  " \xB7 ",
8203
8296
  strings.queued(pending)
@@ -8490,9 +8583,12 @@ function liveTranscriptRows(rows) {
8490
8583
  return Math.max(1, interactiveViewportRows(rows) - INTERACTIVE_CHROME_ROWS);
8491
8584
  }
8492
8585
  function TerminalViewport({ children, rows }) {
8586
+ const { columns } = useTerminalSize();
8493
8587
  return /* @__PURE__ */ jsx10(
8494
8588
  Box10,
8495
8589
  {
8590
+ width: columns,
8591
+ maxWidth: "100%",
8496
8592
  flexDirection: "column",
8497
8593
  maxHeight: interactiveViewportRows(rows),
8498
8594
  overflowY: "hidden",
@@ -8502,18 +8598,18 @@ function TerminalViewport({ children, rows }) {
8502
8598
  }
8503
8599
 
8504
8600
  // src/ui/components/Transcript.tsx
8505
- import { Box as Box12, Static, Text as Text11, useWindowSize as useWindowSize3 } from "ink";
8506
- 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";
8507
8603
  import Spinner2 from "ink-spinner";
8508
8604
  import stringWidth2 from "string-width";
8509
8605
 
8510
8606
  // src/ui/markdown.tsx
8511
- import { Box as Box11, Text as Text10, useWindowSize as useWindowSize2 } from "ink";
8512
- 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";
8513
8609
  import stringWidth from "string-width";
8514
8610
  import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
8515
8611
  function Markdown({ children }) {
8516
- const blocks = useMemo2(() => extractBlocks(children), [children]);
8612
+ const blocks = useMemo3(() => extractBlocks(children), [children]);
8517
8613
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: blocks.map((block, i) => /* @__PURE__ */ jsx11(BlockView, { block }, i)) });
8518
8614
  }
8519
8615
  function BlockView({ block }) {
@@ -8547,7 +8643,7 @@ function BlockView({ block }) {
8547
8643
  case "code":
8548
8644
  return /* @__PURE__ */ jsx11(CodeBlock, { code: block.text ?? "" });
8549
8645
  case "hr":
8550
- 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 });
8551
8647
  }
8552
8648
  }
8553
8649
  function CodeBlock({ code }) {
@@ -8555,7 +8651,7 @@ function CodeBlock({ code }) {
8555
8651
  return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: lines.map((line, i) => /* @__PURE__ */ jsx11(Text10, { color: "cyan", children: line }, i)) });
8556
8652
  }
8557
8653
  function TableView({ block }) {
8558
- const { columns } = useWindowSize2();
8654
+ const { columns } = useTerminalSize();
8559
8655
  const headers = block.headers ?? [];
8560
8656
  const rows = block.rows ?? [];
8561
8657
  if (headers.length === 0 && rows.length === 0) {
@@ -9128,26 +9224,41 @@ var BubbleView = memo2(function BubbleView2({
9128
9224
  return /* @__PURE__ */ jsx12(AssistantView, { bubble, maxRows });
9129
9225
  }
9130
9226
  if (bubble.kind === "subagent") {
9131
- return /* @__PURE__ */ jsx12(SubagentView, { bubble });
9227
+ return /* @__PURE__ */ jsx12(SubagentView, { bubble, maxRows });
9132
9228
  }
9133
9229
  return /* @__PURE__ */ jsx12(ToolView, { bubble });
9134
9230
  });
9135
- function AssistantView({ bubble, maxRows }) {
9136
- 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();
9137
9245
  const frozenThinking = useRef3(void 0);
9138
9246
  const answering = bubble.streaming && bubble.text !== "";
9139
9247
  if (!answering) frozenThinking.current = void 0;
9140
9248
  if (answering && frozenThinking.current === void 0) {
9141
9249
  frozenThinking.current = bubble.thinking;
9142
9250
  }
9143
- const visibleThinking = assistantThinkingForFrame(bubble, frozenThinking.current);
9251
+ const visibleThinking = sanitizeThinkingText(
9252
+ assistantThinkingForFrame(bubble, frozenThinking.current),
9253
+ bubble.streaming
9254
+ );
9144
9255
  const liveBudget = bubble.streaming && maxRows !== void 0 ? Math.max(1, maxRows - 1) : void 0;
9145
9256
  const thinkingBudget = liveBudget === void 0 ? void 0 : bubble.text !== "" ? Math.min(3, Math.max(0, liveBudget - 1)) : Math.max(0, liveBudget - 1);
9146
9257
  const frameThinking = thinkingBudget === void 0 ? visibleThinking.trim() : clipTextToRows(visibleThinking.trim(), thinkingBudget, columns);
9147
9258
  const usedThinkingRows = frameThinking === "" ? 0 : frameThinking.split("\n").length;
9148
9259
  const frameText = liveBudget === void 0 ? bubble.text : clipTextToRows(bubble.text, Math.max(1, liveBudget - usedThinkingRows), columns);
9149
9260
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9150
- 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 }) }),
9151
9262
  bubble.streaming ? /* @__PURE__ */ jsx12(Text11, { children: frameText }) : /* @__PURE__ */ jsx12(Markdown, { children: bubble.text }),
9152
9263
  bubble.streaming && bubble.text === "" && /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
9153
9264
  /* @__PURE__ */ jsx12(Spinner2, { type: "dots" }),
@@ -9186,7 +9297,7 @@ function ToolView({ bubble }) {
9186
9297
  const mark = bubble.state === "running" ? "\u25CC" : bubble.state === "ok" ? "\u25CF" : "\u2717";
9187
9298
  const color = bubble.state === "error" ? theme.error : bubble.state === "ok" ? theme.ok : theme.warn;
9188
9299
  const display = bubble.display;
9189
- const diff = useMemo3(
9300
+ const diff = useMemo4(
9190
9301
  () => display?.kind === "diff" ? diffLines(display.before, display.after) : null,
9191
9302
  [display]
9192
9303
  );
@@ -9224,20 +9335,20 @@ function previewLines(content) {
9224
9335
  if (lines.length <= 6) return lines;
9225
9336
  return [...lines.slice(0, 6), `\u2026 ${lines.length - 6} more lines`];
9226
9337
  }
9227
- function SubagentView({ bubble }) {
9338
+ function SubagentView({ bubble, maxRows }) {
9228
9339
  const theme = useTheme();
9340
+ const { columns } = useTerminalSize();
9229
9341
  const mark = bubble.state === "running" ? "\u25CC" : "\u25CF";
9230
9342
  const color = bubble.state === "running" ? theme.warn : theme.ok;
9231
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", marginTop: 1, children: [
9232
- /* @__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: [
9233
9347
  mark,
9234
9348
  " subagent: ",
9235
9349
  bubble.description
9236
9350
  ] }),
9237
- /* @__PURE__ */ jsxs11(Box12, { marginLeft: 2, flexDirection: "column", children: [
9238
- bubble.bubbles.map((inner, index) => /* @__PURE__ */ jsx12(BubbleView, { bubble: inner }, index)),
9239
- bubble.state === "done" && bubble.result && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "\u2500\u2500 result \u2500\u2500" }) })
9240
- ] })
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 })
9241
9352
  ] });
9242
9353
  }
9243
9354
 
@@ -9488,7 +9599,7 @@ function App({
9488
9599
  warnings = []
9489
9600
  }) {
9490
9601
  const { exit } = useApp();
9491
- const { rows } = useWindowSize4();
9602
+ const { rows } = useTerminalSize();
9492
9603
  const [transcript, setTranscript] = useState5(
9493
9604
  () => warnings.reduce((state, text) => pushNotice(state, "warn", text), fromHistory(initialHistory))
9494
9605
  );
@@ -9506,6 +9617,7 @@ function App({
9506
9617
  const drainQueueRef = useRef4(() => void 0);
9507
9618
  const [attachments, setAttachments] = useState5([]);
9508
9619
  const attachmentsRef = useRef4([]);
9620
+ const attachmentGeneration = useRef4(0);
9509
9621
  const automaticAttachmentTask = useRef4(null);
9510
9622
  const clipboardPasteTask = useRef4(null);
9511
9623
  const [overlay, setOverlay] = useState5({ kind: "none" });
@@ -9570,8 +9682,8 @@ function App({
9570
9682
  const timer = setInterval(() => tick((n) => n + 1), 1e3);
9571
9683
  return () => clearInterval(timer);
9572
9684
  }, [turnStart]);
9573
- const theme = useMemo4(() => makeTheme(accent), [accent]);
9574
- const strings = useMemo4(() => stringsFor(lang), [lang]);
9685
+ const theme = useMemo5(() => makeTheme(accent), [accent]);
9686
+ const strings = useMemo5(() => stringsFor(lang), [lang]);
9575
9687
  const notice = useCallback(
9576
9688
  (level, text) => {
9577
9689
  flushTranscriptEvents();
@@ -10192,6 +10304,9 @@ ${strings.mcpAddUsage}`
10192
10304
  }
10193
10305
  case "attach": {
10194
10306
  if (rawRest.toLowerCase() === "clear") {
10307
+ attachmentGeneration.current += 1;
10308
+ automaticAttachmentTask.current = null;
10309
+ clipboardPasteTask.current = null;
10195
10310
  replaceAttachments([]);
10196
10311
  notice("info", strings.attachmentsCleared);
10197
10312
  return;
@@ -10203,15 +10318,18 @@ ${strings.mcpAddUsage}`
10203
10318
  }
10204
10319
  if (clipboardPasteTask.current) return;
10205
10320
  const task = runtime.loadClipboardImage().then((block) => {
10321
+ if (clipboardPasteTask.current !== task) return;
10206
10322
  if (appendAttachment(block)) {
10207
10323
  notice(
10208
10324
  "info",
10209
10325
  strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image")
10210
10326
  );
10211
10327
  }
10212
- }).catch(
10213
- (error) => notice("error", error instanceof Error ? error.message : String(error))
10214
- );
10328
+ }).catch((error) => {
10329
+ if (clipboardPasteTask.current === task) {
10330
+ notice("error", error instanceof Error ? error.message : String(error));
10331
+ }
10332
+ });
10215
10333
  clipboardPasteTask.current = task;
10216
10334
  try {
10217
10335
  await task;
@@ -10520,6 +10638,7 @@ Rename the file if you want a different name.`);
10520
10638
  return Promise.resolve(true);
10521
10639
  }
10522
10640
  const task = runtime.loadAutomaticAttachment(requestedPath).then((block) => {
10641
+ if (automaticAttachmentTask.current !== task) return true;
10523
10642
  if (!block) return false;
10524
10643
  if (!appendAttachment(block)) {
10525
10644
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
@@ -10527,9 +10646,9 @@ Rename the file if you want a different name.`);
10527
10646
  }
10528
10647
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "attachment"));
10529
10648
  return true;
10530
- }).catch((error) => {
10531
- notice("error", error instanceof Error ? error.message : String(error));
10532
- return true;
10649
+ }).catch(() => {
10650
+ if (automaticAttachmentTask.current !== task) return true;
10651
+ return false;
10533
10652
  });
10534
10653
  automaticAttachmentTask.current = task;
10535
10654
  void task.finally(() => {
@@ -10546,12 +10665,17 @@ Rename the file if you want a different name.`);
10546
10665
  return;
10547
10666
  }
10548
10667
  const task = runtime.loadClipboardImage().then((block) => {
10668
+ if (clipboardPasteTask.current !== task) return;
10549
10669
  if (!appendAttachment(block)) {
10550
10670
  notice("warn", `At most ${MAX_ATTACHMENTS} attachments can be queued for one message.`);
10551
10671
  return;
10552
10672
  }
10553
10673
  notice("info", strings.attachmentAdded(attachmentLabel(block) ?? "clipboard image"));
10554
- }).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
+ });
10555
10679
  clipboardPasteTask.current = task;
10556
10680
  void task.finally(() => {
10557
10681
  if (clipboardPasteTask.current === task) clipboardPasteTask.current = null;
@@ -10559,6 +10683,7 @@ Rename the file if you want a different name.`);
10559
10683
  }, [appendAttachment, notice, runtime, strings]);
10560
10684
  const submit = useCallback(
10561
10685
  async (raw) => {
10686
+ const generation = attachmentGeneration.current;
10562
10687
  const pendingAttachments = [automaticAttachmentTask.current, clipboardPasteTask.current].filter(
10563
10688
  (task) => task !== null
10564
10689
  );
@@ -10566,6 +10691,7 @@ Rename the file if you want a different name.`);
10566
10691
  if (detachedInput) {
10567
10692
  setInput("");
10568
10693
  await Promise.all(pendingAttachments);
10694
+ if (generation !== attachmentGeneration.current) return;
10569
10695
  }
10570
10696
  const text = raw.trim();
10571
10697
  const submitSlash = () => {
@@ -10581,9 +10707,13 @@ Rename the file if you want a different name.`);
10581
10707
  submitSlash();
10582
10708
  return;
10583
10709
  }
10584
- if (text && looksLikeAttachmentPath(text) && await tryQueueAutomaticAttachment(text)) {
10585
- if (!detachedInput) setInput("");
10586
- return;
10710
+ if (text && looksLikeAttachmentPath(text)) {
10711
+ const attached = await tryQueueAutomaticAttachment(text);
10712
+ if (generation !== attachmentGeneration.current) return;
10713
+ if (attached) {
10714
+ if (!detachedInput) setInput("");
10715
+ return;
10716
+ }
10587
10717
  }
10588
10718
  if (text.startsWith("/")) {
10589
10719
  submitSlash();
@@ -10618,15 +10748,19 @@ Rename the file if you want a different name.`);
10618
10748
  return;
10619
10749
  }
10620
10750
  if (!key.escape) return;
10621
- if (input !== "") {
10622
- setInput("");
10751
+ if (busyRef.current && abort.current) {
10752
+ if (!abort.current.signal.aborted) {
10753
+ abort.current.abort();
10754
+ notice("warn", strings.cancelled);
10755
+ }
10623
10756
  return;
10624
10757
  }
10625
- if (busy && abort.current) {
10626
- abort.current.abort();
10627
- queueRef.current = [];
10628
- setPendingCount(0);
10629
- notice("warn", strings.cancelled);
10758
+ if (input !== "" || attachmentsRef.current.length > 0 || automaticAttachmentTask.current || clipboardPasteTask.current) {
10759
+ attachmentGeneration.current += 1;
10760
+ setInput("");
10761
+ replaceAttachments([]);
10762
+ if (automaticAttachmentTask.current) automaticAttachmentTask.current = null;
10763
+ if (clipboardPasteTask.current) clipboardPasteTask.current = null;
10630
10764
  return;
10631
10765
  }
10632
10766
  if (overlay.kind === "permission") {