@wrongstack/tui 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useReducer, useRef, useEffect, useMemo } from 'react';
1
+ import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
2
2
  import { render, useApp, Box, useStdout, Static, Text, useInput } from 'ink';
3
3
  import * as path2 from 'path';
4
4
  import * as fs2 from 'fs/promises';
@@ -187,15 +187,20 @@ function padCell(text, width, align) {
187
187
  }
188
188
  return text + " ".repeat(pad);
189
189
  }
190
- function History({ entries, streamingText }) {
190
+ function History({ entries, streamingText, toolStream }) {
191
191
  const { stdout } = useStdout();
192
192
  const termWidth = stdout?.columns ?? 80;
193
193
  const tail = streamingText ? tailForDisplay(streamingText, MAX_STREAM_DISPLAY_CHARS) : "";
194
+ const toolTail = toolStream && toolStream.text ? tailForDisplay(toolStream.text, MAX_STREAM_DISPLAY_CHARS) : "";
194
195
  return /* @__PURE__ */ jsxs(Fragment, { children: [
195
196
  /* @__PURE__ */ jsx(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth }) }, entry.id) }),
196
197
  tail ? /* @__PURE__ */ jsxs(Box, { children: [
197
198
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
198
199
  /* @__PURE__ */ jsx(Text, { children: tail })
200
+ ] }) : null,
201
+ toolTail ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
202
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `\u25C6 ${toolStream.name} ` }),
203
+ /* @__PURE__ */ jsx(Text, { children: toolTail })
199
204
  ] }) : null
200
205
  ] });
201
206
  }
@@ -212,18 +217,19 @@ function tailForDisplay(text, maxChars) {
212
217
  function DiffBlock({ rows, hidden }) {
213
218
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, marginTop: 0, children: [
214
219
  rows.map((row, i) => {
220
+ const key = i;
215
221
  if (row.kind === "hunk") {
216
- return /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: row.text }, i);
222
+ return /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: row.text }, key);
217
223
  }
218
224
  if (row.kind === "meta") {
219
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, i);
225
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
220
226
  }
221
227
  if (row.kind === "ctx") {
222
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, i);
228
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
223
229
  }
224
230
  const bg = row.kind === "add" ? "green" : "red";
225
231
  const fg = row.kind === "add" ? "black" : "white";
226
- return /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: fg, children: row.text }, i);
232
+ return /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: fg, children: row.text }, key);
227
233
  }),
228
234
  hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
229
235
  ] });
@@ -254,15 +260,13 @@ function Entry({ entry, termWidth }) {
254
260
  ] }) : null,
255
261
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` })
256
262
  ] }),
257
- outLines.map((line, i) => {
258
- const isLast = i === outLines.length - 1 && !diff;
259
- const prefix = isLast ? " \u2514\u2500 " : " \u251C\u2500 ";
260
- const errish = !entry.ok || line.startsWith("!");
261
- return /* @__PURE__ */ jsxs(Text, { children: [
262
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }),
263
- /* @__PURE__ */ jsx(Text, { color: errish ? "red" : void 0, dimColor: !errish, children: line })
264
- ] }, i);
265
- }),
263
+ outLines.map((line, i) => (
264
+ // biome-ignore lint/suspicious/noArrayIndexKey: tool output lines are static, index is stable
265
+ /* @__PURE__ */ jsxs(Text, { children: [
266
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: i === outLines.length - 1 && !diff ? " \u2514\u2500 " : " \u251C\u2500 " }),
267
+ /* @__PURE__ */ jsx(Text, { color: !entry.ok || line.startsWith("!") ? "red" : void 0, dimColor: entry.ok && !line.startsWith("!"), children: line })
268
+ ] }, i)
269
+ )),
266
270
  diff ? /* @__PURE__ */ jsx(DiffBlock, { rows: diff.rows, hidden: diff.hidden }) : null
267
271
  ] });
268
272
  }
@@ -274,6 +278,30 @@ function Entry({ entry, termWidth }) {
274
278
  return /* @__PURE__ */ jsx(Text, { color: "red", children: entry.text });
275
279
  case "turn-summary":
276
280
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
281
+ case "confirm":
282
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
283
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
284
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
285
+ /* @__PURE__ */ jsx(Text, { children: " " }),
286
+ /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
287
+ ] }),
288
+ entry.input && typeof entry.input === "object" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: Object.entries(entry.input).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`).join(" ") }) : null,
289
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
290
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
291
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
292
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
293
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
294
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
295
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
296
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
297
+ " always (",
298
+ entry.suggestedPattern,
299
+ ") "
300
+ ] }),
301
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
302
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
303
+ ] }) })
304
+ ] });
277
305
  case "banner":
278
306
  return /* @__PURE__ */ jsx(Banner, { entry });
279
307
  }
@@ -306,6 +334,15 @@ function Banner({
306
334
  entry.model
307
335
  ] })
308
336
  ] }),
337
+ entry.family ? /* @__PURE__ */ jsxs(Text, { children: [
338
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " family " }),
339
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.family })
340
+ ] }) : null,
341
+ entry.keyTail ? /* @__PURE__ */ jsxs(Text, { children: [
342
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " key " }),
343
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CF\u25CF\u25CF\u2026" }),
344
+ /* @__PURE__ */ jsx(Text, { children: entry.keyTail })
345
+ ] }) : null,
309
346
  /* @__PURE__ */ jsxs(Text, { children: [
310
347
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
311
348
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
@@ -888,7 +925,7 @@ function scanNumberedRange(text) {
888
925
  let count = 0;
889
926
  for (const line of text.split("\n")) {
890
927
  const m = line.match(/^\s*(\d+)→/);
891
- if (m && m[1]) {
928
+ if (m?.[1]) {
892
929
  const n = Number.parseInt(m[1], 10);
893
930
  if (Number.isFinite(n)) {
894
931
  if (first === void 0) first = n;
@@ -928,27 +965,21 @@ function Input({
928
965
  const before = value.slice(0, cursor);
929
966
  const at = value.slice(cursor, cursor + 1) || " ";
930
967
  const after = value.slice(cursor + 1);
931
- const borderColor = disabled ? "red" : value.length > 0 ? "cyan" : "gray";
968
+ const promptColor = disabled ? "red" : "cyan";
932
969
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
933
- placeholders.map((p, i) => /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
934
- " \u21B3 ",
935
- p
936
- ] }, i)),
937
- /* @__PURE__ */ jsx(
938
- Box,
939
- {
940
- borderStyle: "round",
941
- borderColor,
942
- paddingX: 1,
943
- width: "100%",
944
- children: /* @__PURE__ */ jsxs(Text, { children: [
945
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: prompt }),
946
- before,
947
- /* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
948
- after
949
- ] })
950
- }
951
- ),
970
+ placeholders.map((p, i) => (
971
+ // biome-ignore lint/suspicious/noArrayIndexKey: placeholders are append-only, index is stable
972
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
973
+ " \u21B3 ",
974
+ p
975
+ ] }, i)
976
+ )),
977
+ /* @__PURE__ */ jsxs(Text, { children: [
978
+ /* @__PURE__ */ jsx(Text, { color: promptColor, children: prompt }),
979
+ before,
980
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: at }),
981
+ after
982
+ ] }),
952
983
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
953
984
  ] });
954
985
  }
@@ -1186,6 +1217,117 @@ function SlashMenu({ query, matches, selected }) {
1186
1217
  matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1187
1218
  ] });
1188
1219
  }
1220
+ function ModelPicker({
1221
+ step,
1222
+ providerOptions,
1223
+ modelOptions,
1224
+ selected,
1225
+ pickedProviderId,
1226
+ hint
1227
+ }) {
1228
+ if (step === "provider") {
1229
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1230
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1231
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1232
+ providerOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no providers with keys \u2014 add one via `wstack auth`)" }) : providerOptions.map((p, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1233
+ i === selected ? "\u203A " : " ",
1234
+ /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
1235
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1236
+ " [",
1237
+ p.family,
1238
+ "]"
1239
+ ] }),
1240
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1241
+ " ",
1242
+ p.models.length,
1243
+ " model",
1244
+ p.models.length === 1 ? "" : "s"
1245
+ ] })
1246
+ ] }, p.id)),
1247
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1248
+ ] });
1249
+ }
1250
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1251
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1252
+ "\u2501\u2501 Switch model \u2014 Step 2/2: Pick model",
1253
+ " ",
1254
+ "(",
1255
+ pickedProviderId,
1256
+ ") \u2501\u2501"
1257
+ ] }),
1258
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1259
+ modelOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no models known for this provider)" }) : modelOptions.map((id, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1260
+ i === selected ? "\u203A " : " ",
1261
+ id
1262
+ ] }, id)),
1263
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1264
+ ] });
1265
+ }
1266
+ function stringifyInput(input) {
1267
+ if (!input || typeof input !== "object") return "";
1268
+ const obj = input;
1269
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
1270
+ }
1271
+ function truncate(s, max) {
1272
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1273
+ }
1274
+ function hasDiff(input) {
1275
+ return Boolean(
1276
+ input && typeof input === "object" && "diff" in input
1277
+ );
1278
+ }
1279
+ function renderDiffLine(line) {
1280
+ const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
1281
+ return /* @__PURE__ */ jsxs(Text, { color: prefix, children: [
1282
+ line,
1283
+ "\n"
1284
+ ] }, line);
1285
+ }
1286
+ function renderDiff(diff) {
1287
+ const lines = diff.split("\n").filter((l) => l.length > 0).slice(0, 20);
1288
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
1289
+ }
1290
+ function ConfirmPrompt({ toolName, input, suggestedPattern, onDecision }) {
1291
+ useInput((_, key) => {
1292
+ if (key.return) {
1293
+ onDecision("yes");
1294
+ } else if (key.escape) {
1295
+ onDecision("no");
1296
+ } else if (key.ctrl && _.toLowerCase() === "a") {
1297
+ onDecision("always");
1298
+ } else if (key.ctrl && _.toLowerCase() === "d") {
1299
+ onDecision("deny");
1300
+ }
1301
+ });
1302
+ const inputSummary = stringifyInput(input);
1303
+ const showDiff = hasDiff(input);
1304
+ const inp = input;
1305
+ const diff = typeof inp?.diff === "string" ? inp.diff : "";
1306
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
1307
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1308
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
1309
+ /* @__PURE__ */ jsx(Text, { children: " " }),
1310
+ /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
1311
+ ] }),
1312
+ inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
1313
+ showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
1314
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1315
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
1316
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
1317
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
1318
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
1319
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
1320
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
1321
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1322
+ " always (",
1323
+ suggestedPattern,
1324
+ ") "
1325
+ ] }),
1326
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
1327
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
1328
+ ] }) })
1329
+ ] });
1330
+ }
1189
1331
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
1190
1332
  "node_modules",
1191
1333
  ".git",
@@ -1344,7 +1486,9 @@ function runCmd(cmd, args) {
1344
1486
  return new Promise((resolve) => {
1345
1487
  const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1346
1488
  let out = "";
1347
- child.stdout.on("data", (c) => out += String(c));
1489
+ child.stdout.on("data", (c) => {
1490
+ out += String(c);
1491
+ });
1348
1492
  child.on("error", () => resolve(null));
1349
1493
  child.on("exit", (code) => resolve(code === 0 ? out : null));
1350
1494
  });
@@ -1577,6 +1721,26 @@ function reducer(state, action) {
1577
1721
  }
1578
1722
  return state;
1579
1723
  }
1724
+ case "toolStreamAppend": {
1725
+ const cur = state.toolStream;
1726
+ if (cur && cur.toolUseId === action.toolUseId) {
1727
+ return {
1728
+ ...state,
1729
+ toolStream: { ...cur, text: cur.text + action.text }
1730
+ };
1731
+ }
1732
+ return {
1733
+ ...state,
1734
+ toolStream: { toolUseId: action.toolUseId, name: action.name, text: action.text }
1735
+ };
1736
+ }
1737
+ case "toolStreamClear": {
1738
+ if (state.toolStream === null) return state;
1739
+ const t = state.toolStream;
1740
+ if (action.toolUseId !== void 0 && action.toolUseId !== t.toolUseId) return state;
1741
+ if (action.name !== void 0 && action.toolUseId === void 0 && action.name !== t.name) return state;
1742
+ return { ...state, toolStream: null };
1743
+ }
1580
1744
  case "enqueue": {
1581
1745
  const item = { ...action.item, id: state.nextQueueId };
1582
1746
  return {
@@ -1632,6 +1796,72 @@ function reducer(state, action) {
1632
1796
  const entry = next === 0 ? "" : state.inputHistory[next - 1] ?? "";
1633
1797
  return { ...state, historyIndex: next, buffer: entry, cursor: entry.length };
1634
1798
  }
1799
+ case "modelPickerOpen":
1800
+ return {
1801
+ ...state,
1802
+ modelPicker: {
1803
+ open: true,
1804
+ step: "provider",
1805
+ providerOptions: action.providers,
1806
+ modelOptions: [],
1807
+ selected: 0,
1808
+ hint: void 0
1809
+ }
1810
+ };
1811
+ case "modelPickerClose":
1812
+ return {
1813
+ ...state,
1814
+ modelPicker: {
1815
+ open: false,
1816
+ step: "provider",
1817
+ providerOptions: [],
1818
+ modelOptions: [],
1819
+ selected: 0
1820
+ }
1821
+ };
1822
+ case "modelPickerMove": {
1823
+ if (!state.modelPicker.open) return state;
1824
+ const len = state.modelPicker.step === "provider" ? state.modelPicker.providerOptions.length : state.modelPicker.modelOptions.length;
1825
+ if (len === 0) return state;
1826
+ const next = (state.modelPicker.selected + action.delta + len) % len;
1827
+ return {
1828
+ ...state,
1829
+ modelPicker: { ...state.modelPicker, selected: next }
1830
+ };
1831
+ }
1832
+ case "modelPickerPickProvider":
1833
+ return {
1834
+ ...state,
1835
+ modelPicker: {
1836
+ ...state.modelPicker,
1837
+ step: "model",
1838
+ modelOptions: action.models,
1839
+ selected: 0,
1840
+ pickedProviderId: action.providerId,
1841
+ hint: void 0
1842
+ }
1843
+ };
1844
+ case "modelPickerBack":
1845
+ return {
1846
+ ...state,
1847
+ modelPicker: {
1848
+ ...state.modelPicker,
1849
+ step: "provider",
1850
+ modelOptions: [],
1851
+ selected: 0,
1852
+ pickedProviderId: void 0,
1853
+ hint: void 0
1854
+ }
1855
+ };
1856
+ case "modelPickerHint":
1857
+ return {
1858
+ ...state,
1859
+ modelPicker: { ...state.modelPicker, hint: action.text }
1860
+ };
1861
+ case "confirmOpen":
1862
+ return { ...state, confirm: action.info };
1863
+ case "confirmClose":
1864
+ return { ...state, confirm: null };
1635
1865
  }
1636
1866
  }
1637
1867
  var PASTE_THRESHOLD_CHARS = 200;
@@ -1647,10 +1877,16 @@ function App({
1647
1877
  yolo = false,
1648
1878
  appVersion,
1649
1879
  provider,
1880
+ family,
1881
+ keyTail,
1882
+ getPickableProviders,
1883
+ switchProviderAndModel,
1650
1884
  effectiveMaxContext,
1651
1885
  onExit
1652
1886
  }) {
1653
1887
  const { exit } = useApp();
1888
+ const [liveModel, setLiveModel] = useState(model);
1889
+ const [liveProvider, setLiveProvider] = useState(provider ?? "agent");
1654
1890
  const [state, dispatch] = useReducer(reducer, {
1655
1891
  entries: banner ? [
1656
1892
  {
@@ -1659,13 +1895,16 @@ function App({
1659
1895
  version: appVersion ?? "dev",
1660
1896
  provider: provider ?? "agent",
1661
1897
  model,
1662
- cwd: agent.ctx.cwd
1898
+ cwd: agent.ctx.cwd,
1899
+ family,
1900
+ keyTail
1663
1901
  }
1664
1902
  ] : [],
1665
1903
  buffer: "",
1666
1904
  cursor: 0,
1667
1905
  placeholders: [],
1668
1906
  streamingText: "",
1907
+ toolStream: null,
1669
1908
  status: "idle",
1670
1909
  interrupts: 0,
1671
1910
  hint: "",
@@ -1676,7 +1915,15 @@ function App({
1676
1915
  queue: [],
1677
1916
  nextQueueId: 1,
1678
1917
  inputHistory: [],
1679
- historyIndex: 0
1918
+ historyIndex: 0,
1919
+ modelPicker: {
1920
+ open: false,
1921
+ step: "provider",
1922
+ providerOptions: [],
1923
+ modelOptions: [],
1924
+ selected: 0
1925
+ },
1926
+ confirm: null
1680
1927
  });
1681
1928
  const builderRef = useRef(null);
1682
1929
  if (builderRef.current === null) {
@@ -1890,6 +2137,23 @@ function App({
1890
2137
  slashRegistry.unregister("queue");
1891
2138
  };
1892
2139
  }, [slashRegistry]);
2140
+ useEffect(() => {
2141
+ if (!getPickableProviders || !switchProviderAndModel) return;
2142
+ const cmd = {
2143
+ name: "model",
2144
+ aliases: ["provider", "switch"],
2145
+ description: "Pick a provider + model interactively (two-step).",
2146
+ async run() {
2147
+ const providers = await getPickableProviders();
2148
+ dispatch({ type: "modelPickerOpen", providers });
2149
+ return { message: void 0 };
2150
+ }
2151
+ };
2152
+ slashRegistry.register(cmd);
2153
+ return () => {
2154
+ slashRegistry.unregister("model");
2155
+ };
2156
+ }, [slashRegistry, getPickableProviders, switchProviderAndModel]);
1893
2157
  useEffect(() => {
1894
2158
  const FLUSH_MS = 100;
1895
2159
  const flush = () => {
@@ -1908,6 +2172,15 @@ function App({
1908
2172
  const offToolStart = events.on("tool.started", (e) => {
1909
2173
  dispatch({ type: "toolStarted", id: e.id, name: e.name });
1910
2174
  });
2175
+ const offToolProgress = events.on("tool.progress", (e) => {
2176
+ if (e.event.type !== "partial_output" || !e.event.text) return;
2177
+ dispatch({
2178
+ type: "toolStreamAppend",
2179
+ toolUseId: e.id,
2180
+ name: e.name,
2181
+ text: e.event.text
2182
+ });
2183
+ });
1911
2184
  const offTool = events.on("tool.executed", (e) => {
1912
2185
  dispatch({
1913
2186
  type: "addEntry",
@@ -1921,6 +2194,7 @@ function App({
1921
2194
  }
1922
2195
  });
1923
2196
  dispatch({ type: "toolEnded", name: e.name });
2197
+ dispatch({ type: "toolStreamClear", name: e.name });
1924
2198
  });
1925
2199
  const offRetry = events.on("provider.retry", (e) => {
1926
2200
  const secs = (e.delayMs / 1e3).toFixed(e.delayMs >= 1e3 ? 1 : 2);
@@ -1935,12 +2209,35 @@ function App({
1935
2209
  entry: { kind: "error", text: e.description }
1936
2210
  });
1937
2211
  });
2212
+ const offConfirmNeeded = events.on("tool.confirm_needed", (e) => {
2213
+ dispatch({
2214
+ type: "addEntry",
2215
+ entry: {
2216
+ kind: "confirm",
2217
+ toolName: e.tool.name,
2218
+ input: e.input,
2219
+ suggestedPattern: e.suggestedPattern
2220
+ }
2221
+ });
2222
+ dispatch({
2223
+ type: "confirmOpen",
2224
+ info: {
2225
+ toolUseId: e.toolUseId,
2226
+ toolName: e.tool.name,
2227
+ input: e.input,
2228
+ suggestedPattern: e.suggestedPattern,
2229
+ resolve: e.resolve
2230
+ }
2231
+ });
2232
+ });
1938
2233
  return () => {
1939
2234
  offDelta();
1940
2235
  offToolStart();
2236
+ offToolProgress();
1941
2237
  offTool();
1942
2238
  offRetry();
1943
2239
  offProvErr();
2240
+ offConfirmNeeded();
1944
2241
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
1945
2242
  };
1946
2243
  }, [events]);
@@ -1985,6 +2282,53 @@ function App({
1985
2282
  }, [state.interrupts, state.status, exit, onExit]);
1986
2283
  const handleKey = async (input, key) => {
1987
2284
  if (state.status === "aborting") return;
2285
+ if (state.modelPicker.open) {
2286
+ if (key.escape) {
2287
+ if (state.modelPicker.step === "model") {
2288
+ dispatch({ type: "modelPickerBack" });
2289
+ } else {
2290
+ dispatch({ type: "modelPickerClose" });
2291
+ }
2292
+ return;
2293
+ }
2294
+ if (key.upArrow) {
2295
+ dispatch({ type: "modelPickerMove", delta: -1 });
2296
+ return;
2297
+ }
2298
+ if (key.downArrow) {
2299
+ dispatch({ type: "modelPickerMove", delta: 1 });
2300
+ return;
2301
+ }
2302
+ if (key.return) {
2303
+ if (state.modelPicker.step === "provider") {
2304
+ const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
2305
+ if (!opt) return;
2306
+ dispatch({
2307
+ type: "modelPickerPickProvider",
2308
+ providerId: opt.id,
2309
+ models: opt.models
2310
+ });
2311
+ return;
2312
+ }
2313
+ const providerId = state.modelPicker.pickedProviderId;
2314
+ const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
2315
+ if (!providerId || !modelId) return;
2316
+ const err = switchProviderAndModel?.(providerId, modelId);
2317
+ if (err) {
2318
+ dispatch({ type: "modelPickerHint", text: err });
2319
+ return;
2320
+ }
2321
+ setLiveProvider(providerId);
2322
+ setLiveModel(modelId);
2323
+ dispatch({
2324
+ type: "addEntry",
2325
+ entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2326
+ });
2327
+ dispatch({ type: "modelPickerClose" });
2328
+ return;
2329
+ }
2330
+ return;
2331
+ }
1988
2332
  if (state.slashPicker.open) {
1989
2333
  if (key.escape) {
1990
2334
  dispatch({ type: "slashPickerClose" });
@@ -2118,25 +2462,26 @@ function App({
2118
2462
  }
2119
2463
  if (!input || key.ctrl || key.meta) return;
2120
2464
  let bracketedPaste = false;
2465
+ let cleanInput = input;
2121
2466
  if (input.includes("\x1B[200~") || input.includes("\x1B[201~")) {
2122
- input = input.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
2467
+ cleanInput = input.replace(/\x1b\[200~/g, "").replace(/\x1b\[201~/g, "");
2123
2468
  bracketedPaste = true;
2124
2469
  }
2125
- if (bracketedPaste || input.length > PASTE_THRESHOLD_CHARS || input.includes("\n")) {
2470
+ if (bracketedPaste || cleanInput.length > PASTE_THRESHOLD_CHARS || cleanInput.includes("\n")) {
2126
2471
  const builder = builderRef.current;
2127
2472
  if (!builder) return;
2128
- const ph = await builder.appendPaste(input);
2473
+ const ph = await builder.appendPaste(cleanInput);
2129
2474
  if (ph) {
2130
- const lineCount = input.split("\n").length;
2475
+ const lineCount = cleanInput.split("\n").length;
2131
2476
  dispatch({ type: "addPlaceholder", ph: `${ph} (${lineCount} lines)` });
2132
2477
  } else {
2133
- const next2 = state.buffer.slice(0, state.cursor) + input + state.buffer.slice(state.cursor);
2134
- dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor + input.length });
2478
+ const next2 = state.buffer.slice(0, state.cursor) + cleanInput + state.buffer.slice(state.cursor);
2479
+ dispatch({ type: "setBuffer", buffer: next2, cursor: state.cursor + cleanInput.length });
2135
2480
  }
2136
2481
  return;
2137
2482
  }
2138
- const next = state.buffer.slice(0, state.cursor) + input + state.buffer.slice(state.cursor);
2139
- dispatch({ type: "setBuffer", buffer: next, cursor: state.cursor + input.length });
2483
+ const next = state.buffer.slice(0, state.cursor) + cleanInput + state.buffer.slice(state.cursor);
2484
+ dispatch({ type: "setBuffer", buffer: next, cursor: state.cursor + cleanInput.length });
2140
2485
  };
2141
2486
  const runBlocks = async (blocks) => {
2142
2487
  const ctrl = new AbortController();
@@ -2149,7 +2494,7 @@ function App({
2149
2494
  const result = await agent.run(blocks, { signal: ctrl.signal });
2150
2495
  const streamed = streamingTextRef.current;
2151
2496
  const text = result.status === "done" && result.finalText ? result.finalText : streamed;
2152
- if (text && text.trim()) {
2497
+ if (text?.trim()) {
2153
2498
  dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
2154
2499
  }
2155
2500
  streamingTextRef.current = "";
@@ -2162,12 +2507,11 @@ function App({
2162
2507
  if (result.status === "aborted") {
2163
2508
  dispatch({ type: "addEntry", entry: { kind: "warn", text: "Aborted." } });
2164
2509
  } else if (result.status === "failed") {
2510
+ const err = result.error;
2511
+ const text2 = err ? `Failed [${err.severity}${err.recoverable ? ", recoverable" : ""}]: ${err.describe()}` : "Failed.";
2165
2512
  dispatch({
2166
2513
  type: "addEntry",
2167
- entry: {
2168
- kind: "error",
2169
- text: `Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
2170
- }
2514
+ entry: { kind: "error", text: text2 }
2171
2515
  });
2172
2516
  } else if (result.status === "max_iterations") {
2173
2517
  dispatch({
@@ -2215,6 +2559,10 @@ function App({
2215
2559
  if (res?.message) {
2216
2560
  dispatch({ type: "addEntry", entry: { kind: "info", text: res.message } });
2217
2561
  }
2562
+ const ctxModel = agent.ctx.model;
2563
+ if (ctxModel && ctxModel !== liveModel) setLiveModel(ctxModel);
2564
+ const ctxProviderId = agent.ctx.provider?.id;
2565
+ if (ctxProviderId && ctxProviderId !== liveProvider) setLiveProvider(ctxProviderId);
2218
2566
  if (res?.exit) {
2219
2567
  exit();
2220
2568
  onExit(0);
@@ -2253,7 +2601,7 @@ function App({
2253
2601
  return "";
2254
2602
  }, [state.buffer, state.status, state.picker.open]);
2255
2603
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2256
- /* @__PURE__ */ jsx(History, { entries: state.entries, streamingText: state.streamingText }),
2604
+ /* @__PURE__ */ jsx(History, { entries: state.entries, streamingText: state.streamingText, toolStream: state.toolStream }),
2257
2605
  /* @__PURE__ */ jsx(
2258
2606
  Input,
2259
2607
  {
@@ -2281,10 +2629,30 @@ function App({
2281
2629
  selected: state.slashPicker.selected
2282
2630
  }
2283
2631
  ) : null,
2632
+ state.modelPicker.open ? /* @__PURE__ */ jsx(
2633
+ ModelPicker,
2634
+ {
2635
+ step: state.modelPicker.step,
2636
+ providerOptions: state.modelPicker.providerOptions,
2637
+ modelOptions: state.modelPicker.modelOptions,
2638
+ selected: state.modelPicker.selected,
2639
+ pickedProviderId: state.modelPicker.pickedProviderId,
2640
+ hint: state.modelPicker.hint
2641
+ }
2642
+ ) : null,
2643
+ state.confirm ? /* @__PURE__ */ jsx(
2644
+ ConfirmPrompt,
2645
+ {
2646
+ toolName: state.confirm.toolName,
2647
+ input: state.confirm.input,
2648
+ suggestedPattern: state.confirm.suggestedPattern,
2649
+ onDecision: state.confirm.resolve
2650
+ }
2651
+ ) : null,
2284
2652
  /* @__PURE__ */ jsx(
2285
2653
  StatusBar,
2286
2654
  {
2287
- model,
2655
+ model: `${liveProvider}/${liveModel}`,
2288
2656
  state: state.status,
2289
2657
  tokenCounter,
2290
2658
  hint: renderRunningTools(state.runningTools) || state.hint,
@@ -2403,6 +2771,10 @@ async function runTui(opts) {
2403
2771
  yolo: opts.yolo,
2404
2772
  appVersion: opts.appVersion,
2405
2773
  provider: opts.provider,
2774
+ family: opts.family,
2775
+ keyTail: opts.keyTail,
2776
+ getPickableProviders: opts.getPickableProviders,
2777
+ switchProviderAndModel: opts.switchProviderAndModel,
2406
2778
  effectiveMaxContext: opts.effectiveMaxContext,
2407
2779
  onExit
2408
2780
  }),