@oxbit/cli 0.10.1 → 0.11.0

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.
@@ -1,17 +1,120 @@
1
1
  #!/usr/bin/env node
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import { renderToString, Box, Text, useAnimation, measureElement, useInput, useWindowSize, usePaste, Static, render } from "ink";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import { Box, Text, renderToString, useAnimation, measureElement, useInput, useWindowSize, usePaste, Static, render } from "ink";
4
4
  import { createContext, useContext, useMemo, useState, useRef, useEffect, useSyncExternalStore } from "react";
5
- import { M as MODES, w as walkFiles } from "../index.js";
6
- import { mkdirSync, appendFileSync, readFileSync, writeFileSync } from "node:fs";
7
- import { dirname } from "node:path";
5
+ import { c as contextLegend, a as contextGrid, f as formatTokens, b as contextNotes, d as formatPercent, M as MODES } from "../index.js";
6
+ import { memoryTuiConnection, fileHistory } from "./memory-Bdy8hvTl.js";
8
7
  import "node:util";
9
8
  import "node:readline";
10
9
  import "node:fs/promises";
10
+ import "node:path";
11
11
  import "node:crypto";
12
12
  import "node:os";
13
13
  import "node:child_process";
14
14
  import "node:http";
15
+ import "node:fs";
16
+ const THEMES = {
17
+ dark: {
18
+ name: "dark",
19
+ accent: "#8ab4f8",
20
+ muted: "gray",
21
+ user: "#c8d3f5",
22
+ success: "#9ece6a",
23
+ error: "#f7768e",
24
+ warning: "#e0af68",
25
+ link: "#7dcfff",
26
+ code: "#e0af68",
27
+ keyword: "#bb9af7",
28
+ string: "#9ece6a",
29
+ number: "#ff9e64",
30
+ added: "#9ece6a",
31
+ removed: "#f7768e",
32
+ addedBg: "#1d3325",
33
+ removedBg: "#3b1f27",
34
+ border: "gray",
35
+ modes: { ask: "gray", "accept-edits": "#bb9af7", "full-auto": "#e0af68" }
36
+ },
37
+ light: {
38
+ name: "light",
39
+ accent: "#1d4ed8",
40
+ muted: "gray",
41
+ user: "#1e293b",
42
+ success: "#15803d",
43
+ error: "#b91c1c",
44
+ warning: "#b45309",
45
+ link: "#0369a1",
46
+ code: "#9a3412",
47
+ keyword: "#7c3aed",
48
+ string: "#15803d",
49
+ number: "#c2410c",
50
+ added: "#166534",
51
+ removed: "#991b1b",
52
+ addedBg: "#dcfce7",
53
+ removedBg: "#fee2e2",
54
+ border: "gray",
55
+ modes: { ask: "gray", "accept-edits": "#7c3aed", "full-auto": "#b45309" }
56
+ }
57
+ };
58
+ function resolveTheme(choice = "auto", env = process.env) {
59
+ if (choice !== "auto") return THEMES[choice];
60
+ const background = Number(env.COLORFGBG?.split(";").at(-1));
61
+ return THEMES[Number.isInteger(background) && (background === 7 || background >= 9) ? "light" : "dark"];
62
+ }
63
+ const ThemeContext = createContext(THEMES.dark);
64
+ const useTheme = () => useContext(ThemeContext);
65
+ const COLUMNS = 20;
66
+ const GRID_WIDTH = COLUMNS * 2 - 1;
67
+ const glyph = (id) => id === "free" ? "□" : id === "buffer" ? "⊠" : "■";
68
+ const colors = (theme) => ({
69
+ system: theme.accent,
70
+ instructions: theme.keyword,
71
+ skills: theme.link,
72
+ context: theme.user,
73
+ tools: theme.number,
74
+ mcp: theme.removed,
75
+ messages: theme.success,
76
+ free: theme.muted,
77
+ buffer: theme.warning
78
+ });
79
+ function ContextView({ usage, width: width2 }) {
80
+ const theme = useTheme();
81
+ const known = colors(theme);
82
+ const spare = [theme.code, theme.error, theme.string, theme.added];
83
+ const legend = contextLegend(usage).map((row, index) => ({ ...row, color: known[row.id] ?? spare[index % spare.length] }));
84
+ const color = new Map(legend.map((row) => [row.id, row.color]));
85
+ const grid = contextGrid(usage, COLUMNS * 10);
86
+ const rows = Array.from({ length: grid.length / COLUMNS }, (_, row) => grid.slice(row * COLUMNS, (row + 1) * COLUMNS));
87
+ const label = Math.max(...legend.map((row) => row.label.length));
88
+ const counts = legend.map((row) => formatTokens(row.tokens));
89
+ const digits = Math.max(...counts.map((count) => count.length));
90
+ const side = rows.length > 0 && width2 >= GRID_WIDTH + 3 + label + digits + 12;
91
+ const { header, footer } = contextNotes(usage);
92
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, width: width2, children: [
93
+ /* @__PURE__ */ jsxs(Text, { children: [
94
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: "●" }),
95
+ " ",
96
+ /* @__PURE__ */ jsx(Text, { bold: true, children: header })
97
+ ] }),
98
+ /* @__PURE__ */ jsxs(Box, { flexDirection: side ? "row" : "column", marginTop: 1, paddingLeft: 2, children: [
99
+ rows.length ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: GRID_WIDTH, flexShrink: 0, marginRight: side ? 3 : 0, marginBottom: side ? 0 : 1, children: rows.map((cells2, index) => /* @__PURE__ */ jsx(Text, { children: cells2.map((id, at) => /* @__PURE__ */ jsxs(Text, { color: color.get(id), children: [
100
+ at ? " " : "",
101
+ glyph(id)
102
+ ] }, at)) }, index)) }) : null,
103
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: legend.map((row, index) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
104
+ /* @__PURE__ */ jsx(Text, { color: row.color, children: glyph(row.id) }),
105
+ " ",
106
+ row.label.padEnd(label),
107
+ " ",
108
+ counts[index].padStart(digits),
109
+ usage.window ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
110
+ " ",
111
+ formatPercent(row.tokens, usage.window).padStart(6)
112
+ ] }) : null
113
+ ] }, index)) })
114
+ ] }),
115
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, children: footer.map((line, index) => /* @__PURE__ */ jsx(Text, { color: theme.muted, wrap: "wrap", children: line }, index)) })
116
+ ] });
117
+ }
15
118
  const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^\s`]*)/;
16
119
  const HEADING = /^ {0,3}(#{1,6})(?:\s+(.*?))?\s*#*\s*$/;
17
120
  const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
@@ -271,55 +374,6 @@ function highlight(line, lang) {
271
374
  if (last < line.length) tokens.push({ text: line.slice(last) });
272
375
  return tokens;
273
376
  }
274
- const THEMES = {
275
- dark: {
276
- name: "dark",
277
- accent: "#8ab4f8",
278
- muted: "gray",
279
- user: "#c8d3f5",
280
- success: "#9ece6a",
281
- error: "#f7768e",
282
- warning: "#e0af68",
283
- link: "#7dcfff",
284
- code: "#e0af68",
285
- keyword: "#bb9af7",
286
- string: "#9ece6a",
287
- number: "#ff9e64",
288
- added: "#9ece6a",
289
- removed: "#f7768e",
290
- addedBg: "#1d3325",
291
- removedBg: "#3b1f27",
292
- border: "gray",
293
- modes: { ask: "gray", "accept-edits": "#bb9af7", "full-auto": "#e0af68" }
294
- },
295
- light: {
296
- name: "light",
297
- accent: "#1d4ed8",
298
- muted: "gray",
299
- user: "#1e293b",
300
- success: "#15803d",
301
- error: "#b91c1c",
302
- warning: "#b45309",
303
- link: "#0369a1",
304
- code: "#9a3412",
305
- keyword: "#7c3aed",
306
- string: "#15803d",
307
- number: "#c2410c",
308
- added: "#166534",
309
- removed: "#991b1b",
310
- addedBg: "#dcfce7",
311
- removedBg: "#fee2e2",
312
- border: "gray",
313
- modes: { ask: "gray", "accept-edits": "#7c3aed", "full-auto": "#b45309" }
314
- }
315
- };
316
- function resolveTheme(choice = "auto", env = process.env) {
317
- if (choice !== "auto") return THEMES[choice];
318
- const background = Number(env.COLORFGBG?.split(";").at(-1));
319
- return THEMES[Number.isInteger(background) && (background === 7 || background >= 9) ? "light" : "dark"];
320
- }
321
- const ThemeContext = createContext(THEMES.dark);
322
- const useTheme = () => useContext(ThemeContext);
323
377
  const segmenter = new Intl.Segmenter();
324
378
  const ANSI = /\x1b\[[0-9;:?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
325
379
  const ZERO = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}]+$/u;
@@ -789,6 +843,8 @@ function ItemView({ item, live = false, context }) {
789
843
  /* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: theme.error, children: "✗" }) }),
790
844
  /* @__PURE__ */ jsx(Text, { color: theme.error, wrap: "wrap", children: item.text })
791
845
  ] }) : /* @__PURE__ */ jsx(Box, { width: width2, children: /* @__PURE__ */ jsx(Gutter, { children: /* @__PURE__ */ jsx(Text, { color: item.tone === "warn" ? theme.warning : theme.muted, wrap: "wrap", children: item.text }) }) });
846
+ case "context":
847
+ return /* @__PURE__ */ jsx(ContextView, { usage: item.usage, width: width2 });
792
848
  case "divider": {
793
849
  const label = ` ${item.text} `;
794
850
  return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.muted, wrap: "truncate-end", children: [
@@ -801,7 +857,7 @@ function ItemView({ item, live = false, context }) {
801
857
  }
802
858
  const emptyTranscript = () => ({ flushed: [{ id: 0, type: "banner" }], live: [], next: 1 });
803
859
  const STREAMING = /* @__PURE__ */ new Set(["assistant", "thought", "plan"]);
804
- const FINAL = /* @__PURE__ */ new Set(["banner", "user", "notice", "divider"]);
860
+ const FINAL = /* @__PURE__ */ new Set(["banner", "user", "notice", "divider", "context"]);
805
861
  const isDone = (item) => FINAL.has(item.type) || !!item.done;
806
862
  const textOf = (content) => {
807
863
  const block = content;
@@ -863,7 +919,7 @@ function tool(state, update) {
863
919
  function applyUpdate(state, update) {
864
920
  switch (update.sessionUpdate) {
865
921
  case "agent_message_chunk":
866
- return stream(state, "assistant", textOf(update.content));
922
+ return update._meta?.oxbit?.context ? append(state, { type: "context", usage: update._meta.oxbit.context }) : stream(state, "assistant", textOf(update.content));
867
923
  case "agent_thought_chunk":
868
924
  return stream(state, "thought", textOf(update.content));
869
925
  case "user_message_chunk": {
@@ -1217,19 +1273,33 @@ class SessionController {
1217
1273
  if (this.state.running) return this.notice("Wait for the agent to finish, or press Esc to stop it", "warn");
1218
1274
  this.rules.clear();
1219
1275
  if (label) this.transcript((transcript) => append(transcript, { type: "divider", text: label }));
1276
+ await this.load(sessionId, true);
1277
+ }
1278
+ /** Loads the open session again after the agent was rebuilt; its replayed history stays out of the transcript. */
1279
+ async reload() {
1280
+ if (this.state.running) return this.notice("Wait for the agent to finish, or press Esc to stop it", "warn");
1281
+ await this.load(this.state.status.sessionId, false);
1282
+ }
1283
+ async load(sessionId, replay) {
1220
1284
  this.early = [];
1221
1285
  let session;
1222
1286
  try {
1223
1287
  session = sessionId ? await this.connection.loadSession(sessionId, this.cwd) : await this.connection.newSession(this.cwd);
1224
1288
  } catch (error) {
1225
- this.notice(message(error), "error");
1289
+ if (!replay && /Unknown ACP session/.test(message(error))) session = await this.connection.newSession(this.cwd).catch(() => void 0);
1290
+ if (!session) this.notice(message(error), "error");
1226
1291
  }
1227
1292
  const early = this.early;
1228
1293
  this.early = void 0;
1229
1294
  if (!session) return;
1230
- this.set({ status: fromSession(session), info: this.connection.info() });
1231
- for (const [id, update] of early) this.onUpdate(id, update);
1232
- this.transcript(endTurn);
1295
+ const { title, usage, goal, loop, commands } = this.state.status;
1296
+ const kept = replay || session.sessionId !== sessionId ? {} : { title, usage, goal, loop, commands };
1297
+ this.set({ status: { ...fromSession(session), ...kept }, info: this.connection.info() });
1298
+ for (const [id, update] of early) {
1299
+ if (replay) this.onUpdate(id, update);
1300
+ else if (id === session.sessionId) this.set({ status: applyStatus(this.state.status, update) });
1301
+ }
1302
+ if (replay) this.transcript(endTurn);
1233
1303
  }
1234
1304
  async send(text) {
1235
1305
  const sessionId = this.state.status.sessionId;
@@ -1268,8 +1338,10 @@ class SessionController {
1268
1338
  this.set({ permission: this.permissions[0] });
1269
1339
  }
1270
1340
  cycleMode() {
1271
- const ids = MODES.map((mode2) => mode2.id);
1272
- const mode = ids[(ids.indexOf(this.state.status.mode) + 1) % ids.length];
1341
+ const ids = MODES.map((mode) => mode.id);
1342
+ this.setMode(ids[(ids.indexOf(this.state.status.mode) + 1) % ids.length]);
1343
+ }
1344
+ setMode(mode) {
1273
1345
  const sessionId = this.state.status.sessionId;
1274
1346
  this.set({ status: { ...this.state.status, mode } });
1275
1347
  if (sessionId) this.connection.setMode(sessionId, mode).catch((error) => this.notice(message(error), "error"));
@@ -1332,10 +1404,187 @@ class SessionController {
1332
1404
  return item?.tool;
1333
1405
  }
1334
1406
  }
1407
+ const PANELS = ["config", "skills", "hooks", "mcp", "instructions", "providers"];
1408
+ const isPanel = (name) => PANELS.includes(name);
1409
+ function panelRows(panel) {
1410
+ const rows = panel?.rows ?? [];
1411
+ return [...new Set(rows.map((row) => row.section))].flatMap((section) => rows.filter((row) => row.section === section));
1412
+ }
1413
+ class PanelController extends SessionController {
1414
+ panelAbort;
1415
+ panelOverlay() {
1416
+ const overlay = this.state.overlay;
1417
+ return overlay?.kind === "panel" ? overlay : void 0;
1418
+ }
1419
+ patchPanel(patch) {
1420
+ const overlay = this.panelOverlay();
1421
+ if (overlay) this.set({ overlay: { ...overlay, ...patch } });
1422
+ }
1423
+ selectedRow() {
1424
+ const overlay = this.panelOverlay();
1425
+ return overlay && panelRows(overlay.panel)[overlay.index];
1426
+ }
1427
+ openPanel(id) {
1428
+ this.closeOverlay();
1429
+ this.set({ overlay: { kind: "panel", id, index: 0, loading: true } });
1430
+ void this.loadPanel();
1431
+ }
1432
+ /** Closes any overlay and cancels the panel request in flight. */
1433
+ closeOverlay() {
1434
+ this.panelAbort?.abort();
1435
+ this.panelAbort = void 0;
1436
+ if (this.state.overlay) this.set({ overlay: void 0 });
1437
+ }
1438
+ /** Runs one SettingsManager call; undefined when it failed, was cancelled or another call replaced it. */
1439
+ async call(run2) {
1440
+ const overlay = this.panelOverlay();
1441
+ const settings = this.connection.settings;
1442
+ if (!overlay || !settings) return void 0;
1443
+ this.panelAbort?.abort();
1444
+ const abort = this.panelAbort = new AbortController();
1445
+ const current = () => this.panelAbort === abort;
1446
+ const context = {
1447
+ cwd: this.cwd,
1448
+ sessionId: this.state.status.sessionId,
1449
+ signal: abort.signal,
1450
+ progress: (text) => current() && this.patchPanel({ progress: text })
1451
+ };
1452
+ try {
1453
+ const result = await run2(settings, overlay.id, context);
1454
+ return current() ? result : void 0;
1455
+ } catch (error) {
1456
+ if (current()) this.patchPanel({ error: message(error), loading: false, busy: false, progress: void 0 });
1457
+ return void 0;
1458
+ } finally {
1459
+ if (current()) this.panelAbort = void 0;
1460
+ }
1461
+ }
1462
+ async loadPanel() {
1463
+ const panel = await this.call((settings, id, context) => settings.panel(id, context));
1464
+ const overlay = this.panelOverlay();
1465
+ if (!panel || !overlay) return;
1466
+ const rows = panelRows(panel);
1467
+ const selected2 = rows.findIndex((row) => row.id === panelRows(overlay.panel)[overlay.index]?.id);
1468
+ const index = selected2 >= 0 ? selected2 : Math.min(overlay.index, Math.max(0, rows.length - 1));
1469
+ this.patchPanel({ panel, index, loading: false, busy: false, progress: void 0 });
1470
+ }
1471
+ async change(run2) {
1472
+ const overlay = this.panelOverlay();
1473
+ if (!overlay?.panel || overlay.busy) return;
1474
+ this.patchPanel({ busy: true, progress: void 0, error: void 0, menu: void 0, input: void 0 });
1475
+ const change = await this.call(run2);
1476
+ if (!change) return;
1477
+ if (change.reloaded) await this.reload();
1478
+ if (change.mode) this.setMode(change.mode);
1479
+ if (change.draft !== void 0) {
1480
+ this.closeOverlay();
1481
+ this.set({ draft: draftOf(change.draft), completion: void 0 });
1482
+ } else await this.loadPanel();
1483
+ if (change.message) this.flash(change.message);
1484
+ }
1485
+ movePanel(step) {
1486
+ const overlay = this.panelOverlay();
1487
+ const row = this.selectedRow();
1488
+ if (!overlay || overlay.busy) return;
1489
+ if (overlay.menu !== void 0 && row?.actions) {
1490
+ const count2 = row.actions.length;
1491
+ return this.patchPanel({ menu: (overlay.menu + step + count2) % count2 });
1492
+ }
1493
+ const count = panelRows(overlay.panel).length;
1494
+ if (count) this.patchPanel({ index: (overlay.index + step + count) % count });
1495
+ }
1496
+ togglePanelRow() {
1497
+ const row = this.selectedRow();
1498
+ if (row?.enabled !== void 0) void this.change((settings, id, context) => settings.toggle(id, row.id, !row.enabled, context));
1499
+ }
1500
+ cyclePanelChoice(step) {
1501
+ const row = this.selectedRow();
1502
+ const options = row?.choice?.options ?? [];
1503
+ if (!row?.choice || !options.length) return;
1504
+ const at = options.findIndex((option2) => option2.value === row.choice.value);
1505
+ const value = options[(at + step + options.length) % options.length].value;
1506
+ void this.change((settings, id, context) => settings.choose(id, row.id, value, context));
1507
+ }
1508
+ /** Enter: runs the row's only action, lists several, or runs the one picked from that list. */
1509
+ enterPanel() {
1510
+ const overlay = this.panelOverlay();
1511
+ const row = this.selectedRow();
1512
+ if (!overlay || !row || overlay.busy) return;
1513
+ const actions = row.actions ?? [];
1514
+ if (overlay.menu !== void 0) return this.startAction(row, actions[overlay.menu]);
1515
+ if (actions.length === 1) return this.startAction(row, actions[0]);
1516
+ if (actions.length) return this.patchPanel({ menu: 0 });
1517
+ if (row.enabled !== void 0) return this.togglePanelRow();
1518
+ this.cyclePanelChoice(1);
1519
+ }
1520
+ startAction(row, action) {
1521
+ if (!action) return;
1522
+ if (action.reloads && this.state.running) {
1523
+ this.patchPanel({ menu: void 0 });
1524
+ return this.flash("Wait for the turn to finish");
1525
+ }
1526
+ const first = action.inputs?.[0];
1527
+ if (first) return this.patchPanel({ menu: void 0, input: { rowId: row.id, action, values: [], draft: draftOf(first.value ?? "") } });
1528
+ void this.change((settings, id, context) => settings.act(id, row.id, action.id, [], context));
1529
+ }
1530
+ editPanelInput(draft) {
1531
+ const input = this.panelOverlay()?.input;
1532
+ if (input) this.patchPanel({ input: { ...input, draft } });
1533
+ }
1534
+ pastePanelInput(text) {
1535
+ const input = this.panelOverlay()?.input;
1536
+ if (input) this.editPanelInput(insert(input.draft, text.replace(/\n+$/, "").replace(/\n/g, " ")));
1537
+ }
1538
+ /** Enter in the input field: asks the next input, or runs the action with every answer. */
1539
+ submitPanelInput() {
1540
+ const input = this.panelOverlay()?.input;
1541
+ if (!input) return;
1542
+ const values = [...input.values, input.draft.text];
1543
+ const next = input.action.inputs?.[values.length];
1544
+ if (next) return this.patchPanel({ input: { ...input, values, draft: draftOf(next.value ?? "") } });
1545
+ if (input.action.reloads && this.state.running) {
1546
+ this.patchPanel({ input: void 0 });
1547
+ return this.flash("Wait for the turn to finish");
1548
+ }
1549
+ void this.change((settings, id, context) => settings.act(id, input.rowId, input.action.id, values, context));
1550
+ }
1551
+ /** Typed `/providers use <id>`: the agent command cannot rebuild the agent mid-turn, so the panel action switches it here. */
1552
+ async useProvider(id) {
1553
+ const settings = this.connection.settings;
1554
+ if (!settings) return;
1555
+ if (this.state.running) return this.flash("Wait for the turn to finish");
1556
+ const context = { cwd: this.cwd, sessionId: this.state.status.sessionId, signal: new AbortController().signal, progress: () => {
1557
+ } };
1558
+ try {
1559
+ const change = await settings.act("providers", id, "use", [], context);
1560
+ if (change.reloaded) await this.reload();
1561
+ if (change.message) this.flash(change.message);
1562
+ } catch (error) {
1563
+ this.notice(message(error), "error");
1564
+ }
1565
+ }
1566
+ /** Esc: cancels the running call, then leaves the input or action list, then closes the panel. */
1567
+ escapePanel() {
1568
+ const overlay = this.panelOverlay();
1569
+ if (overlay?.busy) {
1570
+ this.panelAbort?.abort();
1571
+ this.panelAbort = void 0;
1572
+ return this.patchPanel({ busy: false, progress: void 0 });
1573
+ }
1574
+ if (overlay?.input || overlay?.menu !== void 0) return this.patchPanel({ input: void 0, menu: void 0 });
1575
+ this.closeOverlay();
1576
+ }
1577
+ }
1335
1578
  const LOCAL_COMMANDS = [
1336
1579
  { name: "model", description: "Choose the model and reasoning effort", hint: "[model]" },
1337
1580
  { name: "new", description: "Start a new session" },
1338
1581
  { name: "resume", description: "Resume a saved session" },
1582
+ { name: "config", description: "Settings: permission mode, auto-compact, hooks and MCP trust" },
1583
+ { name: "skills", description: "Turn skills on or off, or start a prompt with one" },
1584
+ { name: "hooks", description: "Turn hook sources on or off" },
1585
+ { name: "mcp", description: "Add, remove, trust and sign in to MCP servers" },
1586
+ { name: "instructions", description: "Choose which AGENTS.md and CLAUDE.md files load" },
1587
+ { name: "providers", description: "Sign in to providers and set API keys" },
1339
1588
  { name: "help", description: "Show keys and commands" },
1340
1589
  { name: "exit", description: "Quit" }
1341
1590
  ];
@@ -1348,7 +1597,7 @@ function overlayItems(state) {
1348
1597
  return rank(overlay.sessions, (session) => session.title ?? session.sessionId, overlay.filter, 200).map((session) => ({ value: session.sessionId, label: session.title || session.sessionId, detail: session.updatedAt?.slice(0, 16).replace("T", " ") }));
1349
1598
  return [];
1350
1599
  }
1351
- class Controller extends SessionController {
1600
+ class Controller extends PanelController {
1352
1601
  constructor(connection, options) {
1353
1602
  super(connection, options.cwd);
1354
1603
  this.options = options;
@@ -1362,9 +1611,14 @@ class Controller extends SessionController {
1362
1611
  fileTimer;
1363
1612
  files = /* @__PURE__ */ new Map();
1364
1613
  lastInterrupt = 0;
1614
+ /** Server commands, then local ones; a panel command shows the server command's hint, since arguments go to the server. */
1365
1615
  commands() {
1366
- const local = LOCAL_COMMANDS.filter((command) => command.name !== "resume" || this.connection.listSessions);
1367
- return [...this.state.status.commands.filter((command) => !local.some((item) => item.name === command.name)), ...local];
1616
+ const server = this.state.status.commands;
1617
+ const local = LOCAL_COMMANDS.filter((command) => command.name === "resume" ? this.connection.listSessions : !isPanel(command.name) || this.connection.settings).map((command) => {
1618
+ const hint = isPanel(command.name) ? server.find((item) => item.name === command.name)?.hint : void 0;
1619
+ return hint ? { ...command, hint: hint.startsWith("[") ? hint : `[${hint}]` } : command;
1620
+ });
1621
+ return [...server.filter((command) => !local.some((item) => item.name === command.name)), ...local];
1368
1622
  }
1369
1623
  submit() {
1370
1624
  const text = this.state.draft.text.replace(/\s+$/, "");
@@ -1396,6 +1650,21 @@ class Controller extends SessionController {
1396
1650
  if (args) void this.open(args, "Resumed session");
1397
1651
  else void this.openSessions();
1398
1652
  return true;
1653
+ case "config":
1654
+ case "skills":
1655
+ case "hooks":
1656
+ case "mcp":
1657
+ case "instructions":
1658
+ case "providers": {
1659
+ const use = name === "providers" && this.connection.settings ? /^use\s+(\S+)$/i.exec(args) : null;
1660
+ if (use) {
1661
+ void this.useProvider(use[1]);
1662
+ return true;
1663
+ }
1664
+ if (!this.connection.settings || args) return false;
1665
+ this.openPanel(name);
1666
+ return true;
1667
+ }
1399
1668
  case "help":
1400
1669
  this.set({ overlay: { kind: "help" } });
1401
1670
  return true;
@@ -1417,6 +1686,7 @@ class Controller extends SessionController {
1417
1686
  const clean = text.replace(/\r\n?/g, "\n");
1418
1687
  const overlay = this.state.overlay;
1419
1688
  if (picker(overlay)) return this.filter(picker(overlay).filter + clean.replace(/\n/g, " "));
1689
+ if (overlay?.kind === "panel") return this.pastePanelInput(clean);
1420
1690
  if (overlay) return;
1421
1691
  this.setDraft(insert(this.state.draft, clean));
1422
1692
  }
@@ -1459,7 +1729,7 @@ class Controller extends SessionController {
1459
1729
  if (!completion) return;
1460
1730
  const item = completion.items[completion.index];
1461
1731
  const command = completion.kind === "command" ? this.commands().find((entry) => `/${entry.name}` === item.value) : void 0;
1462
- if (run2 && command && !command.hint) {
1732
+ if (run2 && command && (!command.hint || isPanel(command.name) && this.connection.settings)) {
1463
1733
  this.set({ draft: draftOf(item.value) });
1464
1734
  return this.submit();
1465
1735
  }
@@ -1472,7 +1742,9 @@ class Controller extends SessionController {
1472
1742
  const index = this.historyIndex < 0 ? this.history.length - 1 : this.historyIndex + step;
1473
1743
  if (index < 0) return true;
1474
1744
  this.historyIndex = index >= this.history.length ? -1 : index;
1475
- this.setDraft(this.historyIndex < 0 ? this.stash : draftOf(this.history[index]));
1745
+ const draft = this.historyIndex < 0 ? this.stash : draftOf(this.history[index]);
1746
+ this.dismissed = draft.text;
1747
+ this.setDraft(draft);
1476
1748
  return true;
1477
1749
  }
1478
1750
  filter(text) {
@@ -1506,7 +1778,7 @@ class Controller extends SessionController {
1506
1778
  if (now - this.lastInterrupt < 2e3) return this.exit();
1507
1779
  this.lastInterrupt = now;
1508
1780
  if (this.state.running || this.state.permission) this.cancel();
1509
- else if (this.state.overlay) this.set({ overlay: void 0 });
1781
+ else if (this.state.overlay) this.closeOverlay();
1510
1782
  else if (this.state.draft.text) this.setDraft(emptyDraft);
1511
1783
  this.flash("Press Ctrl+C again to exit");
1512
1784
  }
@@ -1611,7 +1883,7 @@ function OverlayView({ state, width: width2 }) {
1611
1883
  ] })
1612
1884
  ] });
1613
1885
  }
1614
- if (overlay.kind === "details") return null;
1886
+ if (overlay.kind === "details" || overlay.kind === "panel") return null;
1615
1887
  const items = overlayItems(state);
1616
1888
  const reasoning = overlay.kind === "models" ? state.status.reasoning : void 0;
1617
1889
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: width2, marginTop: 1, children: [
@@ -1636,6 +1908,20 @@ function OverlayView({ state, width: width2 }) {
1636
1908
  ] })
1637
1909
  ] });
1638
1910
  }
1911
+ function lineEdit(draft, input, key) {
1912
+ if (key.leftArrow) return key.meta || key.ctrl ? wordLeft(draft) : left(draft);
1913
+ if (key.rightArrow) return key.meta || key.ctrl ? wordRight(draft) : right(draft);
1914
+ if (key.home || key.ctrl && input === "a") return home(draft);
1915
+ if (key.end || key.ctrl && input === "e") return end(draft);
1916
+ if (key.backspace) return key.meta || key.ctrl ? deleteWordLeft(draft) : backspace(draft);
1917
+ if (key.delete) return deleteForward(draft);
1918
+ if (key.ctrl && input === "w") return deleteWordLeft(draft);
1919
+ if (key.ctrl && input === "u") return killLineLeft(draft);
1920
+ if (key.ctrl && input === "k") return killLineRight(draft);
1921
+ if (key.meta && input === "b") return wordLeft(draft);
1922
+ if (key.meta && input === "f") return wordRight(draft);
1923
+ if (input && !key.ctrl && !key.meta && !key.tab) return insert(draft, input.replace(/\r\n?/g, "\n"));
1924
+ }
1639
1925
  function permissionKey(controller, input, key) {
1640
1926
  const pending = controller.getState().permission;
1641
1927
  const choose = (index) => controller.decide(DECISIONS[index]);
@@ -1648,8 +1934,23 @@ function permissionKey(controller, input, key) {
1648
1934
  const shortcut = { 1: 0, 2: 1, 3: 2, y: 0, a: 1, n: 2 }[input.toLowerCase()];
1649
1935
  if (shortcut !== void 0) choose(shortcut);
1650
1936
  }
1937
+ function panelKey(controller, input, key, overlay) {
1938
+ if (overlay.input) {
1939
+ if (key.escape) return controller.escapePanel();
1940
+ if (key.return) return controller.submitPanelInput();
1941
+ const next = lineEdit(overlay.input.draft, input.replace(/[\r\n]/g, ""), key);
1942
+ return next && controller.editPanelInput(next);
1943
+ }
1944
+ if (key.escape) return controller.escapePanel();
1945
+ if (key.upArrow || key.downArrow) return controller.movePanel(key.upArrow ? -1 : 1);
1946
+ if (key.return) return controller.enterPanel();
1947
+ if (overlay.menu !== void 0) return;
1948
+ if (input === " ") return controller.togglePanelRow();
1949
+ if (key.leftArrow || key.rightArrow) controller.cyclePanelChoice(key.leftArrow ? -1 : 1);
1950
+ }
1651
1951
  function overlayKey(controller, input, key) {
1652
1952
  const overlay = controller.getState().overlay;
1953
+ if (overlay.kind === "panel") return panelKey(controller, input, key, overlay);
1653
1954
  if (overlay.kind === "details") {
1654
1955
  if (key.escape || key.return || input === "q" || key.ctrl && input === "o") controller.set({ overlay: void 0 });
1655
1956
  else if (key.leftArrow || key.rightArrow) controller.showDetails(key.leftArrow ? -1 : 1);
@@ -1699,18 +2000,130 @@ function handleKey(controller, input, key) {
1699
2000
  if (moved) return edit(moved);
1700
2001
  return void controller.recall(step);
1701
2002
  }
1702
- if (key.leftArrow) return edit(key.meta || key.ctrl ? wordLeft(draft) : left(draft));
1703
- if (key.rightArrow) return edit(key.meta || key.ctrl ? wordRight(draft) : right(draft));
1704
- if (key.home || key.ctrl && input === "a") return edit(home(draft));
1705
- if (key.end || key.ctrl && input === "e") return edit(end(draft));
1706
- if (key.backspace) return edit(key.meta || key.ctrl ? deleteWordLeft(draft) : backspace(draft));
1707
- if (key.delete) return edit(deleteForward(draft));
1708
- if (key.ctrl && input === "w") return edit(deleteWordLeft(draft));
1709
- if (key.ctrl && input === "u") return edit(killLineLeft(draft));
1710
- if (key.ctrl && input === "k") return edit(killLineRight(draft));
1711
- if (key.meta && input === "b") return edit(wordLeft(draft));
1712
- if (key.meta && input === "f") return edit(wordRight(draft));
1713
- if (input && !key.ctrl && !key.meta && !key.tab) edit(insert(draft, input.replace(/\r\n?/g, "\n")));
2003
+ const next = lineEdit(draft, input, key);
2004
+ if (next) edit(next);
2005
+ }
2006
+ const SIZE = 12;
2007
+ function hints(overlay, row) {
2008
+ if (overlay.input) return "enter submit · esc cancel";
2009
+ if (overlay.busy) return "esc cancel";
2010
+ if (overlay.menu !== void 0) return "↑↓ select · enter run · esc back";
2011
+ const actions = row?.actions ?? [];
2012
+ return [
2013
+ "↑↓ move",
2014
+ row?.enabled !== void 0 ? "space toggle" : "",
2015
+ row?.choice ? "←→ change" : "",
2016
+ actions.length === 1 ? `enter ${actions[0].label.toLowerCase()}` : actions.length ? "enter actions" : "",
2017
+ "esc close"
2018
+ ].filter(Boolean).join(" · ");
2019
+ }
2020
+ function Busy() {
2021
+ const theme = useTheme();
2022
+ const { frame } = useAnimation({ interval: 100 });
2023
+ return /* @__PURE__ */ jsxs(Text, { color: theme.accent, children: [
2024
+ SPINNER[frame % SPINNER.length],
2025
+ " Working…"
2026
+ ] });
2027
+ }
2028
+ function RowLine({ row, selected: selected2 }) {
2029
+ const theme = useTheme();
2030
+ const value = row.choice && (row.choice.options.find((option2) => option2.value === row.choice.value)?.name ?? row.choice.value);
2031
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
2032
+ /* @__PURE__ */ jsxs(Text, { color: selected2 ? theme.accent : void 0, bold: selected2, children: [
2033
+ selected2 ? "❯ " : " ",
2034
+ row.enabled === void 0 ? "" : row.enabled ? "[x] " : "[ ] ",
2035
+ row.label
2036
+ ] }),
2037
+ row.choice ? /* @__PURE__ */ jsxs(Text, { children: [
2038
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: " ◂ " }),
2039
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: value }),
2040
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, children: " ▸" })
2041
+ ] }) : null,
2042
+ row.detail ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
2043
+ " ",
2044
+ row.detail
2045
+ ] }) : null
2046
+ ] });
2047
+ }
2048
+ function Rows({ overlay }) {
2049
+ const theme = useTheme();
2050
+ const lines = [];
2051
+ panelRows(overlay.panel).forEach((row, at, rows2) => {
2052
+ if (row.section && row.section !== rows2[at - 1]?.section) lines.push({ heading: row.section });
2053
+ lines.push({ row, at });
2054
+ });
2055
+ if (!lines.length) return /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Nothing to show" });
2056
+ const { start, rows } = windowOf(lines, lines.findIndex((line) => "row" in line && line.at === overlay.index), SIZE);
2057
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2058
+ start > 0 ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
2059
+ " ↑ ",
2060
+ start,
2061
+ " more"
2062
+ ] }) : null,
2063
+ rows.map((line, at) => "heading" in line ? /* @__PURE__ */ jsx(Text, { bold: true, color: theme.muted, wrap: "truncate-end", children: line.heading }, `h${start + at}`) : /* @__PURE__ */ jsx(RowLine, { row: line.row, selected: line.at === overlay.index }, line.row.id)),
2064
+ start + rows.length < lines.length ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
2065
+ " ↓ ",
2066
+ lines.length - start - rows.length,
2067
+ " more"
2068
+ ] }) : null
2069
+ ] });
2070
+ }
2071
+ function Menu({ row, index }) {
2072
+ const theme = useTheme();
2073
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2074
+ /* @__PURE__ */ jsx(Text, { bold: true, wrap: "truncate-end", children: row.label }),
2075
+ (row.actions ?? []).map((action, at) => /* @__PURE__ */ jsxs(Text, { color: at === index ? theme.accent : void 0, bold: at === index, wrap: "truncate-end", children: [
2076
+ at === index ? "❯ " : " ",
2077
+ action.label
2078
+ ] }, action.id))
2079
+ ] });
2080
+ }
2081
+ function Field({ input }) {
2082
+ const theme = useTheme();
2083
+ const inputs = input.action.inputs ?? [];
2084
+ const field = inputs[input.values.length];
2085
+ const mask = (text = "") => field?.secret ? "•".repeat(graphemes(text).length) : text;
2086
+ const line = cursorLines(input.draft)[0];
2087
+ const under = input.draft.cursor < input.draft.text.length ? mask(line.under) : " ";
2088
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2089
+ /* @__PURE__ */ jsxs(Text, { bold: true, wrap: "truncate-end", children: [
2090
+ input.action.label,
2091
+ inputs.length > 1 ? /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
2092
+ " (",
2093
+ input.values.length + 1,
2094
+ "/",
2095
+ inputs.length,
2096
+ ")"
2097
+ ] }) : null
2098
+ ] }),
2099
+ /* @__PURE__ */ jsxs(Text, { wrap: "wrap", children: [
2100
+ /* @__PURE__ */ jsxs(Text, { color: theme.accent, children: [
2101
+ field?.label,
2102
+ ": "
2103
+ ] }),
2104
+ mask(line.before),
2105
+ /* @__PURE__ */ jsx(Text, { inverse: true, children: under }),
2106
+ mask(line.after),
2107
+ input.draft.text ? null : /* @__PURE__ */ jsx(Text, { color: theme.muted, children: field?.placeholder ?? "" })
2108
+ ] })
2109
+ ] });
2110
+ }
2111
+ function PanelView({ overlay, width: width2 }) {
2112
+ const theme = useTheme();
2113
+ const { panel } = overlay;
2114
+ const row = panelRows(panel)[overlay.index];
2115
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
2116
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, width: width2, marginTop: 1, children: [
2117
+ /* @__PURE__ */ jsx(Text, { bold: true, wrap: "truncate-end", children: panel?.title ?? `/${overlay.id}` }),
2118
+ overlay.loading && !panel ? /* @__PURE__ */ jsx(Text, { color: theme.muted, children: "Loading…" }) : null,
2119
+ !panel ? null : overlay.input ? /* @__PURE__ */ jsx(Field, { input: overlay.input }) : overlay.menu !== void 0 && row ? /* @__PURE__ */ jsx(Menu, { row, index: overlay.menu }) : /* @__PURE__ */ jsx(Rows, { overlay }),
2120
+ overlay.busy ? /* @__PURE__ */ jsx(Busy, {}) : null,
2121
+ overlay.error ? /* @__PURE__ */ jsx(Text, { color: theme.error, wrap: "wrap", children: overlay.error }) : null,
2122
+ panel?.footer ? /* @__PURE__ */ jsx(Text, { color: theme.muted, wrap: "wrap", children: panel.footer }) : null,
2123
+ /* @__PURE__ */ jsx(Text, { color: theme.muted, wrap: "truncate-end", children: hints(overlay, row) })
2124
+ ] }),
2125
+ overlay.progress ? /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: overlay.progress }) : null
2126
+ ] });
1714
2127
  }
1715
2128
  const MODE_LABELS = { ask: "◇ ask", "accept-edits": "⏵⏵ accept edits", "full-auto": "⏵⏵⏵ full auto" };
1716
2129
  function PromptBox({ state, width: width2 }) {
@@ -1814,7 +2227,7 @@ function Screen({ controller, version }) {
1814
2227
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
1815
2228
  /* @__PURE__ */ jsx(Static, { items: state.transcript.flushed, children: (item) => /* @__PURE__ */ jsx(Box, { width: width2, flexDirection: "column", children: /* @__PURE__ */ jsx(ItemView, { item, context }) }, item.id) }),
1816
2229
  state.transcript.live.map((item) => /* @__PURE__ */ jsx(ItemView, { item, live: true, context }, item.id)),
1817
- state.permission ? /* @__PURE__ */ jsx(PermissionDialog, { pending: state.permission, controller, width: width2, cwd: state.cwd }) : state.overlay?.kind === "details" ? /* @__PURE__ */ jsx(Details, { id: state.overlay.id, transcript: state.transcript, context }, state.overlay.id) : state.overlay ? /* @__PURE__ */ jsx(OverlayView, { state, width: width2 }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2230
+ state.permission ? /* @__PURE__ */ jsx(PermissionDialog, { pending: state.permission, controller, width: width2, cwd: state.cwd }) : state.overlay?.kind === "details" ? /* @__PURE__ */ jsx(Details, { id: state.overlay.id, transcript: state.transcript, context }, state.overlay.id) : state.overlay?.kind === "panel" ? /* @__PURE__ */ jsx(PanelView, { overlay: state.overlay, width: width2 }) : state.overlay ? /* @__PURE__ */ jsx(OverlayView, { state, width: width2 }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1818
2231
  state.running ? /* @__PURE__ */ jsx(Working, { state }) : null,
1819
2232
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1820
2233
  state.queued.map((text, index) => /* @__PURE__ */ jsxs(Text, { color: theme.muted, wrap: "truncate-end", children: [
@@ -1831,78 +2244,6 @@ function Screen({ controller, version }) {
1831
2244
  function App({ controller, theme, version }) {
1832
2245
  return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(Screen, { controller, version }) });
1833
2246
  }
1834
- const FILE_LIST_TTL_MS = 1e4;
1835
- const FILE_MATCHES = 2e3;
1836
- const HISTORY_LIMIT = 500;
1837
- const subsequence = (query, path) => {
1838
- let at = 0;
1839
- for (const char of query) {
1840
- at = path.indexOf(char, at) + 1;
1841
- if (!at) return false;
1842
- }
1843
- return true;
1844
- };
1845
- function memoryTuiConnection(connection, options = {}) {
1846
- const listed = /* @__PURE__ */ new Map();
1847
- const files = (cwd) => {
1848
- const cached = listed.get(cwd);
1849
- if (cached && Date.now() - cached.at < FILE_LIST_TTL_MS) return cached.files;
1850
- const entry = { at: Date.now(), files: walkFiles(cwd).catch(() => []) };
1851
- listed.set(cwd, entry);
1852
- return entry.files;
1853
- };
1854
- return {
1855
- info: () => ({ provider: options.provider, usingChatGPTPlan: options.usingChatGPTPlan }),
1856
- async newSession(cwd) {
1857
- const state = await connection.newSession({ cwd });
1858
- if (!options.mode || !state.modes || state.modes.currentModeId === options.mode) return state;
1859
- await connection.setMode(state.sessionId, options.mode);
1860
- return { ...state, modes: { ...state.modes, currentModeId: options.mode } };
1861
- },
1862
- loadSession: async (sessionId, cwd) => await connection.loadSession(sessionId, { cwd }),
1863
- listSessions: async (cwd) => (await connection.listSessions(cwd)).sessions,
1864
- prompt: (sessionId, prompt) => connection.prompt(sessionId, prompt),
1865
- cancel: (sessionId) => connection.cancel(sessionId),
1866
- setMode: (sessionId, modeId) => connection.setMode(sessionId, modeId),
1867
- setModel: async (sessionId, modelId) => await connection.setModel(sessionId, modelId),
1868
- setConfigOption: async (sessionId, configId, value) => await connection.setConfigOption(sessionId, configId, value),
1869
- onUpdate: (listener) => connection.onUpdate((sessionId, update) => listener(sessionId, update)),
1870
- onPermission: (handler) => connection.onPermission(async (request) => (await handler(request)).outcome),
1871
- async listFiles(query, cwd) {
1872
- const term = query.toLowerCase();
1873
- return (await files(cwd)).filter((path) => subsequence(term, path.toLowerCase())).slice(0, FILE_MATCHES);
1874
- }
1875
- };
1876
- }
1877
- function fileHistory(path) {
1878
- return {
1879
- load() {
1880
- try {
1881
- return readFileSync(path, "utf8").split("\n").flatMap((line) => {
1882
- try {
1883
- const entry = JSON.parse(line);
1884
- return typeof entry === "string" ? [entry] : [];
1885
- } catch {
1886
- return [];
1887
- }
1888
- }).slice(-HISTORY_LIMIT);
1889
- } catch {
1890
- return [];
1891
- }
1892
- },
1893
- save(entry) {
1894
- try {
1895
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
1896
- appendFileSync(path, `${JSON.stringify(entry)}
1897
- `, { mode: 384 });
1898
- const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
1899
- if (lines.length > HISTORY_LIMIT * 2) writeFileSync(path, `${lines.slice(-HISTORY_LIMIT).join("\n")}
1900
- `, { mode: 384 });
1901
- } catch {
1902
- }
1903
- }
1904
- };
1905
- }
1906
2247
  async function run(connection, options) {
1907
2248
  const controller = new Controller(connection, { cwd: options.cwd, history: options.history });
1908
2249
  const exited = new Promise((resolve) => {
@@ -1951,4 +2292,4 @@ export {
1951
2292
  run,
1952
2293
  runTui
1953
2294
  };
1954
- //# sourceMappingURL=index-BrBnidfI.js.map
2295
+ //# sourceMappingURL=index-T_6HaJx-.js.map