miii-agent 0.1.18 → 0.1.20

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 +1138 -124
  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,6 +80,10 @@ 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"() {
@@ -158,6 +163,30 @@ async function modelContext(entry, model) {
158
163
  throw err;
159
164
  }
160
165
  }
166
+ async function paramCountB(entry, model) {
167
+ try {
168
+ const info = await makeClient(entry).show({ model });
169
+ const details = info.details;
170
+ if (details?.parameter_size) {
171
+ const m = details.parameter_size.match(/([\d.]+)\s*([BM])/i);
172
+ if (m) {
173
+ const n = parseFloat(m[1]);
174
+ if (!isNaN(n)) return m[2].toUpperCase() === "M" ? n / 1e3 : n;
175
+ }
176
+ }
177
+ const modelInfo = info.model_info;
178
+ if (modelInfo) {
179
+ const key = Object.keys(modelInfo).find((k) => k.endsWith("parameter_count"));
180
+ if (key) {
181
+ const val = Number(modelInfo[key]);
182
+ if (!isNaN(val) && val > 0) return val / 1e9;
183
+ }
184
+ }
185
+ return null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
161
190
  async function* chat(entry, model, messages, tools, opts) {
162
191
  if (opts?.signal?.aborted) return;
163
192
  const signal = opts?.signal;
@@ -177,15 +206,31 @@ async function* chat(entry, model, messages, tools, opts) {
177
206
  num_ctx: opts?.num_ctx ?? 8192
178
207
  };
179
208
  if (numPredict !== void 0 && numPredict > 0) options.num_predict = numPredict;
180
- stream = await client.chat({
209
+ const req = {
181
210
  model,
182
211
  messages,
183
- tools,
184
212
  stream: true,
185
213
  think: true,
186
214
  keep_alive: opts?.keep_alive ?? "10m",
187
215
  options
188
- });
216
+ };
217
+ if (opts?.format) req.format = opts.format;
218
+ else if (tools) req.tools = tools;
219
+ try {
220
+ stream = await client.chat(
221
+ req
222
+ );
223
+ } catch (err) {
224
+ const msg = err instanceof Error ? err.message : String(err);
225
+ if (!signal?.aborted && msg.toLowerCase().includes("does not support thinking")) {
226
+ delete req.think;
227
+ stream = await client.chat(
228
+ req
229
+ );
230
+ } else {
231
+ throw err;
232
+ }
233
+ }
189
234
  } catch (err) {
190
235
  if (signal?.aborted) return;
191
236
  if (isConnectionError(err)) {
@@ -215,6 +260,11 @@ async function* chat(entry, model, messages, tools, opts) {
215
260
  if (isConnectionError(err)) {
216
261
  throw new Error(NOT_RUNNING);
217
262
  }
263
+ const msg = err instanceof Error ? err.message : String(err);
264
+ if (msg.includes("Did not receive done or success response in stream")) {
265
+ yield { content: "", done: true, prompt_eval_count: 0, eval_count: 0 };
266
+ return;
267
+ }
218
268
  throw err;
219
269
  } finally {
220
270
  if (opts?.signal) opts.signal.removeEventListener("abort", onAbort);
@@ -452,6 +502,9 @@ var init_openai = __esm({
452
502
  function active() {
453
503
  return resolveProvider();
454
504
  }
505
+ function providerName() {
506
+ return active().name;
507
+ }
455
508
  function isAvailable3() {
456
509
  const { entry } = active();
457
510
  return entry.type === "ollama" ? isAvailable(entry) : isAvailable2(entry);
@@ -468,6 +521,16 @@ async function modelContext3(model) {
468
521
  const { entry } = active();
469
522
  return entry.type === "ollama" ? modelContext(entry, model) : modelContext2(entry, model);
470
523
  }
524
+ async function modelParamCountB(model) {
525
+ const { entry } = active();
526
+ if (entry.type !== "ollama") return null;
527
+ const key = `${entry.baseUrl}:${model}`;
528
+ const cached = paramCountCache.get(key);
529
+ if (cached !== void 0) return cached;
530
+ const params = await paramCountB(entry, model);
531
+ paramCountCache.set(key, params);
532
+ return params;
533
+ }
471
534
  async function* chat3(model, messages, tools, opts) {
472
535
  const { entry } = active();
473
536
  if (entry.type === "ollama") {
@@ -476,12 +539,70 @@ async function* chat3(model, messages, tools, opts) {
476
539
  yield* chat2(entry, model, messages, tools, opts);
477
540
  }
478
541
  }
542
+ var paramCountCache;
479
543
  var init_client = __esm({
480
544
  "src/llm/client.ts"() {
481
545
  "use strict";
482
546
  init_config();
483
547
  init_ollama();
484
548
  init_openai();
549
+ paramCountCache = /* @__PURE__ */ new Map();
550
+ }
551
+ });
552
+
553
+ // src/llm/grammar.ts
554
+ function argProperties(props) {
555
+ const out = {};
556
+ for (const [key, spec] of Object.entries(props)) {
557
+ const node = { type: spec.type };
558
+ if (spec.enum && spec.enum.length) node.enum = spec.enum;
559
+ out[key] = node;
560
+ }
561
+ return out;
562
+ }
563
+ function toolBranch(tool) {
564
+ const args2 = {
565
+ type: "object",
566
+ additionalProperties: false,
567
+ properties: argProperties(tool.input_schema.properties)
568
+ };
569
+ if (tool.input_schema.required && tool.input_schema.required.length) {
570
+ args2.required = tool.input_schema.required;
571
+ }
572
+ return {
573
+ type: "object",
574
+ additionalProperties: false,
575
+ required: ["name", "arguments"],
576
+ properties: {
577
+ name: { const: tool.name },
578
+ arguments: args2
579
+ }
580
+ };
581
+ }
582
+ function respondBranch() {
583
+ return {
584
+ type: "object",
585
+ additionalProperties: false,
586
+ required: ["name", "arguments"],
587
+ properties: {
588
+ name: { const: RESPOND_ACTION },
589
+ arguments: {
590
+ type: "object",
591
+ additionalProperties: false,
592
+ required: ["message"],
593
+ properties: { message: { type: "string" } }
594
+ }
595
+ }
596
+ };
597
+ }
598
+ function buildToolGrammar(tools) {
599
+ return { oneOf: [...tools.map(toolBranch), respondBranch()] };
600
+ }
601
+ var RESPOND_ACTION;
602
+ var init_grammar = __esm({
603
+ "src/llm/grammar.ts"() {
604
+ "use strict";
605
+ RESPOND_ACTION = "respond";
485
606
  }
486
607
  });
487
608
 
@@ -1149,8 +1270,15 @@ var init_context = __esm({
1149
1270
  });
1150
1271
 
1151
1272
  // src/prompt/system.ts
1152
- function buildSystemPrompt(tools, cwd, project) {
1273
+ function buildSystemPrompt(tools, cwd, project, grammarMode = false) {
1153
1274
  const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
1275
+ const actionProtocol = grammarMode ? `
1276
+ # Action protocol (strict)
1277
+ Every reply is exactly ONE JSON action object, nothing else \u2014 no prose outside it, no markdown, no fences. Decoding is grammar-constrained, so malformed output is impossible; your only job is to choose the right action.
1278
+ To use a tool: {"name": "<tool_name>", "arguments": { ...that tool's args }}
1279
+ To give your final answer to the user: {"name": "respond", "arguments": {"message": "<your full answer here>"}}
1280
+ Call tools until the GOAL is met, then emit a single "respond" action with the complete answer. The "respond" action is the ONLY way to end the turn and talk to the user \u2014 never put your final answer in a tool call.
1281
+ ` : "";
1154
1282
  const projectSection = project && project.content.trim() ? `
1155
1283
  # ${CONTEXT_FILENAME} \u2014 project instructions (authoritative, read first)
1156
1284
  The user maintains ${CONTEXT_FILENAME} at ${project.source} to steer how you work in this project: conventions, commands, architecture, do's and don'ts. Treat it as direct instruction from the user, higher priority than your defaults. When it conflicts with a default rule below, ${CONTEXT_FILENAME} wins (except permissions and safety, which you never override).${project.truncated ? `
@@ -1175,6 +1303,17 @@ If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one m
1175
1303
 
1176
1304
  Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
1177
1305
 
1306
+ # Plan summary before acting (conditional)
1307
+ Write a brief plan BEFORE the first tool call ONLY when the work is non-trivial:
1308
+ - Multi-step, OR touches multiple files, OR is destructive/hard to reverse, OR mixes investigation + change.
1309
+ SKIP the plan for trivial work \u2014 a single read, one small edit, a quick search, a direct question. Just act.
1310
+ When you do write one:
1311
+ - One or two plain-text sentences naming what you will do and in what order.
1312
+ - State the intent (the bug/feature/fix and the steps), not a tool-by-tool narration.
1313
+ - Keep it short \u2014 the user reads this to follow along, not a spec.
1314
+ Then begin immediately with the first tool call. Do not wait for approval unless GAPS was non-empty or the work is destructive.
1315
+ This summary is the ONE allowed preamble. It does not override the Tool calls rule below: after this plan, emit tool calls directly with no further narration between them.
1316
+
1178
1317
  # Attention: re-attend to goal at each step
1179
1318
  After each tool result, answer silently: "Does this result move me toward GOAL?"
1180
1319
  YES \u2192 continue
@@ -1183,9 +1322,9 @@ After each tool result, answer silently: "Does this result move me toward GOAL?"
1183
1322
  This prevents drift. Each step attends to the original goal, not just the previous step.
1184
1323
 
1185
1324
  # Output format
1186
- - Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
1187
- - This applies to your reasoning/thinking too. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there either.
1188
- - Quote code, paths, and identifiers inline as plain text. Do not wrap them.
1325
+ - 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.
1326
+ - 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.
1327
+ - This does NOT apply to your reasoning/thinking. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there.
1189
1328
  - Keep prose terse.
1190
1329
 
1191
1330
  # Tone and voice
@@ -1224,7 +1363,7 @@ Ask in a numbered list. One round of questions per turn. Then wait.
1224
1363
  # Tools
1225
1364
  You have access to the following tools. Call them via the function-calling interface.
1226
1365
  ${toolLines}
1227
-
1366
+ ${actionProtocol}
1228
1367
  # Loop semantics
1229
1368
  - When you need to act on the filesystem or run a command, emit a tool call.
1230
1369
  - After each tool result, decide: more tool calls, or a final plain-text answer.
@@ -1289,6 +1428,17 @@ function subjectFor(toolName, input) {
1289
1428
  if (typeof obj.path === "string") return obj.path;
1290
1429
  return "";
1291
1430
  }
1431
+ function generalizeCommand(command) {
1432
+ const tokens = command.trim().split(/\s+/);
1433
+ if (tokens.length === 0 || tokens[0] === "") return command;
1434
+ const prog = tokens[0];
1435
+ const prefixLen = WRAPPER_PROGRAMS.has(prog) && tokens.length > 1 ? 2 : 1;
1436
+ const prefix = tokens.slice(0, prefixLen).join(" ");
1437
+ return `${prefix} *`;
1438
+ }
1439
+ function patternToPersist(toolName, subject) {
1440
+ return toolName === "run_bash" ? generalizeCommand(subject) : subject;
1441
+ }
1292
1442
  function globToRegExp(glob2) {
1293
1443
  const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1294
1444
  const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
@@ -1309,15 +1459,28 @@ async function check(toolName, input, ctx) {
1309
1459
  if (rules.some((r) => matches(r, toolName, subject))) return "allow";
1310
1460
  const answer = await ctx.ask(toolName, input);
1311
1461
  if (answer === "no") return "deny";
1312
- if (answer === "always") addRule(toolName, subject);
1462
+ if (answer === "always") addRule(toolName, patternToPersist(toolName, subject));
1313
1463
  return "allow";
1314
1464
  }
1315
- var RULES_DIR, RULES_PATH, ALWAYS_ALLOW;
1465
+ var RULES_DIR, RULES_PATH, WRAPPER_PROGRAMS, ALWAYS_ALLOW;
1316
1466
  var init_policy = __esm({
1317
1467
  "src/permissions/policy.ts"() {
1318
1468
  "use strict";
1319
1469
  RULES_DIR = join7(homedir5(), ".miii");
1320
1470
  RULES_PATH = join7(RULES_DIR, "permissions.json");
1471
+ WRAPPER_PROGRAMS = /* @__PURE__ */ new Set([
1472
+ "npm",
1473
+ "npx",
1474
+ "pnpm",
1475
+ "yarn",
1476
+ "brew",
1477
+ "pip",
1478
+ "pip3",
1479
+ "cargo",
1480
+ "docker",
1481
+ "kubectl",
1482
+ "go"
1483
+ ]);
1321
1484
  ALWAYS_ALLOW = /* @__PURE__ */ new Set(["read_file", "grep", "glob"]);
1322
1485
  }
1323
1486
  });
@@ -1430,6 +1593,75 @@ function extractFirstJsonObject(s) {
1430
1593
  }
1431
1594
  return null;
1432
1595
  }
1596
+ function parseGrammarAction(content, knownToolNames) {
1597
+ if (!content) return null;
1598
+ let raw = content.trim();
1599
+ if (!raw.startsWith("{")) {
1600
+ const found = extractFirstJsonObject(raw);
1601
+ if (!found) return null;
1602
+ raw = found.json;
1603
+ }
1604
+ let obj;
1605
+ try {
1606
+ obj = JSON.parse(raw);
1607
+ } catch {
1608
+ const found = extractFirstJsonObject(raw);
1609
+ if (!found) return null;
1610
+ try {
1611
+ obj = JSON.parse(found.json);
1612
+ } catch {
1613
+ return null;
1614
+ }
1615
+ }
1616
+ const name = typeof obj.name === "string" ? obj.name : void 0;
1617
+ const args2 = obj.arguments ?? {};
1618
+ if (!name) return null;
1619
+ if (name === "respond") {
1620
+ const message = typeof args2.message === "string" ? args2.message : "";
1621
+ return { kind: "respond", message };
1622
+ }
1623
+ if (!knownToolNames.includes(name)) return null;
1624
+ return { kind: "tool", name, arguments: args2 };
1625
+ }
1626
+ function streamRespondMessage(text) {
1627
+ if (!/"name"\s*:\s*"respond"/.test(text)) return null;
1628
+ const m = text.match(/"message"\s*:\s*"/);
1629
+ if (!m || m.index == null) return null;
1630
+ const start = m.index + m[0].length;
1631
+ const escapes = {
1632
+ n: "\n",
1633
+ t: " ",
1634
+ r: "\r",
1635
+ b: "\b",
1636
+ f: "\f",
1637
+ '"': '"',
1638
+ "\\": "\\",
1639
+ "/": "/"
1640
+ };
1641
+ let out = "";
1642
+ let i = start;
1643
+ while (i < text.length) {
1644
+ const ch = text[i];
1645
+ if (ch === '"') return { message: out, complete: true };
1646
+ if (ch === "\\") {
1647
+ const nx = text[i + 1];
1648
+ if (nx === void 0) break;
1649
+ if (nx === "u") {
1650
+ const hex = text.slice(i + 2, i + 6);
1651
+ if (hex.length < 4) break;
1652
+ out += String.fromCharCode(parseInt(hex, 16));
1653
+ i += 6;
1654
+ continue;
1655
+ }
1656
+ out += escapes[nx] ?? nx;
1657
+ i += 2;
1658
+ continue;
1659
+ }
1660
+ out += ch;
1661
+ i++;
1662
+ }
1663
+ return { message: out, complete: false };
1664
+ }
1433
1665
  function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1434
1666
  const blocks = [];
1435
1667
  let finalText = text;
@@ -1487,8 +1719,15 @@ function markSeen(name, input, seen) {
1487
1719
  async function* runAgent(opts) {
1488
1720
  const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
1489
1721
  const startTime = Date.now();
1490
- const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd));
1722
+ let useGrammar = false;
1723
+ if (providerName() === "ollama") {
1724
+ const params = await modelParamCountB(model);
1725
+ useGrammar = params == null || params <= GRAMMAR_MAX_PARAMS_B;
1726
+ }
1727
+ const system = buildSystemPrompt(TOOLS, cwd, loadProjectContext(cwd), useGrammar);
1728
+ const grammar = useGrammar ? buildToolGrammar(TOOLS) : void 0;
1491
1729
  const ollamaTools = toOllamaTools(TOOLS);
1730
+ const toolNames = TOOLS.map((t) => t.name);
1492
1731
  const effort = EFFORT_OPTIONS[loadConfig().effort ?? "medium"];
1493
1732
  const history = [
1494
1733
  ...opts.history,
@@ -1502,6 +1741,8 @@ async function* runAgent(opts) {
1502
1741
  for (let turn = 0; turn < MAX_TURNS; turn++) {
1503
1742
  let text = "";
1504
1743
  let tool_calls;
1744
+ let respondEmitted = 0;
1745
+ let streamedRespond = false;
1505
1746
  let lastTail = "";
1506
1747
  let tailRepeats = 0;
1507
1748
  let streamLooped = false;
@@ -1509,11 +1750,22 @@ async function* runAgent(opts) {
1509
1750
  const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1510
1751
  if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
1511
1752
  try {
1512
- for await (const chunk of chat3(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature })) {
1753
+ for await (const chunk of chat3(model, toOllamaMessages(history, system), useGrammar ? void 0 : ollamaTools, { signal: composedSignal, num_ctx, num_predict: effort.num_predict, temperature: effort.temperature, format: grammar })) {
1513
1754
  if (signal?.aborted) break;
1514
1755
  if (chunk.content) {
1515
1756
  text += chunk.content;
1516
- yield { type: "text-delta", text: chunk.content };
1757
+ if (!useGrammar) {
1758
+ yield { type: "text-delta", text: chunk.content };
1759
+ } else {
1760
+ const r = streamRespondMessage(text);
1761
+ if (r) {
1762
+ streamedRespond = true;
1763
+ if (r.message.length > respondEmitted) {
1764
+ yield { type: "text-delta", text: r.message.slice(respondEmitted) };
1765
+ respondEmitted = r.message.length;
1766
+ }
1767
+ }
1768
+ }
1517
1769
  if (text.length >= REPEAT_TAIL) {
1518
1770
  const tail = text.slice(-REPEAT_TAIL);
1519
1771
  if (tail === lastTail) {
@@ -1561,7 +1813,19 @@ async function* runAgent(opts) {
1561
1813
  };
1562
1814
  return history;
1563
1815
  }
1564
- const blocks = blocksFromOllama(text, tool_calls, TOOLS.map((t) => t.name));
1816
+ let blocks;
1817
+ if (useGrammar) {
1818
+ const action = parseGrammarAction(text, toolNames);
1819
+ if (action?.kind === "tool") {
1820
+ blocks = [{ type: "tool_use", id: mintToolUseId(), name: action.name, input: action.arguments }];
1821
+ } else {
1822
+ const message = action?.kind === "respond" ? action.message : text.trim();
1823
+ if (message && !streamedRespond) yield { type: "text-delta", text: message };
1824
+ blocks = message ? [{ type: "text", text: message }] : [];
1825
+ }
1826
+ } else {
1827
+ blocks = blocksFromOllama(text, tool_calls, toolNames);
1828
+ }
1565
1829
  const tool_uses = blocks.filter((b) => b.type === "tool_use");
1566
1830
  history.push({ role: "assistant", content: blocks });
1567
1831
  if (tool_uses.length === 0) {
@@ -1670,11 +1934,12 @@ async function* runAgent(opts) {
1670
1934
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1671
1935
  return history;
1672
1936
  }
1673
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL;
1937
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, GRAMMAR_MAX_PARAMS_B;
1674
1938
  var init_loop = __esm({
1675
1939
  "src/agent/loop.ts"() {
1676
1940
  "use strict";
1677
1941
  init_client();
1942
+ init_grammar();
1678
1943
  init_paths();
1679
1944
  init_registry();
1680
1945
  init_validate();
@@ -1686,6 +1951,7 @@ var init_loop = __esm({
1686
1951
  MAX_TURNS = 25;
1687
1952
  REPEAT_TAIL = 120;
1688
1953
  REPEAT_KILL = 4;
1954
+ GRAMMAR_MAX_PARAMS_B = 14;
1689
1955
  }
1690
1956
  });
1691
1957
 
@@ -1915,7 +2181,7 @@ import { createElement } from "react";
1915
2181
  init_client();
1916
2182
  init_config();
1917
2183
  import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
1918
- import { Box as Box10, Text as Text10, useApp } from "ink";
2184
+ import { Box as Box13, Text as Text13, useApp } from "ink";
1919
2185
  import { homedir as homedir6 } from "os";
1920
2186
  import { sep as sep2 } from "path";
1921
2187
 
@@ -1957,7 +2223,7 @@ import { memo, useEffect, useState } from "react";
1957
2223
  import { Box as Box2, Text as Text2 } from "ink";
1958
2224
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1959
2225
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1960
- var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
2226
+ var InputBar = memo(function InputBar2({ input, caret, disabled, processingLabel }) {
1961
2227
  const [frame, setFrame] = useState(0);
1962
2228
  useEffect(() => {
1963
2229
  if (!disabled) return;
@@ -1980,8 +2246,17 @@ var InputBar = memo(function InputBar2({ input, disabled, processingLabel }) {
1980
2246
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (esc to cancel)" })
1981
2247
  ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
1982
2248
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "> " }),
1983
- /* @__PURE__ */ jsx2(Text2, { children: input }),
1984
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u258C" })
2249
+ (() => {
2250
+ const pos = Math.max(0, Math.min(caret ?? input.length, input.length));
2251
+ const before = input.slice(0, pos);
2252
+ const at = input.slice(pos, pos + 1) || " ";
2253
+ const after = input.slice(pos + 1);
2254
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
2255
+ /* @__PURE__ */ jsx2(Text2, { children: before }),
2256
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2257
+ /* @__PURE__ */ jsx2(Text2, { children: after })
2258
+ ] });
2259
+ })()
1985
2260
  ] })
1986
2261
  }
1987
2262
  );
@@ -2390,10 +2665,566 @@ function FilePicker({ matches: matches2, cursor }) {
2390
2665
  }
2391
2666
 
2392
2667
  // src/ui/ChatView.tsx
2393
- import { memo as memo2, useState as useState3, useEffect as useEffect3 } from "react";
2394
- import { Box as Box9, Text as Text9 } from "ink";
2668
+ import { Box as Box12, Text as Text12, Static } from "ink";
2669
+
2670
+ // src/ui/markdown.ts
2671
+ import { Marked } from "marked";
2672
+ import { markedTerminal } from "marked-terminal";
2395
2673
  import { highlight } from "cli-highlight";
2396
2674
 
2675
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
2676
+ var ANSI_BACKGROUND_OFFSET = 10;
2677
+ var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
2678
+ var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
2679
+ var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
2680
+ var styles = {
2681
+ modifier: {
2682
+ reset: [0, 0],
2683
+ // 21 isn't widely supported and 22 does the same thing
2684
+ bold: [1, 22],
2685
+ dim: [2, 22],
2686
+ italic: [3, 23],
2687
+ underline: [4, 24],
2688
+ overline: [53, 55],
2689
+ inverse: [7, 27],
2690
+ hidden: [8, 28],
2691
+ strikethrough: [9, 29]
2692
+ },
2693
+ color: {
2694
+ black: [30, 39],
2695
+ red: [31, 39],
2696
+ green: [32, 39],
2697
+ yellow: [33, 39],
2698
+ blue: [34, 39],
2699
+ magenta: [35, 39],
2700
+ cyan: [36, 39],
2701
+ white: [37, 39],
2702
+ // Bright color
2703
+ blackBright: [90, 39],
2704
+ gray: [90, 39],
2705
+ // Alias of `blackBright`
2706
+ grey: [90, 39],
2707
+ // Alias of `blackBright`
2708
+ redBright: [91, 39],
2709
+ greenBright: [92, 39],
2710
+ yellowBright: [93, 39],
2711
+ blueBright: [94, 39],
2712
+ magentaBright: [95, 39],
2713
+ cyanBright: [96, 39],
2714
+ whiteBright: [97, 39]
2715
+ },
2716
+ bgColor: {
2717
+ bgBlack: [40, 49],
2718
+ bgRed: [41, 49],
2719
+ bgGreen: [42, 49],
2720
+ bgYellow: [43, 49],
2721
+ bgBlue: [44, 49],
2722
+ bgMagenta: [45, 49],
2723
+ bgCyan: [46, 49],
2724
+ bgWhite: [47, 49],
2725
+ // Bright color
2726
+ bgBlackBright: [100, 49],
2727
+ bgGray: [100, 49],
2728
+ // Alias of `bgBlackBright`
2729
+ bgGrey: [100, 49],
2730
+ // Alias of `bgBlackBright`
2731
+ bgRedBright: [101, 49],
2732
+ bgGreenBright: [102, 49],
2733
+ bgYellowBright: [103, 49],
2734
+ bgBlueBright: [104, 49],
2735
+ bgMagentaBright: [105, 49],
2736
+ bgCyanBright: [106, 49],
2737
+ bgWhiteBright: [107, 49]
2738
+ }
2739
+ };
2740
+ var modifierNames = Object.keys(styles.modifier);
2741
+ var foregroundColorNames = Object.keys(styles.color);
2742
+ var backgroundColorNames = Object.keys(styles.bgColor);
2743
+ var colorNames = [...foregroundColorNames, ...backgroundColorNames];
2744
+ function assembleStyles() {
2745
+ const codes = /* @__PURE__ */ new Map();
2746
+ for (const [groupName, group] of Object.entries(styles)) {
2747
+ for (const [styleName, style] of Object.entries(group)) {
2748
+ styles[styleName] = {
2749
+ open: `\x1B[${style[0]}m`,
2750
+ close: `\x1B[${style[1]}m`
2751
+ };
2752
+ group[styleName] = styles[styleName];
2753
+ codes.set(style[0], style[1]);
2754
+ }
2755
+ Object.defineProperty(styles, groupName, {
2756
+ value: group,
2757
+ enumerable: false
2758
+ });
2759
+ }
2760
+ Object.defineProperty(styles, "codes", {
2761
+ value: codes,
2762
+ enumerable: false
2763
+ });
2764
+ styles.color.close = "\x1B[39m";
2765
+ styles.bgColor.close = "\x1B[49m";
2766
+ styles.color.ansi = wrapAnsi16();
2767
+ styles.color.ansi256 = wrapAnsi256();
2768
+ styles.color.ansi16m = wrapAnsi16m();
2769
+ styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
2770
+ styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
2771
+ styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
2772
+ Object.defineProperties(styles, {
2773
+ rgbToAnsi256: {
2774
+ value(red, green, blue) {
2775
+ if (red === green && green === blue) {
2776
+ if (red < 8) {
2777
+ return 16;
2778
+ }
2779
+ if (red > 248) {
2780
+ return 231;
2781
+ }
2782
+ return Math.round((red - 8) / 247 * 24) + 232;
2783
+ }
2784
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
2785
+ },
2786
+ enumerable: false
2787
+ },
2788
+ hexToRgb: {
2789
+ value(hex) {
2790
+ const matches2 = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
2791
+ if (!matches2) {
2792
+ return [0, 0, 0];
2793
+ }
2794
+ let [colorString] = matches2;
2795
+ if (colorString.length === 3) {
2796
+ colorString = [...colorString].map((character) => character + character).join("");
2797
+ }
2798
+ const integer = Number.parseInt(colorString, 16);
2799
+ return [
2800
+ /* eslint-disable no-bitwise */
2801
+ integer >> 16 & 255,
2802
+ integer >> 8 & 255,
2803
+ integer & 255
2804
+ /* eslint-enable no-bitwise */
2805
+ ];
2806
+ },
2807
+ enumerable: false
2808
+ },
2809
+ hexToAnsi256: {
2810
+ value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
2811
+ enumerable: false
2812
+ },
2813
+ ansi256ToAnsi: {
2814
+ value(code) {
2815
+ if (code < 8) {
2816
+ return 30 + code;
2817
+ }
2818
+ if (code < 16) {
2819
+ return 90 + (code - 8);
2820
+ }
2821
+ let red;
2822
+ let green;
2823
+ let blue;
2824
+ if (code >= 232) {
2825
+ red = ((code - 232) * 10 + 8) / 255;
2826
+ green = red;
2827
+ blue = red;
2828
+ } else {
2829
+ code -= 16;
2830
+ const remainder = code % 36;
2831
+ red = Math.floor(code / 36) / 5;
2832
+ green = Math.floor(remainder / 6) / 5;
2833
+ blue = remainder % 6 / 5;
2834
+ }
2835
+ const value = Math.max(red, green, blue) * 2;
2836
+ if (value === 0) {
2837
+ return 30;
2838
+ }
2839
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
2840
+ if (value === 2) {
2841
+ result += 60;
2842
+ }
2843
+ return result;
2844
+ },
2845
+ enumerable: false
2846
+ },
2847
+ rgbToAnsi: {
2848
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
2849
+ enumerable: false
2850
+ },
2851
+ hexToAnsi: {
2852
+ value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
2853
+ enumerable: false
2854
+ }
2855
+ });
2856
+ return styles;
2857
+ }
2858
+ var ansiStyles = assembleStyles();
2859
+ var ansi_styles_default = ansiStyles;
2860
+
2861
+ // node_modules/chalk/source/vendor/supports-color/index.js
2862
+ import process2 from "process";
2863
+ import os from "os";
2864
+ import tty from "tty";
2865
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
2866
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
2867
+ const position = argv.indexOf(prefix + flag);
2868
+ const terminatorPosition = argv.indexOf("--");
2869
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
2870
+ }
2871
+ var { env } = process2;
2872
+ var flagForceColor;
2873
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
2874
+ flagForceColor = 0;
2875
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
2876
+ flagForceColor = 1;
2877
+ }
2878
+ function envForceColor() {
2879
+ if ("FORCE_COLOR" in env) {
2880
+ if (env.FORCE_COLOR === "true") {
2881
+ return 1;
2882
+ }
2883
+ if (env.FORCE_COLOR === "false") {
2884
+ return 0;
2885
+ }
2886
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
2887
+ }
2888
+ }
2889
+ function translateLevel(level) {
2890
+ if (level === 0) {
2891
+ return false;
2892
+ }
2893
+ return {
2894
+ level,
2895
+ hasBasic: true,
2896
+ has256: level >= 2,
2897
+ has16m: level >= 3
2898
+ };
2899
+ }
2900
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
2901
+ const noFlagForceColor = envForceColor();
2902
+ if (noFlagForceColor !== void 0) {
2903
+ flagForceColor = noFlagForceColor;
2904
+ }
2905
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
2906
+ if (forceColor === 0) {
2907
+ return 0;
2908
+ }
2909
+ if (sniffFlags) {
2910
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
2911
+ return 3;
2912
+ }
2913
+ if (hasFlag("color=256")) {
2914
+ return 2;
2915
+ }
2916
+ }
2917
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
2918
+ return 1;
2919
+ }
2920
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
2921
+ return 0;
2922
+ }
2923
+ const min = forceColor || 0;
2924
+ if (env.TERM === "dumb") {
2925
+ return min;
2926
+ }
2927
+ if (process2.platform === "win32") {
2928
+ const osRelease = os.release().split(".");
2929
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
2930
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
2931
+ }
2932
+ return 1;
2933
+ }
2934
+ if ("CI" in env) {
2935
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
2936
+ return 3;
2937
+ }
2938
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
2939
+ return 1;
2940
+ }
2941
+ return min;
2942
+ }
2943
+ if ("TEAMCITY_VERSION" in env) {
2944
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
2945
+ }
2946
+ if (env.COLORTERM === "truecolor") {
2947
+ return 3;
2948
+ }
2949
+ if (env.TERM === "xterm-kitty") {
2950
+ return 3;
2951
+ }
2952
+ if (env.TERM === "xterm-ghostty") {
2953
+ return 3;
2954
+ }
2955
+ if (env.TERM === "wezterm") {
2956
+ return 3;
2957
+ }
2958
+ if ("TERM_PROGRAM" in env) {
2959
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
2960
+ switch (env.TERM_PROGRAM) {
2961
+ case "iTerm.app": {
2962
+ return version >= 3 ? 3 : 2;
2963
+ }
2964
+ case "Apple_Terminal": {
2965
+ return 2;
2966
+ }
2967
+ }
2968
+ }
2969
+ if (/-256(color)?$/i.test(env.TERM)) {
2970
+ return 2;
2971
+ }
2972
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
2973
+ return 1;
2974
+ }
2975
+ if ("COLORTERM" in env) {
2976
+ return 1;
2977
+ }
2978
+ return min;
2979
+ }
2980
+ function createSupportsColor(stream, options = {}) {
2981
+ const level = _supportsColor(stream, {
2982
+ streamIsTTY: stream && stream.isTTY,
2983
+ ...options
2984
+ });
2985
+ return translateLevel(level);
2986
+ }
2987
+ var supportsColor = {
2988
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
2989
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
2990
+ };
2991
+ var supports_color_default = supportsColor;
2992
+
2993
+ // node_modules/chalk/source/utilities.js
2994
+ function stringReplaceAll(string, substring, replacer) {
2995
+ let index = string.indexOf(substring);
2996
+ if (index === -1) {
2997
+ return string;
2998
+ }
2999
+ const substringLength = substring.length;
3000
+ let endIndex = 0;
3001
+ let returnValue = "";
3002
+ do {
3003
+ returnValue += string.slice(endIndex, index) + substring + replacer;
3004
+ endIndex = index + substringLength;
3005
+ index = string.indexOf(substring, endIndex);
3006
+ } while (index !== -1);
3007
+ returnValue += string.slice(endIndex);
3008
+ return returnValue;
3009
+ }
3010
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
3011
+ let endIndex = 0;
3012
+ let returnValue = "";
3013
+ do {
3014
+ const gotCR = string[index - 1] === "\r";
3015
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
3016
+ endIndex = index + 1;
3017
+ index = string.indexOf("\n", endIndex);
3018
+ } while (index !== -1);
3019
+ returnValue += string.slice(endIndex);
3020
+ return returnValue;
3021
+ }
3022
+
3023
+ // node_modules/chalk/source/index.js
3024
+ var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
3025
+ var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
3026
+ var STYLER = /* @__PURE__ */ Symbol("STYLER");
3027
+ var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
3028
+ var levelMapping = [
3029
+ "ansi",
3030
+ "ansi",
3031
+ "ansi256",
3032
+ "ansi16m"
3033
+ ];
3034
+ var styles2 = /* @__PURE__ */ Object.create(null);
3035
+ var applyOptions = (object, options = {}) => {
3036
+ if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
3037
+ throw new Error("The `level` option should be an integer from 0 to 3");
3038
+ }
3039
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
3040
+ object.level = options.level === void 0 ? colorLevel : options.level;
3041
+ };
3042
+ var chalkFactory = (options) => {
3043
+ const chalk2 = (...strings) => strings.join(" ");
3044
+ applyOptions(chalk2, options);
3045
+ Object.setPrototypeOf(chalk2, createChalk.prototype);
3046
+ return chalk2;
3047
+ };
3048
+ function createChalk(options) {
3049
+ return chalkFactory(options);
3050
+ }
3051
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
3052
+ for (const [styleName, style] of Object.entries(ansi_styles_default)) {
3053
+ styles2[styleName] = {
3054
+ get() {
3055
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
3056
+ Object.defineProperty(this, styleName, { value: builder });
3057
+ return builder;
3058
+ }
3059
+ };
3060
+ }
3061
+ styles2.visible = {
3062
+ get() {
3063
+ const builder = createBuilder(this, this[STYLER], true);
3064
+ Object.defineProperty(this, "visible", { value: builder });
3065
+ return builder;
3066
+ }
3067
+ };
3068
+ var getModelAnsi = (model, level, type, ...arguments_) => {
3069
+ if (model === "rgb") {
3070
+ if (level === "ansi16m") {
3071
+ return ansi_styles_default[type].ansi16m(...arguments_);
3072
+ }
3073
+ if (level === "ansi256") {
3074
+ return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
3075
+ }
3076
+ return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
3077
+ }
3078
+ if (model === "hex") {
3079
+ return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
3080
+ }
3081
+ return ansi_styles_default[type][model](...arguments_);
3082
+ };
3083
+ var usedModels = ["rgb", "hex", "ansi256"];
3084
+ for (const model of usedModels) {
3085
+ styles2[model] = {
3086
+ get() {
3087
+ const { level } = this;
3088
+ return function(...arguments_) {
3089
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
3090
+ return createBuilder(this, styler, this[IS_EMPTY]);
3091
+ };
3092
+ }
3093
+ };
3094
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
3095
+ styles2[bgModel] = {
3096
+ get() {
3097
+ const { level } = this;
3098
+ return function(...arguments_) {
3099
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
3100
+ return createBuilder(this, styler, this[IS_EMPTY]);
3101
+ };
3102
+ }
3103
+ };
3104
+ }
3105
+ var proto = Object.defineProperties(() => {
3106
+ }, {
3107
+ ...styles2,
3108
+ level: {
3109
+ enumerable: true,
3110
+ get() {
3111
+ return this[GENERATOR].level;
3112
+ },
3113
+ set(level) {
3114
+ this[GENERATOR].level = level;
3115
+ }
3116
+ }
3117
+ });
3118
+ var createStyler = (open, close, parent) => {
3119
+ let openAll;
3120
+ let closeAll;
3121
+ if (parent === void 0) {
3122
+ openAll = open;
3123
+ closeAll = close;
3124
+ } else {
3125
+ openAll = parent.openAll + open;
3126
+ closeAll = close + parent.closeAll;
3127
+ }
3128
+ return {
3129
+ open,
3130
+ close,
3131
+ openAll,
3132
+ closeAll,
3133
+ parent
3134
+ };
3135
+ };
3136
+ var createBuilder = (self, _styler, _isEmpty) => {
3137
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
3138
+ Object.setPrototypeOf(builder, proto);
3139
+ builder[GENERATOR] = self;
3140
+ builder[STYLER] = _styler;
3141
+ builder[IS_EMPTY] = _isEmpty;
3142
+ return builder;
3143
+ };
3144
+ var applyStyle = (self, string) => {
3145
+ if (self.level <= 0 || !string) {
3146
+ return self[IS_EMPTY] ? "" : string;
3147
+ }
3148
+ let styler = self[STYLER];
3149
+ if (styler === void 0) {
3150
+ return string;
3151
+ }
3152
+ const { openAll, closeAll } = styler;
3153
+ if (string.includes("\x1B")) {
3154
+ while (styler !== void 0) {
3155
+ string = stringReplaceAll(string, styler.close, styler.open);
3156
+ styler = styler.parent;
3157
+ }
3158
+ }
3159
+ const lfIndex = string.indexOf("\n");
3160
+ if (lfIndex !== -1) {
3161
+ string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
3162
+ }
3163
+ return openAll + string + closeAll;
3164
+ };
3165
+ Object.defineProperties(createChalk.prototype, styles2);
3166
+ var chalk = createChalk();
3167
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3168
+ var source_default = chalk;
3169
+
3170
+ // src/ui/markdown.ts
3171
+ var theme = {
3172
+ heading: source_default.hex("#7fa8d4").bold,
3173
+ // dusty blue
3174
+ firstHeading: source_default.hex("#9ab8de").bold,
3175
+ // slightly lighter for h1
3176
+ strong: source_default.hex("#d6c9a8").bold,
3177
+ // warm sand
3178
+ em: source_default.hex("#b59ec4").italic,
3179
+ // soft mauve
3180
+ del: source_default.hex("#6b7280").strikethrough,
3181
+ // dim gray
3182
+ codespan: source_default.hex("#c8a98a"),
3183
+ // muted clay
3184
+ link: source_default.hex("#83b3a6").underline,
3185
+ // sage teal
3186
+ href: source_default.hex("#83b3a6").underline,
3187
+ blockquote: source_default.hex("#8a9aa8").italic,
3188
+ // slate
3189
+ listitem: source_default.hex("#c4c9cf"),
3190
+ // off-white
3191
+ paragraph: source_default.hex("#c4c9cf"),
3192
+ // off-white body text
3193
+ hr: source_default.hex("#4b5563")
3194
+ // faint rule
3195
+ };
3196
+ function highlightCode(code, lang) {
3197
+ if (!lang) return code;
3198
+ try {
3199
+ return highlight(code, { language: lang, ignoreIllegals: true });
3200
+ } catch {
3201
+ return code;
3202
+ }
3203
+ }
3204
+ var md = new Marked();
3205
+ md.use(
3206
+ markedTerminal({
3207
+ // Drop the literal `#` prefix on headings; render them styled instead.
3208
+ showSectionPrefix: false,
3209
+ // marked-terminal calls this for ``` blocks; fall back to plain on unknown lang.
3210
+ code: (code, lang) => highlightCode(code, lang),
3211
+ ...theme
3212
+ })
3213
+ );
3214
+ function renderMarkdown(content) {
3215
+ try {
3216
+ const out = md.parse(content, { async: false });
3217
+ return out.replace(/\n+$/, "");
3218
+ } catch {
3219
+ return content;
3220
+ }
3221
+ }
3222
+ function renderMarkdownStreaming(content) {
3223
+ const fences = (content.match(/^```/gm) ?? []).length;
3224
+ const balanced = fences % 2 === 1 ? content + "\n```" : content;
3225
+ return renderMarkdown(balanced);
3226
+ }
3227
+
2397
3228
  // src/ui/ThinkingBlock.tsx
2398
3229
  import { useState as useState2, useEffect as useEffect2 } from "react";
2399
3230
  import { Box as Box8, Text as Text8 } from "ink";
@@ -2436,7 +3267,12 @@ function ThinkingBlock({ content }) {
2436
3267
  " thoughts"
2437
3268
  ] })
2438
3269
  ] }),
2439
- visible && content ? /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: content }) }) : null
3270
+ visible && content ? (() => {
3271
+ const max = Math.max(4, (process.stdout.rows ?? 24) - 10);
3272
+ const lines = content.split("\n");
3273
+ const shown = lines.length > max ? lines.slice(-max) : lines;
3274
+ return /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: shown.join("\n") }) });
3275
+ })() : null
2440
3276
  ] });
2441
3277
  }
2442
3278
 
@@ -2450,9 +3286,16 @@ var EMPTY_STATE_HINTS = [
2450
3286
  ];
2451
3287
  var EMPTY_STATE_TITLE = "Ask anything, or try:";
2452
3288
 
2453
- // src/ui/ChatView.tsx
2454
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2455
- var COLLAPSED_LINES = 3;
3289
+ // src/ui/Message.tsx
3290
+ import { memo as memo2 } from "react";
3291
+ import { Box as Box10, Text as Text10 } from "ink";
3292
+
3293
+ // src/ui/ToolBlock.tsx
3294
+ import { Box as Box9, Text as Text9 } from "ink";
3295
+ import { highlight as highlight2 } from "cli-highlight";
3296
+
3297
+ // src/ui/toolExpand.ts
3298
+ import { useState as useState3, useEffect as useEffect3 } from "react";
2456
3299
  var globalToolExpanded = false;
2457
3300
  var toolExpandListeners = /* @__PURE__ */ new Set();
2458
3301
  function toggleToolExpanded() {
@@ -2470,6 +3313,8 @@ function useToolExpanded() {
2470
3313
  }, []);
2471
3314
  return expanded;
2472
3315
  }
3316
+
3317
+ // src/ui/layout.ts
2473
3318
  function formatTokens(n) {
2474
3319
  if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
2475
3320
  return String(n);
@@ -2485,6 +3330,61 @@ function countLines(s) {
2485
3330
  if (!s) return 0;
2486
3331
  return s.split("\n").length;
2487
3332
  }
3333
+ function truncate2(s, max) {
3334
+ if (s.length <= max) return s;
3335
+ return s.slice(0, max - 1) + "\u2026";
3336
+ }
3337
+ function clipTail(rendered, max) {
3338
+ const lines = rendered.split("\n");
3339
+ if (lines.length <= max) return { text: rendered, clipped: 0 };
3340
+ return { text: lines.slice(-max).join("\n"), clipped: lines.length - max };
3341
+ }
3342
+ function liveFrameRows() {
3343
+ const rows = process.stdout.rows ?? 24;
3344
+ return Math.max(6, rows - 8);
3345
+ }
3346
+ var COLLAPSED_LINES = 3;
3347
+ function estimateToolRows(use, result) {
3348
+ const input = use.input ?? {};
3349
+ const noErr = !result?.is_error;
3350
+ if (use.name === "write_file" && noErr) {
3351
+ const total = countLines(String(input.content ?? ""));
3352
+ const shown = Math.min(total, COLLAPSED_LINES);
3353
+ return 2 + shown + (total > shown ? 1 : 0);
3354
+ }
3355
+ if (use.name === "edit_file" && noErr) {
3356
+ const total = countLines(String(input.old_str ?? "")) + countLines(String(input.new_str ?? ""));
3357
+ const shown = Math.min(total, COLLAPSED_LINES);
3358
+ return 2 + shown + (total > shown ? 1 : 0);
3359
+ }
3360
+ let rows = 1;
3361
+ if (result) {
3362
+ const lines = (result.content ?? "").split("\n");
3363
+ const multi = (use.name === "run_bash" || use.name === "grep" || use.name === "glob" || result.is_error) && lines.length > 1;
3364
+ if (multi) {
3365
+ const shown = Math.min(lines.length, COLLAPSED_LINES);
3366
+ rows += 1 + shown + (lines.length > shown ? 1 : 0);
3367
+ } else {
3368
+ rows += 1;
3369
+ }
3370
+ }
3371
+ return rows;
3372
+ }
3373
+ function contentWidth() {
3374
+ return Math.max(20, (process.stdout.columns ?? 80) - 4);
3375
+ }
3376
+
3377
+ // src/ui/ToolBlock.tsx
3378
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3379
+ var COLLAPSED_LINES2 = 3;
3380
+ var TOOL_LABEL = {
3381
+ write_file: "Write",
3382
+ edit_file: "Update",
3383
+ read_file: "Read",
3384
+ run_bash: "Bash",
3385
+ glob: "Glob",
3386
+ grep: "Grep"
3387
+ };
2488
3388
  var EXT_LANG = {
2489
3389
  ts: "typescript",
2490
3390
  tsx: "typescript",
@@ -2529,7 +3429,7 @@ function langFromPath(path) {
2529
3429
  function highlightLine(text, lang) {
2530
3430
  if (!lang) return text;
2531
3431
  try {
2532
- return highlight(text, { language: lang, ignoreIllegals: true });
3432
+ return highlight2(text, { language: lang, ignoreIllegals: true });
2533
3433
  } catch {
2534
3434
  return text;
2535
3435
  }
@@ -2542,7 +3442,7 @@ function FileEditBlock({
2542
3442
  previewLines
2543
3443
  }) {
2544
3444
  const expanded = useToolExpanded();
2545
- const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES);
3445
+ const shown = expanded ? previewLines : previewLines.slice(0, COLLAPSED_LINES2);
2546
3446
  const extra = previewLines.length - shown.length;
2547
3447
  const lang = langFromPath(path);
2548
3448
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
@@ -2585,18 +3485,6 @@ function FileEditBlock({
2585
3485
  ] }) })
2586
3486
  ] });
2587
3487
  }
2588
- var TOOL_LABEL = {
2589
- write_file: "Write",
2590
- edit_file: "Update",
2591
- read_file: "Read",
2592
- run_bash: "Bash",
2593
- glob: "Glob",
2594
- grep: "Grep"
2595
- };
2596
- function truncate2(s, max) {
2597
- if (s.length <= max) return s;
2598
- return s.slice(0, max - 1) + "\u2026";
2599
- }
2600
3488
  function toolHeader(use) {
2601
3489
  const label = TOOL_LABEL[use.name] ?? use.name;
2602
3490
  const input = use.input ?? {};
@@ -2658,7 +3546,7 @@ function ToolResultBlock({ result, toolName }) {
2658
3546
  ] }) });
2659
3547
  }
2660
3548
  const MAX_LINE_WIDTH = 200;
2661
- const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
3549
+ const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES2);
2662
3550
  const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
2663
3551
  const extra = lines.length - shown.length;
2664
3552
  const header = toolName === "grep" || toolName === "glob" ? summarizeResult(result, toolName) : `${lines.length} line${lines.length === 1 ? "" : "s"}`;
@@ -2710,28 +3598,35 @@ function ToolUseLine({ use, result }) {
2710
3598
  result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
2711
3599
  ] });
2712
3600
  }
3601
+
3602
+ // src/ui/Message.tsx
3603
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2713
3604
  var UserMessage = memo2(function UserMessage2({ msg }) {
2714
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2715
- /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u276F " }),
2716
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
3605
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", marginBottom: 1, children: [
3606
+ /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "\u276F " }),
3607
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1, children: /* @__PURE__ */ jsx10(Text10, { children: msg.content }) })
2717
3608
  ] });
2718
3609
  });
2719
3610
  var AssistantMessage = memo2(function AssistantMessage2({ msg }) {
2720
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
2721
- msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
2722
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
2723
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
3611
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", marginBottom: 1, children: [
3612
+ msg.content && /* @__PURE__ */ jsxs10(Box10, { flexDirection: "row", children: [
3613
+ /* @__PURE__ */ jsx10(Text10, { color: "blue", children: "\u25CF " }),
3614
+ /* @__PURE__ */ jsx10(Box10, { width: contentWidth(), children: /* @__PURE__ */ jsx10(Text10, { wrap: "wrap", children: renderMarkdown(msg.content) }) })
2724
3615
  ] }),
2725
3616
  msg.tool_uses?.map((u) => {
2726
3617
  const r = msg.tool_results?.find((x) => x.tool_use_id === u.id);
2727
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
3618
+ return /* @__PURE__ */ jsx10(ToolUseLine, { use: u, result: r }, u.id);
2728
3619
  }),
2729
- msg.tokens && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
3620
+ msg.tokens && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, children: /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2730
3621
  `\u21B3 Completed \xB7 ${formatTokens(msg.tokens.prompt_eval + msg.tokens.eval)} tokens`,
2731
3622
  msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
2732
3623
  ] }) })
2733
3624
  ] });
2734
3625
  });
3626
+
3627
+ // src/ui/PermissionPrompt.tsx
3628
+ import { Box as Box11, Text as Text11 } from "ink";
3629
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2735
3630
  function summarizeInput(input) {
2736
3631
  if (!input || typeof input !== "object") return "";
2737
3632
  const obj = input;
@@ -2758,15 +3653,15 @@ function PermissionPrompt({ req, cursor }) {
2758
3653
  { label: "No", key: "no" }
2759
3654
  ];
2760
3655
  const summary = summarizeInput(req.input);
2761
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
2762
- /* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
2763
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
3656
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
3657
+ /* @__PURE__ */ jsx11(Text11, { color: "blue", bold: true, children: "Tool use" }),
3658
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text11, { children: [
2764
3659
  "Allow ",
2765
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
3660
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: label }),
2766
3661
  "?"
2767
3662
  ] }) }),
2768
- summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { wrap: "truncate", dimColor: true, children: summary }) }),
2769
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
3663
+ summary && /* @__PURE__ */ jsx11(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { wrap: "truncate", dimColor: true, children: summary }) }),
3664
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs11(Text11, { color: i === cursor ? "blue" : void 0, children: [
2770
3665
  i === cursor ? "\u276F " : " ",
2771
3666
  i + 1,
2772
3667
  ". ",
@@ -2774,6 +3669,9 @@ function PermissionPrompt({ req, cursor }) {
2774
3669
  ] }, opt.key)) })
2775
3670
  ] });
2776
3671
  }
3672
+
3673
+ // src/ui/ChatView.tsx
3674
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2777
3675
  function ChatView({
2778
3676
  messages,
2779
3677
  streaming,
@@ -2784,33 +3682,70 @@ function ChatView({
2784
3682
  pendingPermission,
2785
3683
  permissionCursor = 0,
2786
3684
  activeToolUses,
2787
- activeToolResults
3685
+ activeToolResults,
3686
+ header
2788
3687
  }) {
2789
3688
  const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
2790
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
2791
- empty && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
2792
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: EMPTY_STATE_TITLE }),
2793
- EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2794
- " ",
2795
- h
2796
- ] }, i))
2797
- ] }),
2798
- messages.map(
2799
- (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsx9(UserMessage, { msg }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
2800
- ),
2801
- thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
2802
- streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2803
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
2804
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
2805
- ] }),
2806
- activeToolUses?.map((u) => {
2807
- const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
2808
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
2809
- }),
2810
- pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
2811
- error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
2812
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
2813
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
3689
+ const log = [];
3690
+ if (header) log.push({ key: "header", node: header });
3691
+ messages.forEach((msg, i) => {
3692
+ log.push({
3693
+ key: `msg-${i}`,
3694
+ node: msg.role === "user" ? /* @__PURE__ */ jsx12(UserMessage, { msg }) : /* @__PURE__ */ jsx12(AssistantMessage, { msg })
3695
+ });
3696
+ });
3697
+ const liveBudget = liveFrameRows();
3698
+ let streamNode = null;
3699
+ let streamRows = 0;
3700
+ if (streaming && streamingContent) {
3701
+ const { text, clipped } = clipTail(renderMarkdownStreaming(streamingContent), liveBudget);
3702
+ streamRows = text.split("\n").length + (clipped > 0 ? 1 : 0);
3703
+ streamNode = /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
3704
+ clipped > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${clipped} more line${clipped === 1 ? "" : "s"} above \u2014 streaming\u2026` }),
3705
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", children: [
3706
+ /* @__PURE__ */ jsx12(Text12, { color: "blue", children: "\u25CF " }),
3707
+ /* @__PURE__ */ jsx12(Box12, { width: contentWidth(), children: /* @__PURE__ */ jsx12(Text12, { wrap: "wrap", children: text }) })
3708
+ ] })
3709
+ ] });
3710
+ }
3711
+ let toolNode = null;
3712
+ if (activeToolUses?.length) {
3713
+ const remaining = Math.max(4, liveBudget - streamRows);
3714
+ const resultById = new Map(activeToolResults?.map((r) => [r.tool_use_id, r]));
3715
+ const kept = [];
3716
+ let rows = 0;
3717
+ for (let i = activeToolUses.length - 1; i >= 0; i--) {
3718
+ const u = activeToolUses[i];
3719
+ const r = resultById.get(u.id);
3720
+ const h = estimateToolRows(u, r);
3721
+ if (rows + h > remaining && kept.length) break;
3722
+ rows += h;
3723
+ kept.unshift({ u, r });
3724
+ }
3725
+ const hidden = activeToolUses.length - kept.length;
3726
+ toolNode = /* @__PURE__ */ jsxs12(Fragment2, { children: [
3727
+ hidden > 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: `\u2191 ${hidden} earlier tool call${hidden === 1 ? "" : "s"} above` }),
3728
+ kept.map(({ u, r }) => /* @__PURE__ */ jsx12(ToolUseLine, { use: u, result: r }, u.id))
3729
+ ] });
3730
+ }
3731
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
3732
+ /* @__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) }),
3733
+ /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
3734
+ empty && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginBottom: 1, children: [
3735
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: EMPTY_STATE_TITLE }),
3736
+ EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3737
+ " ",
3738
+ h
3739
+ ] }, i))
3740
+ ] }),
3741
+ thinking && /* @__PURE__ */ jsx12(ThinkingBlock, { content: thinkingContent }),
3742
+ streamNode,
3743
+ toolNode,
3744
+ pendingPermission && /* @__PURE__ */ jsx12(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
3745
+ error && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", marginBottom: 1, children: [
3746
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: "\u25CF " }),
3747
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: error })
3748
+ ] })
2814
3749
  ] })
2815
3750
  ] });
2816
3751
  }
@@ -3050,6 +3985,14 @@ function clearPasteStore() {
3050
3985
  pasteStore.clear();
3051
3986
  pasteCounter = 0;
3052
3987
  }
3988
+ var inputHistory = [];
3989
+ var historyIndex = -1;
3990
+ var historyDraft = "";
3991
+ function pushHistory(line) {
3992
+ if (!line) return;
3993
+ if (inputHistory[inputHistory.length - 1] === line) return;
3994
+ inputHistory.push(line);
3995
+ }
3053
3996
  function expandPastes(text) {
3054
3997
  let out = text;
3055
3998
  for (const [chip, full] of pasteStore) out = out.split(chip).join(full);
@@ -3087,6 +4030,8 @@ function useKeyboard(opts) {
3087
4030
  agent,
3088
4031
  input,
3089
4032
  setInput,
4033
+ caret,
4034
+ setCaret,
3090
4035
  paletteCursor,
3091
4036
  setPaletteCursor,
3092
4037
  filePickerCursor,
@@ -3294,36 +4239,43 @@ function useKeyboard(opts) {
3294
4239
  const mention = !paletteOpen ? parseMention(input) : null;
3295
4240
  const fileMatches = mention ? searchFiles(process.cwd(), mention.query) : [];
3296
4241
  const fileOpen = mention !== null && fileMatches.length > 0;
3297
- if (paletteOpen && key.upArrow) {
4242
+ if (paletteOpen && historyIndex === -1 && key.upArrow) {
3298
4243
  setPaletteCursor((i) => Math.max(0, i - 1));
3299
4244
  return;
3300
4245
  }
3301
- if (paletteOpen && key.downArrow) {
4246
+ if (paletteOpen && historyIndex === -1 && key.downArrow) {
3302
4247
  setPaletteCursor((i) => Math.min(matches2.length - 1, i + 1));
3303
4248
  return;
3304
4249
  }
3305
4250
  if (paletteOpen && (key.tab || key.return) && matches2[paletteCursor] && input !== matches2[paletteCursor].name) {
3306
- setInput(() => matches2[paletteCursor].name);
4251
+ const name = matches2[paletteCursor].name;
4252
+ setInput(() => name);
4253
+ setCaret(() => name.length);
3307
4254
  setPaletteCursor(() => 0);
3308
4255
  return;
3309
4256
  }
3310
4257
  if (paletteOpen && key.escape) {
3311
4258
  clearPasteStore();
3312
4259
  setInput(() => "");
4260
+ setCaret(() => 0);
3313
4261
  setPaletteCursor(() => 0);
3314
4262
  return;
3315
4263
  }
3316
- if (fileOpen && key.upArrow) {
4264
+ if (fileOpen && historyIndex === -1 && key.upArrow) {
3317
4265
  setFilePickerCursor((i) => Math.max(0, i - 1));
3318
4266
  return;
3319
4267
  }
3320
- if (fileOpen && key.downArrow) {
4268
+ if (fileOpen && historyIndex === -1 && key.downArrow) {
3321
4269
  setFilePickerCursor((i) => Math.min(fileMatches.length - 1, i + 1));
3322
4270
  return;
3323
4271
  }
3324
4272
  if (fileOpen && key.tab && fileMatches[filePickerCursor]) {
3325
4273
  const picked = fileMatches[filePickerCursor];
3326
- setInput((s) => s.slice(0, mention.start) + "@" + picked + " ");
4274
+ setInput((s) => {
4275
+ const next = s.slice(0, mention.start) + "@" + picked + " ";
4276
+ setCaret(() => next.length);
4277
+ return next;
4278
+ });
3327
4279
  setFilePickerCursor(() => 0);
3328
4280
  return;
3329
4281
  }
@@ -3331,8 +4283,36 @@ function useKeyboard(opts) {
3331
4283
  setFilePickerCursor(() => 0);
3332
4284
  return;
3333
4285
  }
4286
+ if ((historyIndex !== -1 || !paletteOpen && !fileOpen) && key.upArrow) {
4287
+ if (inputHistory.length === 0) return;
4288
+ if (historyIndex === -1) {
4289
+ historyDraft = input;
4290
+ historyIndex = inputHistory.length - 1;
4291
+ } else if (historyIndex > 0) historyIndex--;
4292
+ const val = inputHistory[historyIndex];
4293
+ setInput(() => val);
4294
+ setCaret(() => val.length);
4295
+ return;
4296
+ }
4297
+ if ((historyIndex !== -1 || !paletteOpen && !fileOpen) && key.downArrow) {
4298
+ if (historyIndex === -1) return;
4299
+ if (historyIndex < inputHistory.length - 1) {
4300
+ historyIndex++;
4301
+ const val = inputHistory[historyIndex];
4302
+ setInput(() => val);
4303
+ setCaret(() => val.length);
4304
+ } else {
4305
+ historyIndex = -1;
4306
+ setInput(() => historyDraft);
4307
+ setCaret(() => historyDraft.length);
4308
+ }
4309
+ return;
4310
+ }
3334
4311
  if (key.return) {
3335
4312
  const trimmed = input.trim();
4313
+ pushHistory(trimmed);
4314
+ historyIndex = -1;
4315
+ historyDraft = "";
3336
4316
  if (trimmed === "/models") {
3337
4317
  setPickerQuery("");
3338
4318
  setCursor(() => Math.max(0, models.findIndex((m) => m === cfg.model)));
@@ -3369,30 +4349,49 @@ function useKeyboard(opts) {
3369
4349
  }
3370
4350
  clearPasteStore();
3371
4351
  setInput(() => "");
4352
+ setCaret(() => 0);
3372
4353
  setPaletteCursor(() => 0);
3373
4354
  return;
3374
4355
  }
4356
+ if (key.leftArrow) {
4357
+ setCaret((i) => Math.max(0, i - 1));
4358
+ return;
4359
+ }
4360
+ if (key.rightArrow) {
4361
+ setCaret((i) => Math.min(input.length, i + 1));
4362
+ return;
4363
+ }
4364
+ if (key.ctrl && char === "a") {
4365
+ setCaret(() => 0);
4366
+ return;
4367
+ }
4368
+ if (key.ctrl && char === "e") {
4369
+ setCaret(() => input.length);
4370
+ return;
4371
+ }
3375
4372
  if (key.backspace || key.delete) {
3376
- setInput((s) => {
3377
- setPaletteCursor(() => 0);
3378
- setFilePickerCursor(() => 0);
3379
- let match = "";
3380
- for (const chip of pasteStore.keys()) {
3381
- if (s.endsWith(chip) && chip.length > match.length) match = chip;
3382
- }
3383
- if (match) {
3384
- pasteStore.delete(match);
3385
- return s.slice(0, -match.length);
3386
- }
3387
- return s.slice(0, -1);
3388
- });
4373
+ historyIndex = -1;
4374
+ setPaletteCursor(() => 0);
4375
+ setFilePickerCursor(() => 0);
4376
+ if (caret <= 0) return;
4377
+ const before = input.slice(0, caret);
4378
+ let match = "";
4379
+ for (const chip of pasteStore.keys()) {
4380
+ if (before.endsWith(chip) && chip.length > match.length) match = chip;
4381
+ }
4382
+ const cut = match ? match.length : 1;
4383
+ if (match) pasteStore.delete(match);
4384
+ setInput((s) => s.slice(0, caret - cut) + s.slice(caret));
4385
+ setCaret((i) => Math.max(0, i - cut));
3389
4386
  } else if (char && !key.ctrl && !key.meta && !key.tab) {
3390
4387
  const text = sanitizePaste(char);
3391
- if (text) setInput((s) => {
4388
+ if (text) {
4389
+ historyIndex = -1;
3392
4390
  setPaletteCursor(() => 0);
3393
4391
  setFilePickerCursor(() => 0);
3394
- return s + text;
3395
- });
4392
+ setInput((s) => s.slice(0, caret) + text + s.slice(caret));
4393
+ setCaret((i) => i + text.length);
4394
+ }
3396
4395
  }
3397
4396
  }
3398
4397
  });
@@ -3434,14 +4433,16 @@ async function checkForUpdate() {
3434
4433
  }
3435
4434
 
3436
4435
  // src/ui/App.tsx
3437
- import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
4436
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
3438
4437
  function App() {
3439
4438
  const { exit } = useApp();
3440
4439
  const cwd = process.cwd().replace(homedir6(), "~").split(sep2).join("/");
3441
4440
  const [cfg, setCfg] = useState5(loadConfig());
3442
4441
  const [models, setModels] = useState5([]);
3443
- const [contexts, setContexts] = useState5({});
3444
- const [activeCtx, setActiveCtx] = useState5(null);
4442
+ const [contexts, setContexts] = useState5(() => cfg.modelContexts ?? {});
4443
+ const [activeCtx, setActiveCtx] = useState5(
4444
+ () => cfg.model ? cfg.modelContexts?.[cfg.model] ?? null : null
4445
+ );
3445
4446
  const [state, setState] = useState5("loading");
3446
4447
  const [cursor, setCursor] = useState5(0);
3447
4448
  const [pickerQuery, setPickerQuery] = useState5("");
@@ -3451,6 +4452,7 @@ function App() {
3451
4452
  const [sessions, setSessions] = useState5([]);
3452
4453
  const [notice, setNotice] = useState5(null);
3453
4454
  const [input, setInput] = useState5("");
4455
+ const [caret, setCaret] = useState5(0);
3454
4456
  const [paletteCursor, setPaletteCursor] = useState5(0);
3455
4457
  const [filePickerCursor, setFilePickerCursor] = useState5(0);
3456
4458
  const agent = useAgentRunner(cfg.model, activeCtx);
@@ -3492,12 +4494,20 @@ function App() {
3492
4494
  } else {
3493
4495
  setState(hasModel ? "ready" : "select-model");
3494
4496
  }
3495
- Promise.all(m.map((name) => modelContext3(name).then((ctx) => [name, ctx]))).then((pairs) => {
4497
+ Promise.all(
4498
+ m.map(
4499
+ (name) => modelContext3(name).then((ctx) => [name, ctx]).catch(() => [name, null])
4500
+ )
4501
+ ).then((pairs) => {
3496
4502
  if (stale()) return;
3497
4503
  const map = Object.fromEntries(pairs);
3498
4504
  setContexts(map);
4505
+ const resolved = Object.fromEntries(
4506
+ pairs.filter((p) => p[1] != null)
4507
+ );
4508
+ if (Object.keys(resolved).length) setModelContexts(resolved);
3499
4509
  const active2 = (hasModel ? cfg.model : void 0) ?? m[0];
3500
- if (active2 && map[active2]) setActiveCtx(map[active2]);
4510
+ if (active2 && map[active2] != null) setActiveCtx(map[active2]);
3501
4511
  }).catch(() => {
3502
4512
  });
3503
4513
  }).catch((err) => {
@@ -3542,6 +4552,8 @@ function App() {
3542
4552
  agent,
3543
4553
  input,
3544
4554
  setInput,
4555
+ caret,
4556
+ setCaret,
3545
4557
  paletteCursor,
3546
4558
  setPaletteCursor,
3547
4559
  filePickerCursor,
@@ -3562,10 +4574,10 @@ function App() {
3562
4574
  if (used < activeCtx * 0.7) return null;
3563
4575
  return Math.round(used / activeCtx * 100);
3564
4576
  })();
3565
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, children: [
3566
- /* @__PURE__ */ jsx10(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
3567
- state === "loading" && !agent.error && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
3568
- agent.error && state !== "ready" && /* @__PURE__ */ jsx10(
4577
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
4578
+ state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
4579
+ state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
4580
+ agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
3569
4581
  ChatView,
3570
4582
  {
3571
4583
  messages: [],
@@ -3575,7 +4587,7 @@ function App() {
3575
4587
  error: agent.error
3576
4588
  }
3577
4589
  ),
3578
- (state === "select-model" || state === "models") && /* @__PURE__ */ jsx10(
4590
+ (state === "select-model" || state === "models") && /* @__PURE__ */ jsx13(
3579
4591
  ModelsView,
3580
4592
  {
3581
4593
  models: filteredModels,
@@ -3588,7 +4600,7 @@ function App() {
3588
4600
  requireSelection: state === "select-model"
3589
4601
  }
3590
4602
  ),
3591
- state === "providers" && /* @__PURE__ */ jsx10(
4603
+ state === "providers" && /* @__PURE__ */ jsx13(
3592
4604
  ProviderPicker,
3593
4605
  {
3594
4606
  entries: filteredProviders,
@@ -3597,10 +4609,10 @@ function App() {
3597
4609
  query: pickerQuery
3598
4610
  }
3599
4611
  ),
3600
- state === "sessions" && /* @__PURE__ */ jsx10(SessionsView, { sessions, cursor }),
3601
- state === "ready" && /* @__PURE__ */ jsxs10(Fragment2, { children: [
3602
- notice && /* @__PURE__ */ jsx10(Box10, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: `\u2713 ${notice}` }) }),
3603
- /* @__PURE__ */ jsx10(
4612
+ state === "sessions" && /* @__PURE__ */ jsx13(SessionsView, { sessions, cursor }),
4613
+ state === "ready" && /* @__PURE__ */ jsxs13(Fragment3, { children: [
4614
+ notice && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "green", children: `\u2713 ${notice}` }) }),
4615
+ /* @__PURE__ */ jsx13(
3604
4616
  ChatView,
3605
4617
  {
3606
4618
  messages: agent.messages,
@@ -3612,18 +4624,20 @@ function App() {
3612
4624
  pendingPermission: agent.pendingPermission,
3613
4625
  permissionCursor: agent.permissionCursor,
3614
4626
  activeToolUses: agent.activeToolUses,
3615
- activeToolResults: agent.activeToolResults
4627
+ activeToolResults: agent.activeToolResults,
4628
+ header: /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd })
3616
4629
  }
3617
4630
  ),
3618
- input.startsWith("/") && /* @__PURE__ */ jsx10(CommandPalette, { filter: input, cursor: paletteCursor }),
3619
- 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` }) }),
4631
+ updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` }) }),
4632
+ input.startsWith("/") && /* @__PURE__ */ jsx13(CommandPalette, { filter: input, cursor: paletteCursor }),
4633
+ 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` }) }),
3620
4634
  !input.startsWith("/") && (() => {
3621
4635
  const m = parseMention(input);
3622
4636
  if (!m) return null;
3623
- return /* @__PURE__ */ jsx10(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
4637
+ return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
3624
4638
  })(),
3625
- /* @__PURE__ */ jsx10(InputBar, { input, disabled: agent.busy, processingLabel: agent.processingLabel }),
3626
- !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" }) })
4639
+ /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
4640
+ !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" }) })
3627
4641
  ] })
3628
4642
  ] });
3629
4643
  }