miii-agent 0.1.19 → 0.1.21

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.
Files changed (3) hide show
  1. package/README.md +78 -247
  2. package/dist/cli.js +987 -169
  3. package/package.json +4 -2
package/dist/cli.js CHANGED
@@ -34,7 +34,8 @@ function migrate(raw) {
34
34
  model: raw.model,
35
35
  provider: raw.provider,
36
36
  effort: raw.effort,
37
- providers
37
+ providers,
38
+ modelContexts: raw.modelContexts
38
39
  };
39
40
  }
40
41
  function readRawConfig() {
@@ -79,13 +80,17 @@ function setEffort(effort) {
79
80
  function setProvider(provider) {
80
81
  saveConfig({ ...readRawConfig(), provider });
81
82
  }
83
+ function setModelContexts(contexts) {
84
+ const raw = readRawConfig();
85
+ saveConfig({ ...raw, modelContexts: { ...raw.modelContexts, ...contexts } });
86
+ }
82
87
  var EFFORT_OPTIONS, CONFIG_DIR, CONFIG_PATH;
83
88
  var init_config = __esm({
84
89
  "src/config.ts"() {
85
90
  "use strict";
86
91
  EFFORT_OPTIONS = {
87
- low: { temperature: 0.2, num_predict: 1024 },
88
- medium: { temperature: 0.7, num_predict: 2048 },
92
+ low: { temperature: 0.2, num_predict: 8192 },
93
+ medium: { temperature: 0.7, num_predict: 16384 },
89
94
  high: { temperature: 1, num_predict: -1 }
90
95
  };
91
96
  CONFIG_DIR = join(homedir(), ".miii");
@@ -158,30 +163,6 @@ async function modelContext(entry, model) {
158
163
  throw err;
159
164
  }
160
165
  }
161
- async function paramCountB(entry, model) {
162
- try {
163
- const info = await makeClient(entry).show({ model });
164
- const details = info.details;
165
- if (details?.parameter_size) {
166
- const m = details.parameter_size.match(/([\d.]+)\s*([BM])/i);
167
- if (m) {
168
- const n = parseFloat(m[1]);
169
- if (!isNaN(n)) return m[2].toUpperCase() === "M" ? n / 1e3 : n;
170
- }
171
- }
172
- const modelInfo = info.model_info;
173
- if (modelInfo) {
174
- const key = Object.keys(modelInfo).find((k) => k.endsWith("parameter_count"));
175
- if (key) {
176
- const val = Number(modelInfo[key]);
177
- if (!isNaN(val) && val > 0) return val / 1e9;
178
- }
179
- }
180
- return null;
181
- } catch {
182
- return null;
183
- }
184
- }
185
166
  async function* chat(entry, model, messages, tools, opts) {
186
167
  if (opts?.signal?.aborted) return;
187
168
  const signal = opts?.signal;
@@ -211,9 +192,21 @@ async function* chat(entry, model, messages, tools, opts) {
211
192
  };
212
193
  if (opts?.format) req.format = opts.format;
213
194
  else if (tools) req.tools = tools;
214
- stream = await client.chat(
215
- req
216
- );
195
+ try {
196
+ stream = await client.chat(
197
+ req
198
+ );
199
+ } catch (err) {
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ if (!signal?.aborted && msg.toLowerCase().includes("does not support thinking")) {
202
+ delete req.think;
203
+ stream = await client.chat(
204
+ req
205
+ );
206
+ } else {
207
+ throw err;
208
+ }
209
+ }
217
210
  } catch (err) {
218
211
  if (signal?.aborted) return;
219
212
  if (isConnectionError(err)) {
@@ -232,6 +225,7 @@ async function* chat(entry, model, messages, tools, opts) {
232
225
  content: stripHarmony(chunk.message.content),
233
226
  thinking: stripHarmony(chunk.message.thinking),
234
227
  done: chunk.done,
228
+ done_reason: chunk.done_reason,
235
229
  tool_calls: chunk.message.tool_calls,
236
230
  prompt_eval_count: chunk.prompt_eval_count,
237
231
  eval_count: chunk.eval_count
@@ -243,6 +237,11 @@ async function* chat(entry, model, messages, tools, opts) {
243
237
  if (isConnectionError(err)) {
244
238
  throw new Error(NOT_RUNNING);
245
239
  }
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ if (msg.includes("Did not receive done or success response in stream")) {
242
+ yield { content: "", done: true, prompt_eval_count: 0, eval_count: 0 };
243
+ return;
244
+ }
246
245
  throw err;
247
246
  } finally {
248
247
  if (opts?.signal) opts.signal.removeEventListener("abort", onAbort);
@@ -362,6 +361,7 @@ async function* chat2(entry, model, messages, tools, opts) {
362
361
  if (oaTools) body.tools = oaTools;
363
362
  if (opts?.num_predict && opts.num_predict > 0) body.max_tokens = opts.num_predict;
364
363
  const toolCallAccum = /* @__PURE__ */ new Map();
364
+ let lastFinishReason;
365
365
  const TIMEOUT_MS = 18e4;
366
366
  const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS);
367
367
  const combinedSignal = opts?.signal && typeof AbortSignal.any === "function" ? AbortSignal.any([opts.signal, timeoutSignal]) : opts?.signal ?? timeoutSignal;
@@ -401,6 +401,7 @@ async function* chat2(entry, model, messages, tools, opts) {
401
401
  if (!choices || choices.length === 0) continue;
402
402
  const delta = choices[0].delta ?? {};
403
403
  const finishReason = choices[0].finish_reason;
404
+ if (finishReason) lastFinishReason = finishReason;
404
405
  if (delta.content) {
405
406
  yield { content: delta.content, done: false };
406
407
  }
@@ -465,6 +466,9 @@ async function* chat2(entry, model, messages, tools, opts) {
465
466
  yield {
466
467
  content: "",
467
468
  done: true,
469
+ // OpenAI signals a hit token cap as finish_reason 'length'; normalize to the
470
+ // Ollama spelling so the agent loop can detect truncation uniformly.
471
+ done_reason: lastFinishReason === "length" ? "length" : lastFinishReason ?? void 0,
468
472
  tool_calls: toolCalls.length > 0 ? toolCalls : void 0
469
473
  };
470
474
  }
@@ -480,9 +484,6 @@ var init_openai = __esm({
480
484
  function active() {
481
485
  return resolveProvider();
482
486
  }
483
- function providerName() {
484
- return active().name;
485
- }
486
487
  function isAvailable3() {
487
488
  const { entry } = active();
488
489
  return entry.type === "ollama" ? isAvailable(entry) : isAvailable2(entry);
@@ -499,16 +500,6 @@ async function modelContext3(model) {
499
500
  const { entry } = active();
500
501
  return entry.type === "ollama" ? modelContext(entry, model) : modelContext2(entry, model);
501
502
  }
502
- async function modelParamCountB(model) {
503
- const { entry } = active();
504
- if (entry.type !== "ollama") return null;
505
- const key = `${entry.baseUrl}:${model}`;
506
- const cached = paramCountCache.get(key);
507
- if (cached !== void 0) return cached;
508
- const params = await paramCountB(entry, model);
509
- paramCountCache.set(key, params);
510
- return params;
511
- }
512
503
  async function* chat3(model, messages, tools, opts) {
513
504
  const { entry } = active();
514
505
  if (entry.type === "ollama") {
@@ -517,14 +508,12 @@ async function* chat3(model, messages, tools, opts) {
517
508
  yield* chat2(entry, model, messages, tools, opts);
518
509
  }
519
510
  }
520
- var paramCountCache;
521
511
  var init_client = __esm({
522
512
  "src/llm/client.ts"() {
523
513
  "use strict";
524
514
  init_config();
525
515
  init_ollama();
526
516
  init_openai();
527
- paramCountCache = /* @__PURE__ */ new Map();
528
517
  }
529
518
  });
530
519
 
@@ -727,7 +716,10 @@ var init_edit_file = __esm({
727
716
  handler: ({ path, old_str, new_str, replace_all }) => {
728
717
  try {
729
718
  if (old_str === new_str) {
730
- return { content: `old_str and new_str are identical \u2014 nothing to change in ${path}.`, is_error: true };
719
+ return {
720
+ content: `old_str and new_str are identical \u2014 nothing to change in ${path}. If the file is already correct, do NOT edit again: finish with the respond action and tell the user it is done.`,
721
+ is_error: true
722
+ };
731
723
  }
732
724
  const abs = confinePath(path);
733
725
  const src = readFileSync3(abs, "utf-8");
@@ -1196,6 +1188,31 @@ function toZod(schema) {
1196
1188
  }
1197
1189
  return z.object(shape).passthrough();
1198
1190
  }
1191
+ function exampleValue(spec) {
1192
+ if (spec.enum && spec.enum.length) return spec.enum[0];
1193
+ switch (spec.type) {
1194
+ case "number":
1195
+ case "integer":
1196
+ return 0;
1197
+ case "boolean":
1198
+ return false;
1199
+ case "array":
1200
+ return [];
1201
+ case "object":
1202
+ return {};
1203
+ default:
1204
+ return "...";
1205
+ }
1206
+ }
1207
+ function exampleInput(schema) {
1208
+ const required = schema.required ?? [];
1209
+ const obj = {};
1210
+ for (const key of required) {
1211
+ const spec = schema.properties[key];
1212
+ if (spec) obj[key] = exampleValue(spec);
1213
+ }
1214
+ return JSON.stringify(obj);
1215
+ }
1199
1216
  function validateInput(schema, input) {
1200
1217
  const result = toZod(schema).safeParse(input ?? {});
1201
1218
  if (result.success) return null;
@@ -1300,9 +1317,9 @@ After each tool result, answer silently: "Does this result move me toward GOAL?"
1300
1317
  This prevents drift. Each step attends to the original goal, not just the previous step.
1301
1318
 
1302
1319
  # Output format
1303
- - Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
1304
- - This applies to your reasoning/thinking too. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there either.
1305
- - Quote code, paths, and identifiers inline as plain text. Do not wrap them.
1320
+ - Your reply is rendered as Markdown in the terminal. You may use the Markdown the renderer supports: \`#\` headings, \`**bold**\` / \`*italic*\`, \`-\` bullet and numbered lists, fenced \`\`\` code blocks (with a language tag for syntax highlighting), inline backticks, blockquotes, links, and tables.
1321
+ - Use it sparingly and only where it aids clarity. Wrap code, paths, commands, and identifiers in inline backticks; put multi-line code in fenced blocks with a language tag. Keep prose plain \u2014 no decorative formatting.
1322
+ - This does NOT apply to your reasoning/thinking. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there.
1306
1323
  - Keep prose terse.
1307
1324
 
1308
1325
  # Tone and voice
@@ -1592,8 +1609,22 @@ function parseGrammarAction(content, knownToolNames) {
1592
1609
  }
1593
1610
  }
1594
1611
  const name = typeof obj.name === "string" ? obj.name : void 0;
1595
- const args2 = obj.arguments ?? {};
1596
1612
  if (!name) return null;
1613
+ let args2;
1614
+ const wrapped = obj.arguments ?? obj.parameters ?? obj.input ?? obj.args;
1615
+ if (typeof wrapped === "string") {
1616
+ try {
1617
+ const parsed = JSON.parse(wrapped);
1618
+ args2 = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1619
+ } catch {
1620
+ args2 = {};
1621
+ }
1622
+ } else if (wrapped && typeof wrapped === "object" && !Array.isArray(wrapped)) {
1623
+ args2 = wrapped;
1624
+ } else {
1625
+ const { name: _n, ...rest } = obj;
1626
+ args2 = rest;
1627
+ }
1597
1628
  if (name === "respond") {
1598
1629
  const message = typeof args2.message === "string" ? args2.message : "";
1599
1630
  return { kind: "respond", message };
@@ -1685,6 +1716,30 @@ function readGuard(name, input, seen) {
1685
1716
  const verb = name === "edit_file" ? "edit" : "overwrite";
1686
1717
  return `Refusing to ${verb} ${p}: you have not read it this turn. Call read_file on ${p} first, then retry the ${name}.`;
1687
1718
  }
1719
+ function unwrapEnvelope(name, input) {
1720
+ if (!("arguments" in input)) return input;
1721
+ if ("name" in input && input.name !== name) return input;
1722
+ let args2 = input.arguments;
1723
+ if (typeof args2 === "string") {
1724
+ try {
1725
+ args2 = JSON.parse(args2);
1726
+ } catch {
1727
+ return input;
1728
+ }
1729
+ }
1730
+ if (args2 && typeof args2 === "object" && !Array.isArray(args2)) return args2;
1731
+ return input;
1732
+ }
1733
+ function splitWriteHint(name, cause) {
1734
+ const lead = cause === "truncated" ? `Your response was cut off at the output token limit, so this ${name} call is incomplete and was NOT run.` : `Your ${name} call arrived with missing or garbled arguments \u2014 usually the response was cut off or mangled while writing a large value. It was NOT run.`;
1735
+ return `${lead} Do not resend the whole file in one call. Instead create the file with write_file containing only the first portion, then append the rest with successive edit_file calls. Keep each call small.`;
1736
+ }
1737
+ function looksTruncatedWrite(name, input) {
1738
+ if (!BIG_WRITE_TOOLS.has(name)) return false;
1739
+ if (typeof input.path !== "string" || !input.path) return true;
1740
+ if (name === "write_file" && typeof input.content !== "string") return true;
1741
+ return false;
1742
+ }
1688
1743
  function markSeen(name, input, seen) {
1689
1744
  if (name !== "read_file" && name !== "edit_file" && name !== "write_file") return;
1690
1745
  const p = input.path;
@@ -1697,11 +1752,7 @@ function markSeen(name, input, seen) {
1697
1752
  async function* runAgent(opts) {
1698
1753
  const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
1699
1754
  const startTime = Date.now();
1700
- let useGrammar = false;
1701
- if (providerName() === "ollama") {
1702
- const params = await modelParamCountB(model);
1703
- useGrammar = params == null || params <= GRAMMAR_MAX_PARAMS_B;
1704
- }
1755
+ const useGrammar = false;
1705
1756
  const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd), useGrammar);
1706
1757
  const grammar = useGrammar ? buildToolGrammar(TOOLS) : void 0;
1707
1758
  const ollamaTools = toOllamaTools(TOOLS);
@@ -1721,9 +1772,11 @@ async function* runAgent(opts) {
1721
1772
  let tool_calls;
1722
1773
  let respondEmitted = 0;
1723
1774
  let streamedRespond = false;
1775
+ let emittedText = false;
1724
1776
  let lastTail = "";
1725
1777
  let tailRepeats = 0;
1726
1778
  let streamLooped = false;
1779
+ let truncated = false;
1727
1780
  const ac = new AbortController();
1728
1781
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1729
1782
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
@@ -1733,12 +1786,14 @@ async function* runAgent(opts) {
1733
1786
  if (chunk.content) {
1734
1787
  text += chunk.content;
1735
1788
  if (!useGrammar) {
1789
+ emittedText = true;
1736
1790
  yield { type: "text-delta", text: chunk.content };
1737
1791
  } else {
1738
1792
  const r = streamRespondMessage(text);
1739
1793
  if (r) {
1740
1794
  streamedRespond = true;
1741
1795
  if (r.message.length > respondEmitted) {
1796
+ emittedText = true;
1742
1797
  yield { type: "text-delta", text: r.message.slice(respondEmitted) };
1743
1798
  respondEmitted = r.message.length;
1744
1799
  }
@@ -1759,7 +1814,7 @@ async function* runAgent(opts) {
1759
1814
  }
1760
1815
  }
1761
1816
  }
1762
- if (chunk.thinking) {
1817
+ if (chunk.thinking && !emittedText) {
1763
1818
  yield { type: "thinking-delta", text: chunk.thinking };
1764
1819
  }
1765
1820
  if (chunk.tool_calls && chunk.tool_calls.length > 0) {
@@ -1768,6 +1823,7 @@ async function* runAgent(opts) {
1768
1823
  if (chunk.done) {
1769
1824
  promptTokens += chunk.prompt_eval_count ?? 0;
1770
1825
  evalTokens += chunk.eval_count ?? 0;
1826
+ if (chunk.done_reason === "length") truncated = true;
1771
1827
  }
1772
1828
  }
1773
1829
  } catch (err) {
@@ -1806,6 +1862,19 @@ async function* runAgent(opts) {
1806
1862
  }
1807
1863
  const tool_uses = blocks.filter((b) => b.type === "tool_use");
1808
1864
  history.push({ role: "assistant", content: blocks });
1865
+ if (truncated && tool_uses.length > 0) {
1866
+ const results2 = tool_uses.map((use) => ({
1867
+ type: "tool_result",
1868
+ tool_use_id: use.id,
1869
+ content: splitWriteHint(use.name, "truncated"),
1870
+ is_error: true
1871
+ }));
1872
+ for (const u of tool_uses) yield { type: "tool-use", block: u };
1873
+ for (const r of results2) yield { type: "tool-result", block: r };
1874
+ history.push({ role: "user", content: results2 });
1875
+ yield { type: "turn-end", stop_reason: "tool_use" };
1876
+ continue;
1877
+ }
1809
1878
  if (tool_uses.length === 0) {
1810
1879
  yield { type: "turn-end", stop_reason: "end_turn" };
1811
1880
  break;
@@ -1840,12 +1909,14 @@ async function* runAgent(opts) {
1840
1909
  yield { type: "tool-result", block: r2 };
1841
1910
  continue;
1842
1911
  }
1912
+ use.input = unwrapEnvelope(use.name, use.input);
1843
1913
  const invalid = validateInput(tool.input_schema, use.input);
1844
1914
  if (invalid) {
1915
+ const content = looksTruncatedWrite(use.name, use.input) ? splitWriteHint(use.name, "garbled") : `${invalid} for ${use.name}. Pass the arguments directly as the tool input \u2014 do NOT wrap them in {"name":...,"arguments":...}. Correct shape: ${exampleInput(tool.input_schema)}. Retry with all required fields.`;
1845
1916
  const r2 = {
1846
1917
  type: "tool_result",
1847
1918
  tool_use_id: use.id,
1848
- content: `${invalid} for ${use.name}.`,
1919
+ content,
1849
1920
  is_error: true
1850
1921
  };
1851
1922
  results.push(r2);
@@ -1912,7 +1983,7 @@ async function* runAgent(opts) {
1912
1983
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1913
1984
  return history;
1914
1985
  }
1915
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B;
1986
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, BIG_WRITE_TOOLS;
1916
1987
  var init_loop = __esm({
1917
1988
  "src/agent/loop.ts"() {
1918
1989
  "use strict";
@@ -1929,7 +2000,7 @@ var init_loop = __esm({
1929
2000
  MAX_TURNS = 25;
1930
2001
  REPEAT_TAIL = 120;
1931
2002
  REPEAT_KILL = 4;
1932
- GRAMMAR_MAX_PARAMS_B = 14;
2003
+ BIG_WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
1933
2004
  }
1934
2005
  });
1935
2006
 
@@ -2159,7 +2230,7 @@ import { createElement } from "react";
2159
2230
  init_client();
2160
2231
  init_config();
2161
2232
  import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
2162
- import { Box as Box10, Text as Text10, useApp } from "ink";
2233
+ import { Box as Box13, Text as Text13, useApp } from "ink";
2163
2234
  import { homedir as homedir6 } from "os";
2164
2235
  import { sep as sep2 } from "path";
2165
2236
 
@@ -2201,7 +2272,7 @@ import { memo, useEffect, useState } from "react";
2201
2272
  import { Box as Box2, Text as Text2 } from "ink";
2202
2273
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2203
2274
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2204
- var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
2275
+ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel }) {
2205
2276
  const [frame, setFrame] = useState(0);
2206
2277
  useEffect(() => {
2207
2278
  if (!disabled) return;
@@ -2224,8 +2295,17 @@ var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
2224
2295
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (esc to cancel)" })
2225
2296
  ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2226
2297
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "> " }),
2227
- /* @__PURE__ */ jsx2(Text2, { children: input }),
2228
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u258C" })
2298
+ (() => {
2299
+ const pos = Math.max(0, Math.min(caret ?? input.length, input.length));
2300
+ const before = input.slice(0, pos);
2301
+ const at = input.slice(pos, pos + 1) || " ";
2302
+ const after = input.slice(pos + 1);
2303
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2304
+ /* @__PURE__ */ jsx2(Text2, { children: before }),
2305
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2306
+ /* @__PURE__ */ jsx2(Text2, { children: after })
2307
+ ] });
2308
+ })()
2229
2309
  ] })
2230
2310
  }
2231
2311
  );
@@ -2634,9 +2714,565 @@ function FilePicker({ matches: matches2, cursor }) {
2634
2714
  }
2635
2715
 
2636
2716
  // src/ui/ChatView.tsx
2637
- import { memo as memo2, useState as useState3, useEffect as useEffect3 } from "react";
2638
- import { Box as Box9, Text as Text9 } from "ink";
2639
- import { highlight } from "cli-highlight";
2717
+ import { Box as Box12, Text as Text12, Static } from "ink";
2718
+
2719
+ // src/ui/markdown.ts
2720
+ import { Marked } from "marked";
2721
+ import { markedTerminal } from "marked-terminal";
2722
+ import { highlight, supportsLanguage } from "cli-highlight";
2723
+
2724
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
2725
+ var ANSI_BACKGROUND_OFFSET = 10;
2726
+ var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
2727
+ var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
2728
+ var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
2729
+ var styles = {
2730
+ modifier: {
2731
+ reset: [0, 0],
2732
+ // 21 isn't widely supported and 22 does the same thing
2733
+ bold: [1, 22],
2734
+ dim: [2, 22],
2735
+ italic: [3, 23],
2736
+ underline: [4, 24],
2737
+ overline: [53, 55],
2738
+ inverse: [7, 27],
2739
+ hidden: [8, 28],
2740
+ strikethrough: [9, 29]
2741
+ },
2742
+ color: {
2743
+ black: [30, 39],
2744
+ red: [31, 39],
2745
+ green: [32, 39],
2746
+ yellow: [33, 39],
2747
+ blue: [34, 39],
2748
+ magenta: [35, 39],
2749
+ cyan: [36, 39],
2750
+ white: [37, 39],
2751
+ // Bright color
2752
+ blackBright: [90, 39],
2753
+ gray: [90, 39],
2754
+ // Alias of `blackBright`
2755
+ grey: [90, 39],
2756
+ // Alias of `blackBright`
2757
+ redBright: [91, 39],
2758
+ greenBright: [92, 39],
2759
+ yellowBright: [93, 39],
2760
+ blueBright: [94, 39],
2761
+ magentaBright: [95, 39],
2762
+ cyanBright: [96, 39],
2763
+ whiteBright: [97, 39]
2764
+ },
2765
+ bgColor: {
2766
+ bgBlack: [40, 49],
2767
+ bgRed: [41, 49],
2768
+ bgGreen: [42, 49],
2769
+ bgYellow: [43, 49],
2770
+ bgBlue: [44, 49],
2771
+ bgMagenta: [45, 49],
2772
+ bgCyan: [46, 49],
2773
+ bgWhite: [47, 49],
2774
+ // Bright color
2775
+ bgBlackBright: [100, 49],
2776
+ bgGray: [100, 49],
2777
+ // Alias of `bgBlackBright`
2778
+ bgGrey: [100, 49],
2779
+ // Alias of `bgBlackBright`
2780
+ bgRedBright: [101, 49],
2781
+ bgGreenBright: [102, 49],
2782
+ bgYellowBright: [103, 49],
2783
+ bgBlueBright: [104, 49],
2784
+ bgMagentaBright: [105, 49],
2785
+ bgCyanBright: [106, 49],
2786
+ bgWhiteBright: [107, 49]
2787
+ }
2788
+ };
2789
+ var modifierNames = Object.keys(styles.modifier);
2790
+ var foregroundColorNames = Object.keys(styles.color);
2791
+ var backgroundColorNames = Object.keys(styles.bgColor);
2792
+ var colorNames = [...foregroundColorNames, ...backgroundColorNames];
2793
+ function assembleStyles() {
2794
+ const codes = /* @__PURE__ */ new Map();
2795
+ for (const [groupName, group] of Object.entries(styles)) {
2796
+ for (const [styleName, style] of Object.entries(group)) {
2797
+ styles[styleName] = {
2798
+ open: `\x1B[${style[0]}m`,
2799
+ close: `\x1B[${style[1]}m`
2800
+ };
2801
+ group[styleName] = styles[styleName];
2802
+ codes.set(style[0], style[1]);
2803
+ }
2804
+ Object.defineProperty(styles, groupName, {
2805
+ value: group,
2806
+ enumerable: false
2807
+ });
2808
+ }
2809
+ Object.defineProperty(styles, "codes", {
2810
+ value: codes,
2811
+ enumerable: false
2812
+ });
2813
+ styles.color.close = "\x1B[39m";
2814
+ styles.bgColor.close = "\x1B[49m";
2815
+ styles.color.ansi = wrapAnsi16();
2816
+ styles.color.ansi256 = wrapAnsi256();
2817
+ styles.color.ansi16m = wrapAnsi16m();
2818
+ styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
2819
+ styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
2820
+ styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
2821
+ Object.defineProperties(styles, {
2822
+ rgbToAnsi256: {
2823
+ value(red, green, blue) {
2824
+ if (red === green && green === blue) {
2825
+ if (red < 8) {
2826
+ return 16;
2827
+ }
2828
+ if (red > 248) {
2829
+ return 231;
2830
+ }
2831
+ return Math.round((red - 8) / 247 * 24) + 232;
2832
+ }
2833
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
2834
+ },
2835
+ enumerable: false
2836
+ },
2837
+ hexToRgb: {
2838
+ value(hex) {
2839
+ const matches2 = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
2840
+ if (!matches2) {
2841
+ return [0, 0, 0];
2842
+ }
2843
+ let [colorString] = matches2;
2844
+ if (colorString.length === 3) {
2845
+ colorString = [...colorString].map((character) => character + character).join("");
2846
+ }
2847
+ const integer = Number.parseInt(colorString, 16);
2848
+ return [
2849
+ /* eslint-disable no-bitwise */
2850
+ integer >> 16 & 255,
2851
+ integer >> 8 & 255,
2852
+ integer & 255
2853
+ /* eslint-enable no-bitwise */
2854
+ ];
2855
+ },
2856
+ enumerable: false
2857
+ },
2858
+ hexToAnsi256: {
2859
+ value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
2860
+ enumerable: false
2861
+ },
2862
+ ansi256ToAnsi: {
2863
+ value(code) {
2864
+ if (code < 8) {
2865
+ return 30 + code;
2866
+ }
2867
+ if (code < 16) {
2868
+ return 90 + (code - 8);
2869
+ }
2870
+ let red;
2871
+ let green;
2872
+ let blue;
2873
+ if (code >= 232) {
2874
+ red = ((code - 232) * 10 + 8) / 255;
2875
+ green = red;
2876
+ blue = red;
2877
+ } else {
2878
+ code -= 16;
2879
+ const remainder = code % 36;
2880
+ red = Math.floor(code / 36) / 5;
2881
+ green = Math.floor(remainder / 6) / 5;
2882
+ blue = remainder % 6 / 5;
2883
+ }
2884
+ const value = Math.max(red, green, blue) * 2;
2885
+ if (value === 0) {
2886
+ return 30;
2887
+ }
2888
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
2889
+ if (value === 2) {
2890
+ result += 60;
2891
+ }
2892
+ return result;
2893
+ },
2894
+ enumerable: false
2895
+ },
2896
+ rgbToAnsi: {
2897
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
2898
+ enumerable: false
2899
+ },
2900
+ hexToAnsi: {
2901
+ value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
2902
+ enumerable: false
2903
+ }
2904
+ });
2905
+ return styles;
2906
+ }
2907
+ var ansiStyles = assembleStyles();
2908
+ var ansi_styles_default = ansiStyles;
2909
+
2910
+ // node_modules/chalk/source/vendor/supports-color/index.js
2911
+ import process2 from "process";
2912
+ import os from "os";
2913
+ import tty from "tty";
2914
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
2915
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
2916
+ const position = argv.indexOf(prefix + flag);
2917
+ const terminatorPosition = argv.indexOf("--");
2918
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
2919
+ }
2920
+ var { env } = process2;
2921
+ var flagForceColor;
2922
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
2923
+ flagForceColor = 0;
2924
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
2925
+ flagForceColor = 1;
2926
+ }
2927
+ function envForceColor() {
2928
+ if ("FORCE_COLOR" in env) {
2929
+ if (env.FORCE_COLOR === "true") {
2930
+ return 1;
2931
+ }
2932
+ if (env.FORCE_COLOR === "false") {
2933
+ return 0;
2934
+ }
2935
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
2936
+ }
2937
+ }
2938
+ function translateLevel(level) {
2939
+ if (level === 0) {
2940
+ return false;
2941
+ }
2942
+ return {
2943
+ level,
2944
+ hasBasic: true,
2945
+ has256: level >= 2,
2946
+ has16m: level >= 3
2947
+ };
2948
+ }
2949
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
2950
+ const noFlagForceColor = envForceColor();
2951
+ if (noFlagForceColor !== void 0) {
2952
+ flagForceColor = noFlagForceColor;
2953
+ }
2954
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
2955
+ if (forceColor === 0) {
2956
+ return 0;
2957
+ }
2958
+ if (sniffFlags) {
2959
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
2960
+ return 3;
2961
+ }
2962
+ if (hasFlag("color=256")) {
2963
+ return 2;
2964
+ }
2965
+ }
2966
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
2967
+ return 1;
2968
+ }
2969
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
2970
+ return 0;
2971
+ }
2972
+ const min = forceColor || 0;
2973
+ if (env.TERM === "dumb") {
2974
+ return min;
2975
+ }
2976
+ if (process2.platform === "win32") {
2977
+ const osRelease = os.release().split(".");
2978
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
2979
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
2980
+ }
2981
+ return 1;
2982
+ }
2983
+ if ("CI" in env) {
2984
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
2985
+ return 3;
2986
+ }
2987
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
2988
+ return 1;
2989
+ }
2990
+ return min;
2991
+ }
2992
+ if ("TEAMCITY_VERSION" in env) {
2993
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
2994
+ }
2995
+ if (env.COLORTERM === "truecolor") {
2996
+ return 3;
2997
+ }
2998
+ if (env.TERM === "xterm-kitty") {
2999
+ return 3;
3000
+ }
3001
+ if (env.TERM === "xterm-ghostty") {
3002
+ return 3;
3003
+ }
3004
+ if (env.TERM === "wezterm") {
3005
+ return 3;
3006
+ }
3007
+ if ("TERM_PROGRAM" in env) {
3008
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
3009
+ switch (env.TERM_PROGRAM) {
3010
+ case "iTerm.app": {
3011
+ return version >= 3 ? 3 : 2;
3012
+ }
3013
+ case "Apple_Terminal": {
3014
+ return 2;
3015
+ }
3016
+ }
3017
+ }
3018
+ if (/-256(color)?$/i.test(env.TERM)) {
3019
+ return 2;
3020
+ }
3021
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
3022
+ return 1;
3023
+ }
3024
+ if ("COLORTERM" in env) {
3025
+ return 1;
3026
+ }
3027
+ return min;
3028
+ }
3029
+ function createSupportsColor(stream, options = {}) {
3030
+ const level = _supportsColor(stream, {
3031
+ streamIsTTY: stream && stream.isTTY,
3032
+ ...options
3033
+ });
3034
+ return translateLevel(level);
3035
+ }
3036
+ var supportsColor = {
3037
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
3038
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
3039
+ };
3040
+ var supports_color_default = supportsColor;
3041
+
3042
+ // node_modules/chalk/source/utilities.js
3043
+ function stringReplaceAll(string, substring, replacer) {
3044
+ let index = string.indexOf(substring);
3045
+ if (index === -1) {
3046
+ return string;
3047
+ }
3048
+ const substringLength = substring.length;
3049
+ let endIndex = 0;
3050
+ let returnValue = "";
3051
+ do {
3052
+ returnValue += string.slice(endIndex, index) + substring + replacer;
3053
+ endIndex = index + substringLength;
3054
+ index = string.indexOf(substring, endIndex);
3055
+ } while (index !== -1);
3056
+ returnValue += string.slice(endIndex);
3057
+ return returnValue;
3058
+ }
3059
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
3060
+ let endIndex = 0;
3061
+ let returnValue = "";
3062
+ do {
3063
+ const gotCR = string[index - 1] === "\r";
3064
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
3065
+ endIndex = index + 1;
3066
+ index = string.indexOf("\n", endIndex);
3067
+ } while (index !== -1);
3068
+ returnValue += string.slice(endIndex);
3069
+ return returnValue;
3070
+ }
3071
+
3072
+ // node_modules/chalk/source/index.js
3073
+ var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
3074
+ var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
3075
+ var STYLER = /* @__PURE__ */ Symbol("STYLER");
3076
+ var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
3077
+ var levelMapping = [
3078
+ "ansi",
3079
+ "ansi",
3080
+ "ansi256",
3081
+ "ansi16m"
3082
+ ];
3083
+ var styles2 = /* @__PURE__ */ Object.create(null);
3084
+ var applyOptions = (object, options = {}) => {
3085
+ if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
3086
+ throw new Error("The `level` option should be an integer from 0 to 3");
3087
+ }
3088
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
3089
+ object.level = options.level === void 0 ? colorLevel : options.level;
3090
+ };
3091
+ var chalkFactory = (options) => {
3092
+ const chalk2 = (...strings) => strings.join(" ");
3093
+ applyOptions(chalk2, options);
3094
+ Object.setPrototypeOf(chalk2, createChalk.prototype);
3095
+ return chalk2;
3096
+ };
3097
+ function createChalk(options) {
3098
+ return chalkFactory(options);
3099
+ }
3100
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
3101
+ for (const [styleName, style] of Object.entries(ansi_styles_default)) {
3102
+ styles2[styleName] = {
3103
+ get() {
3104
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
3105
+ Object.defineProperty(this, styleName, { value: builder });
3106
+ return builder;
3107
+ }
3108
+ };
3109
+ }
3110
+ styles2.visible = {
3111
+ get() {
3112
+ const builder = createBuilder(this, this[STYLER], true);
3113
+ Object.defineProperty(this, "visible", { value: builder });
3114
+ return builder;
3115
+ }
3116
+ };
3117
+ var getModelAnsi = (model, level, type, ...arguments_) => {
3118
+ if (model === "rgb") {
3119
+ if (level === "ansi16m") {
3120
+ return ansi_styles_default[type].ansi16m(...arguments_);
3121
+ }
3122
+ if (level === "ansi256") {
3123
+ return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
3124
+ }
3125
+ return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
3126
+ }
3127
+ if (model === "hex") {
3128
+ return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
3129
+ }
3130
+ return ansi_styles_default[type][model](...arguments_);
3131
+ };
3132
+ var usedModels = ["rgb", "hex", "ansi256"];
3133
+ for (const model of usedModels) {
3134
+ styles2[model] = {
3135
+ get() {
3136
+ const { level } = this;
3137
+ return function(...arguments_) {
3138
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
3139
+ return createBuilder(this, styler, this[IS_EMPTY]);
3140
+ };
3141
+ }
3142
+ };
3143
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
3144
+ styles2[bgModel] = {
3145
+ get() {
3146
+ const { level } = this;
3147
+ return function(...arguments_) {
3148
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
3149
+ return createBuilder(this, styler, this[IS_EMPTY]);
3150
+ };
3151
+ }
3152
+ };
3153
+ }
3154
+ var proto = Object.defineProperties(() => {
3155
+ }, {
3156
+ ...styles2,
3157
+ level: {
3158
+ enumerable: true,
3159
+ get() {
3160
+ return this[GENERATOR].level;
3161
+ },
3162
+ set(level) {
3163
+ this[GENERATOR].level = level;
3164
+ }
3165
+ }
3166
+ });
3167
+ var createStyler = (open, close, parent) => {
3168
+ let openAll;
3169
+ let closeAll;
3170
+ if (parent === void 0) {
3171
+ openAll = open;
3172
+ closeAll = close;
3173
+ } else {
3174
+ openAll = parent.openAll + open;
3175
+ closeAll = close + parent.closeAll;
3176
+ }
3177
+ return {
3178
+ open,
3179
+ close,
3180
+ openAll,
3181
+ closeAll,
3182
+ parent
3183
+ };
3184
+ };
3185
+ var createBuilder = (self, _styler, _isEmpty) => {
3186
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
3187
+ Object.setPrototypeOf(builder, proto);
3188
+ builder[GENERATOR] = self;
3189
+ builder[STYLER] = _styler;
3190
+ builder[IS_EMPTY] = _isEmpty;
3191
+ return builder;
3192
+ };
3193
+ var applyStyle = (self, string) => {
3194
+ if (self.level <= 0 || !string) {
3195
+ return self[IS_EMPTY] ? "" : string;
3196
+ }
3197
+ let styler = self[STYLER];
3198
+ if (styler === void 0) {
3199
+ return string;
3200
+ }
3201
+ const { openAll, closeAll } = styler;
3202
+ if (string.includes("\x1B")) {
3203
+ while (styler !== void 0) {
3204
+ string = stringReplaceAll(string, styler.close, styler.open);
3205
+ styler = styler.parent;
3206
+ }
3207
+ }
3208
+ const lfIndex = string.indexOf("\n");
3209
+ if (lfIndex !== -1) {
3210
+ string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
3211
+ }
3212
+ return openAll + string + closeAll;
3213
+ };
3214
+ Object.defineProperties(createChalk.prototype, styles2);
3215
+ var chalk = createChalk();
3216
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3217
+ var source_default = chalk;
3218
+
3219
+ // src/ui/markdown.ts
3220
+ var theme = {
3221
+ heading: source_default.hex("#7fa8d4").bold,
3222
+ // dusty blue
3223
+ firstHeading: source_default.hex("#9ab8de").bold,
3224
+ // slightly lighter for h1
3225
+ strong: source_default.hex("#d6c9a8").bold,
3226
+ // warm sand
3227
+ em: source_default.hex("#b59ec4").italic,
3228
+ // soft mauve
3229
+ del: source_default.hex("#6b7280").strikethrough,
3230
+ // dim gray
3231
+ codespan: source_default.hex("#c8a98a"),
3232
+ // muted clay
3233
+ link: source_default.hex("#83b3a6").underline,
3234
+ // sage teal
3235
+ href: source_default.hex("#83b3a6").underline,
3236
+ blockquote: source_default.hex("#8a9aa8").italic,
3237
+ // slate
3238
+ listitem: source_default.hex("#c4c9cf"),
3239
+ // off-white
3240
+ paragraph: source_default.hex("#c4c9cf"),
3241
+ // off-white body text
3242
+ hr: source_default.hex("#4b5563")
3243
+ // faint rule
3244
+ };
3245
+ function highlightCode(code, lang) {
3246
+ if (!lang || !supportsLanguage(lang)) return code;
3247
+ try {
3248
+ return highlight(code, { language: lang, ignoreIllegals: true });
3249
+ } catch {
3250
+ return code;
3251
+ }
3252
+ }
3253
+ var md = new Marked();
3254
+ md.use(
3255
+ markedTerminal({
3256
+ // Drop the literal `#` prefix on headings; render them styled instead.
3257
+ showSectionPrefix: false,
3258
+ // marked-terminal calls this for ``` blocks; fall back to plain on unknown lang.
3259
+ code: (code, lang) => highlightCode(code, lang),
3260
+ ...theme
3261
+ })
3262
+ );
3263
+ function renderMarkdown(content) {
3264
+ try {
3265
+ const out = md.parse(content, { async: false });
3266
+ return out.replace(/\n+$/, "");
3267
+ } catch {
3268
+ return content;
3269
+ }
3270
+ }
3271
+ function renderMarkdownStreaming(content) {
3272
+ const fences = (content.match(/^```/gm) ?? []).length;
3273
+ const balanced = fences % 2 === 1 ? content + "\n```" : content;
3274
+ return renderMarkdown(balanced);
3275
+ }
2640
3276
 
2641
3277
  // src/ui/ThinkingBlock.tsx
2642
3278
  import { useState as useState2, useEffect as useEffect2 } from "react";
@@ -2680,7 +3316,12 @@ function ThinkingBlock({ content }) {
2680
3316
  " thoughts"
2681
3317
  ] })
2682
3318
  ] }),
2683
- visible && content ? /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: content }) }) : null
3319
+ visible && content ? (() => {
3320
+ const max = Math.max(4, (process.stdout.rows ?? 24) - 10);
3321
+ const lines = content.split("\n");
3322
+ const shown = lines.length > max ? lines.slice(-max) : lines;
3323
+ return /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: shown.join("\n") }) });
3324
+ })() : null
2684
3325
  ] });
2685
3326
  }
2686
3327
 
@@ -2694,9 +3335,16 @@ var EMPTY_STATE_HINTS = [
2694
3335
  ];
2695
3336
  var EMPTY_STATE_TITLE = "Ask anything, or try:";
2696
3337
 
2697
- // src/ui/ChatView.tsx
2698
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2699
- var COLLAPSED_LINES = 3;
3338
+ // src/ui/Message.tsx
3339
+ import { memo as memo2 } from "react";
3340
+ import { Box as Box10, Text as Text10 } from "ink";
3341
+
3342
+ // src/ui/ToolBlock.tsx
3343
+ import { Box as Box9, Text as Text9 } from "ink";
3344
+ import { highlight as highlight2, supportsLanguage as supportsLanguage2 } from "cli-highlight";
3345
+
3346
+ // src/ui/toolExpand.ts
3347
+ import { useState as useState3, useEffect as useEffect3 } from "react";
2700
3348
  var globalToolExpanded = false;
2701
3349
  var toolExpandListeners = /* @__PURE__ */ new Set();
2702
3350
  function toggleToolExpanded() {
@@ -2714,6 +3362,8 @@ function useToolExpanded() {
2714
3362
  }, []);
2715
3363
  return expanded;
2716
3364
  }
3365
+
3366
+ // src/ui/layout.ts
2717
3367
  function formatTokens(n) {
2718
3368
  if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
2719
3369
  return String(n);
@@ -2729,6 +3379,61 @@ function countLines(s) {
2729
3379
  if (!s) return 0;
2730
3380
  return s.split("\n").length;
2731
3381
  }
3382
+ function truncate2(s, max) {
3383
+ if (s.length <= max) return s;
3384
+ return s.slice(0, max - 1) + "\u2026";
3385
+ }
3386
+ function clipTail(rendered, max) {
3387
+ const lines = rendered.split("\n");
3388
+ if (lines.length <= max) return { text: rendered, clipped: 0 };
3389
+ return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
3390
+ }
3391
+ function liveFrameRows() {
3392
+ const rows = process.stdout.rows ?? 24;
3393
+ return Math.max(6, rows - 8);
3394
+ }
3395
+ var COLLAPSED_LINES = 3;
3396
+ function estimateToolRows(use, result) {
3397
+ const input = use.input ?? {};
3398
+ const noErr = !result?.is_error;
3399
+ if (use.name === "write_file" && noErr) {
3400
+ const total = countLines(String(input.content ?? ""));
3401
+ const shown = Math.min(total, COLLAPSED_LINES);
3402
+ return 2 + shown + (total > shown ? 1 : 0);
3403
+ }
3404
+ if (use.name === "edit_file" && noErr) {
3405
+ const total = countLines(String(input.old_str ?? "")) + countLines(String(input.new_str ?? ""));
3406
+ const shown = Math.min(total, COLLAPSED_LINES);
3407
+ return 2 + shown + (total > shown ? 1 : 0);
3408
+ }
3409
+ let rows = 1;
3410
+ if (result) {
3411
+ const lines = (result.content ?? "").split("\n");
3412
+ const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
3413
+ if (multi) {
3414
+ const shown = Math.min(lines.length, COLLAPSED_LINES);
3415
+ rows += 1 + shown + (lines.length > shown ? 1 : 0);
3416
+ } else {
3417
+ rows += 1;
3418
+ }
3419
+ }
3420
+ return rows;
3421
+ }
3422
+ function contentWidth() {
3423
+ return Math.max(20, (process.stdout.columns ?? 80) - 4);
3424
+ }
3425
+
3426
+ // src/ui/ToolBlock.tsx
3427
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3428
+ var COLLAPSED_LINES2 = 3;
3429
+ var TOOL_LABEL = {
3430
+ write_file: "Write",
3431
+ edit_file: "Update",
3432
+ read_file: "Read",
3433
+ run_bash: "Bash",
3434
+ glob: "Glob",
3435
+ grep: "Grep"
3436
+ };
2732
3437
  var EXT_LANG = {
2733
3438
  ts: "typescript",
2734
3439
  tsx: "typescript",
@@ -2771,9 +3476,9 @@ function langFromPath(path) {
2771
3476
  return ext ? EXT_LANG[ext] : void 0;
2772
3477
  }
2773
3478
  function highlightLine(text, lang) {
2774
- if (!lang) return text;
3479
+ if (!lang || !supportsLanguage2(lang)) return text;
2775
3480
  try {
2776
- return highlight(text, { language: lang, ignoreIllegals: true });
3481
+ return highlight2(text, { language: lang, ignoreIllegals: true });
2777
3482
  } catch {
2778
3483
  return text;
2779
3484
  }
@@ -2786,7 +3491,7 @@ function FileEditBlock({
2786
3491
  previewLines
2787
3492
  }) {
2788
3493
  const expanded = useToolExpanded();
2789
- const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
3494
+ const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES2);
2790
3495
  const extra = previewLines.length - shown.length;
2791
3496
  const lang = langFromPath(path);
2792
3497
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
@@ -2829,18 +3534,6 @@ function FileEditBlock({
2829
3534
  ] }) })
2830
3535
  ] });
2831
3536
  }
2832
- var TOOL_LABEL = {
2833
- write_file: "Write",
2834
- edit_file: "Update",
2835
- read_file: "Read",
2836
- run_bash: "Bash",
2837
- glob: "Glob",
2838
- grep: "Grep"
2839
- };
2840
- function truncate2(s, max) {
2841
- if (s.length <= max) return s;
2842
- return s.slice(0, max - 1) + "\u2026";
2843
- }
2844
3537
  function toolHeader(use) {
2845
3538
  const label = TOOL_LABEL[use.name] ?? use.name;
2846
3539
  const input = use.input ?? {};
@@ -2902,7 +3595,7 @@ function ToolResultBlock({ result, toolName }) {
2902
3595
  ] }) });
2903
3596
  }
2904
3597
  const MAX_LINE_WIDTH = 200;
2905
- const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
3598
+ const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES2);
2906
3599
  const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
2907
3600
  const extra = lines.length - shown.length;
2908
3601
  const header = toolName === "grep" || toolName === "glob" ? summarizeResult(result, toolName) : `${lines.length} line${lines.length === 1 ? "" : "s"}`;
@@ -2954,28 +3647,35 @@ function ToolUseLine({ use, result }) {
2954
3647
  result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
2955
3648
  ] });
2956
3649
  }
3650
+
3651
+ // src/ui/Message.tsx
3652
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2957
3653
  var UserMessage = memo2(function UserMessage2({ msg }) {
2958
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2959
- /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u276F " }),
2960
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
3654
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", marginBottom: 1, children: [
3655
+ /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "\u276F " }),
3656
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1, children: /* @__PURE__ */ jsx10(Text10, { children: msg.content }) })
2961
3657
  ] });
2962
3658
  });
2963
3659
  var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
2964
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
2965
- msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
2966
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
2967
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
3660
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginBottom: 1, children: [
3661
+ msg.content && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
3662
+ /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u25CF " }),
3663
+ /* @__PURE__ */ jsx10(Box10, { width: contentWidth(), children: /* @__PURE__ */ jsx10(Text10, { wrap: "wrap", children: renderMarkdown(msg.content) }) })
2968
3664
  ] }),
2969
3665
  msg.tool_uses?.map((u) => {
2970
3666
  const r = msg.tool_results?.find((x) => x.tool_use_id === u.id);
2971
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
3667
+ return /* @__PURE__ */ jsx10(ToolUseLine, { use: u, result: r }, u.id);
2972
3668
  }),
2973
- msg.tokens && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
3669
+ msg.tokens && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2974
3670
  `\u21B3 Completed \xB7 ${formatTokens(msg.tokens.prompt_eval + msg.tokens.eval)} tokens`,
2975
3671
  msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
2976
3672
  ] }) })
2977
3673
  ] });
2978
3674
  });
3675
+
3676
+ // src/ui/PermissionPrompt.tsx
3677
+ import { Box as Box11, Text as Text11 } from "ink";
3678
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2979
3679
  function summarizeInput(input) {
2980
3680
  if (!input || typeof input !== "object") return "";
2981
3681
  const obj = input;
@@ -3002,15 +3702,15 @@ function PermissionPrompt({ req, cursor }) {
3002
3702
  { label: "No", key: "no" }
3003
3703
  ];
3004
3704
  const summary = summarizeInput(req.input);
3005
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
3006
- /* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
3007
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
3705
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
3706
+ /* @__PURE__ */ jsx11(Text11, { color: "blue", bold: true, children: "Tool use" }),
3707
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { children: [
3008
3708
  "Allow ",
3009
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
3709
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: label }),
3010
3710
  "?"
3011
3711
  ] }) }),
3012
- summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate", dimColor: true, children: summary }) }),
3013
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
3712
+ summary && /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { wrap: "truncate", dimColor: true, children: summary }) }),
3713
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs11(Text11, { color: i === cursor ? "blue" : void 0, children: [
3014
3714
  i === cursor ? "\u276F " : " ",
3015
3715
  i + 1,
3016
3716
  ". ",
@@ -3018,6 +3718,9 @@ function PermissionPrompt({ req, cursor }) {
3018
3718
  ] }, opt.key)) })
3019
3719
  ] });
3020
3720
  }
3721
+
3722
+ // src/ui/ChatView.tsx
3723
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
3021
3724
  function ChatView({
3022
3725
  messages,
3023
3726
  streaming,
@@ -3028,33 +3731,70 @@ function ChatView({
3028
3731
  pendingPermission,
3029
3732
  permissionCursor = 0,
3030
3733
  activeToolUses,
3031
- activeToolResults
3734
+ activeToolResults,
3735
+ header
3032
3736
  }) {
3033
3737
  const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
3034
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
3035
- empty && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
3036
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: EMPTY_STATE_TITLE }),
3037
- EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
3038
- " ",
3039
- h
3040
- ] }, i))
3041
- ] }),
3042
- messages.map(
3043
- (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx9(UserMessage, { msg }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
3044
- ),
3045
- thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
3046
- streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
3047
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
3048
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
3049
- ] }),
3050
- activeToolUses?.map((u) => {
3051
- const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
3052
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
3053
- }),
3054
- pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
3055
- error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
3056
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
3057
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
3738
+ const log = [];
3739
+ if (header) log.push({ key: "header", node: header });
3740
+ messages.forEach((msg, i) => {
3741
+ log.push({
3742
+ key: `msg-${i}`,
3743
+ node: msg.role === "user" ? /* @__PURE__ */ jsx12(UserMessage, { msg }) : /* @__PURE__ */ jsx12(AssistantMessage, { msg })
3744
+ });
3745
+ });
3746
+ const liveBudget = liveFrameRows();
3747
+ let streamNode = null;
3748
+ let streamRows = 0;
3749
+ if (streaming && streamingContent) {
3750
+ const { text, clipped } = clipTail(renderMarkdownStreaming(streamingContent), liveBudget);
3751
+ streamRows = text.split("\n").length + (clipped > 0 ? 1 : 0);
3752
+ streamNode = /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
3753
+ clipped > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${clipped} more line${clipped === 1 ? "" : "s"} above \u2014 streaming\u2026` }),
3754
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", children: [
3755
+ /* @__PURE__ */ jsx12(Text12, { color: "blue", children: "\u25CF " }),
3756
+ /* @__PURE__ */ jsx12(Box12, { width: contentWidth(), children: /* @__PURE__ */ jsx12(Text12, { wrap: "wrap", children: text }) })
3757
+ ] })
3758
+ ] });
3759
+ }
3760
+ let toolNode = null;
3761
+ if (activeToolUses?.length) {
3762
+ const remaining = Math.max(4, liveBudget - streamRows);
3763
+ const resultById = new Map(activeToolResults?.map((r) => [r.tool_use_id, r]));
3764
+ const kept = [];
3765
+ let rows = 0;
3766
+ for (let i = activeToolUses.length - 1; i >= 0; i--) {
3767
+ const u = activeToolUses[i];
3768
+ const r = resultById.get(u.id);
3769
+ const h = estimateToolRows(u, r);
3770
+ if (rows + h > remaining && kept.length) break;
3771
+ rows += h;
3772
+ kept.unshift({ u, r });
3773
+ }
3774
+ const hidden = activeToolUses.length - kept.length;
3775
+ toolNode = /* @__PURE__ */ jsxs12(Fragment2, { children: [
3776
+ hidden > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${hidden} earlier tool call${hidden === 1 ? "" : "s"} above` }),
3777
+ kept.map(({ u, r }) => /* @__PURE__ */ jsx12(ToolUseLine, { use: u, result: r }, u.id))
3778
+ ] });
3779
+ }
3780
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
3781
+ /* @__PURE__ */ jsx12(Static, { items: log, children: (item) => item.key === "header" ? /* @__PURE__ */ jsx12(Box12, { children: item.node }, item.key) : /* @__PURE__ */ jsx12(Box12, { marginLeft: 1, children: item.node }, item.key) }),
3782
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
3783
+ empty && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
3784
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: EMPTY_STATE_TITLE }),
3785
+ EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3786
+ " ",
3787
+ h
3788
+ ] }, i))
3789
+ ] }),
3790
+ thinking && /* @__PURE__ */ jsx12(ThinkingBlock, { content: thinkingContent }),
3791
+ streamNode,
3792
+ toolNode,
3793
+ pendingPermission && /* @__PURE__ */ jsx12(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
3794
+ error && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", marginBottom: 1, children: [
3795
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: "\u25CF " }),
3796
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: error })
3797
+ ] })
3058
3798
  ] })
3059
3799
  ] });
3060
3800
  }
@@ -3294,6 +4034,14 @@ function clearPasteStore() {
3294
4034
  pasteStore.clear();
3295
4035
  pasteCounter = 0;
3296
4036
  }
4037
+ var inputHistory = [];
4038
+ var historyIndex = -1;
4039
+ var historyDraft = "";
4040
+ function pushHistory(line) {
4041
+ if (!line) return;
4042
+ if (inputHistory[inputHistory.length - 1] === line) return;
4043
+ inputHistory.push(line);
4044
+ }
3297
4045
  function expandPastes(text) {
3298
4046
  let out = text;
3299
4047
  for (const [chip, full] of pasteStore) out = out.split(chip).join(full);
@@ -3331,6 +4079,8 @@ function useKeyboard(opts) {
3331
4079
  agent,
3332
4080
  input,
3333
4081
  setInput,
4082
+ caret,
4083
+ setCaret,
3334
4084
  paletteCursor,
3335
4085
  setPaletteCursor,
3336
4086
  filePickerCursor,
@@ -3538,36 +4288,43 @@ function useKeyboard(opts) {
3538
4288
  const mention = !paletteOpen ? parseMention(input) : null;
3539
4289
  const fileMatches = mention ? searchFiles(process.cwd(), mention.query) : [];
3540
4290
  const fileOpen = mention !== null && fileMatches.length > 0;
3541
- if (paletteOpen && key.upArrow) {
4291
+ if (paletteOpen && historyIndex === -1 && key.upArrow) {
3542
4292
  setPaletteCursor((i) => Math.max(0, i - 1));
3543
4293
  return;
3544
4294
  }
3545
- if (paletteOpen && key.downArrow) {
4295
+ if (paletteOpen && historyIndex === -1 && key.downArrow) {
3546
4296
  setPaletteCursor((i) => Math.min(matches2.length - 1, i + 1));
3547
4297
  return;
3548
4298
  }
3549
4299
  if (paletteOpen && (key.tab || key.return) && matches2[paletteCursor] && input !== matches2[paletteCursor].name) {
3550
- setInput(() => matches2[paletteCursor].name);
4300
+ const name = matches2[paletteCursor].name;
4301
+ setInput(() => name);
4302
+ setCaret(() => name.length);
3551
4303
  setPaletteCursor(() => 0);
3552
4304
  return;
3553
4305
  }
3554
4306
  if (paletteOpen && key.escape) {
3555
4307
  clearPasteStore();
3556
4308
  setInput(() => "");
4309
+ setCaret(() => 0);
3557
4310
  setPaletteCursor(() => 0);
3558
4311
  return;
3559
4312
  }
3560
- if (fileOpen && key.upArrow) {
4313
+ if (fileOpen && historyIndex === -1 && key.upArrow) {
3561
4314
  setFilePickerCursor((i) => Math.max(0, i - 1));
3562
4315
  return;
3563
4316
  }
3564
- if (fileOpen && key.downArrow) {
4317
+ if (fileOpen && historyIndex === -1 && key.downArrow) {
3565
4318
  setFilePickerCursor((i) => Math.min(fileMatches.length - 1, i + 1));
3566
4319
  return;
3567
4320
  }
3568
4321
  if (fileOpen && key.tab && fileMatches[filePickerCursor]) {
3569
4322
  const picked = fileMatches[filePickerCursor];
3570
- setInput((s) => s.slice(0, mention.start) + "@" + picked + " ");
4323
+ setInput((s) => {
4324
+ const next = s.slice(0, mention.start) + "@" + picked + " ";
4325
+ setCaret(() => next.length);
4326
+ return next;
4327
+ });
3571
4328
  setFilePickerCursor(() => 0);
3572
4329
  return;
3573
4330
  }
@@ -3575,8 +4332,36 @@ function useKeyboard(opts) {
3575
4332
  setFilePickerCursor(() => 0);
3576
4333
  return;
3577
4334
  }
4335
+ if ((historyIndex !== -1 || !paletteOpen && !fileOpen) && key.upArrow) {
4336
+ if (inputHistory.length === 0) return;
4337
+ if (historyIndex === -1) {
4338
+ historyDraft = input;
4339
+ historyIndex = inputHistory.length - 1;
4340
+ } else if (historyIndex > 0) historyIndex--;
4341
+ const val = inputHistory[historyIndex];
4342
+ setInput(() => val);
4343
+ setCaret(() => val.length);
4344
+ return;
4345
+ }
4346
+ if ((historyIndex !== -1 || !paletteOpen && !fileOpen) && key.downArrow) {
4347
+ if (historyIndex === -1) return;
4348
+ if (historyIndex < inputHistory.length - 1) {
4349
+ historyIndex++;
4350
+ const val = inputHistory[historyIndex];
4351
+ setInput(() => val);
4352
+ setCaret(() => val.length);
4353
+ } else {
4354
+ historyIndex = -1;
4355
+ setInput(() => historyDraft);
4356
+ setCaret(() => historyDraft.length);
4357
+ }
4358
+ return;
4359
+ }
3578
4360
  if (key.return) {
3579
4361
  const trimmed = input.trim();
4362
+ pushHistory(trimmed);
4363
+ historyIndex = -1;
4364
+ historyDraft = "";
3580
4365
  if (trimmed === "/models") {
3581
4366
  setPickerQuery("");
3582
4367
  setCursor(() => Math.max(0, models.findIndex((m) => m === cfg.model)));
@@ -3613,30 +4398,49 @@ function useKeyboard(opts) {
3613
4398
  }
3614
4399
  clearPasteStore();
3615
4400
  setInput(() => "");
4401
+ setCaret(() => 0);
3616
4402
  setPaletteCursor(() => 0);
3617
4403
  return;
3618
4404
  }
4405
+ if (key.leftArrow) {
4406
+ setCaret((i) => Math.max(0, i - 1));
4407
+ return;
4408
+ }
4409
+ if (key.rightArrow) {
4410
+ setCaret((i) => Math.min(input.length, i + 1));
4411
+ return;
4412
+ }
4413
+ if (key.ctrl && char === "a") {
4414
+ setCaret(() => 0);
4415
+ return;
4416
+ }
4417
+ if (key.ctrl && char === "e") {
4418
+ setCaret(() => input.length);
4419
+ return;
4420
+ }
3619
4421
  if (key.backspace || key.delete) {
3620
- setInput((s) => {
3621
- setPaletteCursor(() => 0);
3622
- setFilePickerCursor(() => 0);
3623
- let match = "";
3624
- for (const chip of pasteStore.keys()) {
3625
- if (s.endsWith(chip) && chip.length > match.length) match = chip;
3626
- }
3627
- if (match) {
3628
- pasteStore.delete(match);
3629
- return s.slice(0, -match.length);
3630
- }
3631
- return s.slice(0, -1);
3632
- });
4422
+ historyIndex = -1;
4423
+ setPaletteCursor(() => 0);
4424
+ setFilePickerCursor(() => 0);
4425
+ if (caret <= 0) return;
4426
+ const before = input.slice(0, caret);
4427
+ let match = "";
4428
+ for (const chip of pasteStore.keys()) {
4429
+ if (before.endsWith(chip) && chip.length > match.length) match = chip;
4430
+ }
4431
+ const cut = match ? match.length : 1;
4432
+ if (match) pasteStore.delete(match);
4433
+ setInput((s) => s.slice(0, caret - cut) + s.slice(caret));
4434
+ setCaret((i) => Math.max(0, i - cut));
3633
4435
  } else if (char && !key.ctrl && !key.meta && !key.tab) {
3634
4436
  const text = sanitizePaste(char);
3635
- if (text) setInput((s) => {
4437
+ if (text) {
4438
+ historyIndex = -1;
3636
4439
  setPaletteCursor(() => 0);
3637
4440
  setFilePickerCursor(() => 0);
3638
- return s + text;
3639
- });
4441
+ setInput((s) => s.slice(0, caret) + text + s.slice(caret));
4442
+ setCaret((i) => i + text.length);
4443
+ }
3640
4444
  }
3641
4445
  }
3642
4446
  });
@@ -3678,14 +4482,16 @@ async function checkForUpdate() {
3678
4482
  }
3679
4483
 
3680
4484
  // src/ui/App.tsx
3681
- import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
4485
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3682
4486
  function App() {
3683
4487
  const { exit } = useApp();
3684
4488
  const cwd = process.cwd().replace(homedir6(), "~").split(sep2).join("/");
3685
4489
  const [cfg, setCfg] = useState5(loadConfig());
3686
4490
  const [models, setModels] = useState5([]);
3687
- const [contexts, setContexts] = useState5({});
3688
- const [activeCtx, setActiveCtx] = useState5(null);
4491
+ const [contexts, setContexts] = useState5(() => cfg.modelContexts ?? {});
4492
+ const [activeCtx, setActiveCtx] = useState5(
4493
+ () => cfg.model ? cfg.modelContexts?.[cfg.model] ?? null : null
4494
+ );
3689
4495
  const [state, setState] = useState5("loading");
3690
4496
  const [cursor, setCursor] = useState5(0);
3691
4497
  const [pickerQuery, setPickerQuery] = useState5("");
@@ -3695,6 +4501,7 @@ function App() {
3695
4501
  const [sessions, setSessions] = useState5([]);
3696
4502
  const [notice, setNotice] = useState5(null);
3697
4503
  const [input, setInput] = useState5("");
4504
+ const [caret, setCaret] = useState5(0);
3698
4505
  const [paletteCursor, setPaletteCursor] = useState5(0);
3699
4506
  const [filePickerCursor, setFilePickerCursor] = useState5(0);
3700
4507
  const agent = useAgentRunner(cfg.model, activeCtx);
@@ -3736,12 +4543,20 @@ function App() {
3736
4543
  } else {
3737
4544
  setState(hasModel ? "ready" : "select-model");
3738
4545
  }
3739
- Promise.all(m.map((name) => modelContext3(name).then((ctx) => [name, ctx]))).then((pairs) => {
4546
+ Promise.all(
4547
+ m.map(
4548
+ (name) => modelContext3(name).then((ctx) => [name, ctx]).catch(() => [name, null])
4549
+ )
4550
+ ).then((pairs) => {
3740
4551
  if (stale()) return;
3741
4552
  const map = Object.fromEntries(pairs);
3742
4553
  setContexts(map);
4554
+ const resolved = Object.fromEntries(
4555
+ pairs.filter((p) => p[1] != null)
4556
+ );
4557
+ if (Object.keys(resolved).length) setModelContexts(resolved);
3743
4558
  const active2 = (hasModel ? cfg.model : void 0) ?? m[0];
3744
- if (active2 && map[active2]) setActiveCtx(map[active2]);
4559
+ if (active2 && map[active2] != null) setActiveCtx(map[active2]);
3745
4560
  }).catch(() => {
3746
4561
  });
3747
4562
  }).catch((err) => {
@@ -3786,6 +4601,8 @@ function App() {
3786
4601
  agent,
3787
4602
  input,
3788
4603
  setInput,
4604
+ caret,
4605
+ setCaret,
3789
4606
  paletteCursor,
3790
4607
  setPaletteCursor,
3791
4608
  filePickerCursor,
@@ -3806,10 +4623,10 @@ function App() {
3806
4623
  if (used < activeCtx * 0.7) return null;
3807
4624
  return Math.round(used / activeCtx * 100);
3808
4625
  })();
3809
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
3810
- /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
3811
- state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
3812
- agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
4626
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
4627
+ state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
4628
+ state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
4629
+ agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
3813
4630
  ChatView,
3814
4631
  {
3815
4632
  messages: [],
@@ -3819,7 +4636,7 @@ function App() {
3819
4636
  error: agent.error
3820
4637
  }
3821
4638
  ),
3822
- (state === "select-model" || state === "models") && /* @__PURE__ */ jsx10(
4639
+ (state === "select-model" || state === "models") && /* @__PURE__ */ jsx13(
3823
4640
  ModelsView,
3824
4641
  {
3825
4642
  models: filteredModels,
@@ -3832,7 +4649,7 @@ function App() {
3832
4649
  requireSelection: state === "select-model"
3833
4650
  }
3834
4651
  ),
3835
- state === "providers" && /* @__PURE__ */ jsx10(
4652
+ state === "providers" && /* @__PURE__ */ jsx13(
3836
4653
  ProviderPicker,
3837
4654
  {
3838
4655
  entries: filteredProviders,
@@ -3841,10 +4658,10 @@ function App() {
3841
4658
  query: pickerQuery
3842
4659
  }
3843
4660
  ),
3844
- state === "sessions" && /* @__PURE__ */ jsx10(SessionsView, { sessions, cursor }),
3845
- state === "ready" && /* @__PURE__ */ jsxs10(Fragment2, { children: [
3846
- notice && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: `\u2713 ${notice}` }) }),
3847
- /* @__PURE__ */ jsx10(
4661
+ state === "sessions" && /* @__PURE__ */ jsx13(SessionsView, { sessions, cursor }),
4662
+ state === "ready" && /* @__PURE__ */ jsxs13(Fragment3, { children: [
4663
+ notice && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: `\u2713 ${notice}` }) }),
4664
+ /* @__PURE__ */ jsx13(
3848
4665
  ChatView,
3849
4666
  {
3850
4667
  messages: agent.messages,
@@ -3856,18 +4673,19 @@ function App() {
3856
4673
  pendingPermission: agent.pendingPermission,
3857
4674
  permissionCursor: agent.permissionCursor,
3858
4675
  activeToolUses: agent.activeToolUses,
3859
- activeToolResults: agent.activeToolResults
4676
+ activeToolResults: agent.activeToolResults,
4677
+ header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
3860
4678
  }
3861
4679
  ),
3862
- input.startsWith("/") && /* @__PURE__ */ jsx10(CommandPalette, { filter: input, cursor: paletteCursor }),
3863
- contextWarning !== null && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: `\u26A0 context ${contextWarning}% full \u2014 run /clear and start fresh` }) }),
4680
+ input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
4681
+ contextWarning !== null && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u26A0 context ${contextWarning}% full \u2014 run /clear and start fresh` }) }),
3864
4682
  !input.startsWith("/") && (() => {
3865
4683
  const m = parseMention(input);
3866
4684
  if (!m) return null;
3867
- return /* @__PURE__ */ jsx10(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
4685
+ return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
3868
4686
  })(),
3869
- /* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
3870
- !agent.busy && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) })
4687
+ /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
4688
+ !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) })
3871
4689
  ] })
3872
4690
  ] });
3873
4691
  }