@oxbit/cli 0.11.2 → 0.11.4

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,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { Box, Text, renderToString, useAnimation, measureElement, useInput, useWindowSize, usePaste, Static, render } from "ink";
4
- import { createContext, useContext, useMemo, useState, useRef, useEffect, useSyncExternalStore } from "react";
4
+ import { createContext, useContext, useMemo, useState, useRef, useEffect, memo, useSyncExternalStore } from "react";
5
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";
6
+ import { writeFile } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { resolve } from "node:path";
9
+ import { memoryTuiConnection, fileHistory } from "./memory-C3ixKimB.js";
10
+ import "node:child_process";
7
11
  import "node:util";
8
12
  import "node:readline";
9
- import "node:fs/promises";
10
- import "node:path";
11
13
  import "node:crypto";
12
- import "node:os";
13
- import "node:child_process";
14
14
  import "node:http";
15
15
  import "node:fs";
16
16
  const THEMES = {
@@ -1195,6 +1195,7 @@ function applyStatus(status, update) {
1195
1195
  return next;
1196
1196
  }
1197
1197
  const modelName = (status) => status.models.find((model) => model.value === status.model)?.name ?? status.model;
1198
+ const NO_SESSION = "There is no open session. Run /new to start one.";
1198
1199
  const STOP_NOTES = {
1199
1200
  cancelled: "Interrupted",
1200
1201
  max_tokens: "The response reached the output token limit",
@@ -1212,6 +1213,7 @@ class SessionController {
1212
1213
  draft: emptyDraft,
1213
1214
  running: false,
1214
1215
  queued: [],
1216
+ generation: 0,
1215
1217
  status: emptyStatus(),
1216
1218
  info: connection.info()
1217
1219
  };
@@ -1226,6 +1228,7 @@ class SessionController {
1226
1228
  timer;
1227
1229
  hintTimer;
1228
1230
  early;
1231
+ opening;
1229
1232
  stop;
1230
1233
  onExit;
1231
1234
  subscribe = (listener) => {
@@ -1263,9 +1266,9 @@ class SessionController {
1263
1266
  };
1264
1267
  commandsChanged() {
1265
1268
  }
1266
- onPermission = (request) => new Promise((resolve) => {
1267
- if (this.rules.has(ruleKey(request))) return resolve(outcomeOf(request, "once").outcome);
1268
- this.permissions.push({ request, resolve, index: 0 });
1269
+ onPermission = (request) => new Promise((resolve2) => {
1270
+ if (this.rules.has(ruleKey(request))) return resolve2(outcomeOf(request, "once").outcome);
1271
+ this.permissions.push({ request, resolve: resolve2, index: 0 });
1269
1272
  if (this.permissions.length === 1) this.set({ permission: this.permissions[0] });
1270
1273
  });
1271
1274
  /** Opens `sessionId` (or a new session) and replays updates that arrived before the response. */
@@ -1280,7 +1283,15 @@ class SessionController {
1280
1283
  if (this.state.running) return this.notice("Wait for the agent to finish, or press Esc to stop it", "warn");
1281
1284
  await this.load(this.state.status.sessionId, false);
1282
1285
  }
1283
- async load(sessionId, replay) {
1286
+ /** Opens the session; prompts sent before it resolves wait for it. */
1287
+ load(sessionId, replay) {
1288
+ const opening = this.connect(sessionId, replay);
1289
+ this.opening = opening;
1290
+ return opening.finally(() => {
1291
+ if (this.opening === opening) this.opening = void 0;
1292
+ });
1293
+ }
1294
+ async connect(sessionId, replay) {
1284
1295
  this.early = [];
1285
1296
  let session;
1286
1297
  try {
@@ -1302,12 +1313,14 @@ class SessionController {
1302
1313
  if (replay) this.transcript(endTurn);
1303
1314
  }
1304
1315
  async send(text) {
1305
- const sessionId = this.state.status.sessionId;
1306
- if (!sessionId) return this.notice("There is no open session. Run /new to start one.", "error");
1316
+ if (!this.state.status.sessionId && !this.opening) return this.notice(NO_SESSION, "error");
1307
1317
  this.set({ transcript: append(this.state.transcript, { type: "user", text }), running: true, startedAt: Date.now() });
1308
1318
  let failed;
1309
1319
  let stopReason = "end_turn";
1310
1320
  try {
1321
+ await this.opening;
1322
+ const sessionId = this.state.status.sessionId;
1323
+ if (!sessionId) throw new Error(NO_SESSION);
1311
1324
  stopReason = (await this.connection.prompt(sessionId, [{ type: "text", text }])).stopReason;
1312
1325
  } catch (error) {
1313
1326
  failed = message(error);
@@ -1575,9 +1588,53 @@ class PanelController extends SessionController {
1575
1588
  this.closeOverlay();
1576
1589
  }
1577
1590
  }
1591
+ const joined = (items) => items.reduce((text, item) => item.part || !text ? text + item.text : `${text.trimEnd()}
1592
+
1593
+ ${item.text}`, "").trim();
1594
+ function lastReply(items) {
1595
+ const end2 = items.findLastIndex((item) => item.type === "assistant" && item.text.trim());
1596
+ if (end2 < 0) return void 0;
1597
+ const start = items.slice(0, end2).findLastIndex((item) => item.type === "user") + 1;
1598
+ return joined(items.slice(start, end2 + 1).filter((item) => item.type === "assistant")) || void 0;
1599
+ }
1600
+ function toMarkdown(items, title, sessionId) {
1601
+ const blocks = [`# ${title || "Oxbit Agent session"}`, ...sessionId ? [`Session \`${sessionId}\``] : []];
1602
+ let reply = [];
1603
+ let agent = false;
1604
+ const close = () => {
1605
+ const text = joined(reply);
1606
+ if (text) blocks.push(text);
1607
+ reply = [];
1608
+ };
1609
+ for (const item of items) {
1610
+ if (item.type === "user") {
1611
+ close();
1612
+ agent = false;
1613
+ blocks.push(item.continuation ? "## User (continuation)" : "## User", item.text.trim());
1614
+ continue;
1615
+ }
1616
+ if (item.type !== "assistant" && item.type !== "tool") continue;
1617
+ if (!agent) blocks.push("## Agent");
1618
+ agent = true;
1619
+ if (item.type === "assistant") {
1620
+ if (!item.part) close();
1621
+ reply.push(item);
1622
+ } else {
1623
+ close();
1624
+ blocks.push(`- Tool: ${item.tool.title} (${item.tool.status})`);
1625
+ }
1626
+ }
1627
+ close();
1628
+ return `${blocks.join("\n\n")}
1629
+ `;
1630
+ }
1631
+ const osc52 = (text) => `\x1B]52;c;${Buffer.from(text).toString("base64")}\x07`;
1632
+ const oscTitle = (title) => `\x1B]2;${title.replace(/[\x00-\x1f\x7f]/g, " ")}\x07`;
1633
+ const windowTitle = (title, running) => `${running ? "● " : ""}${title || "Oxbit Agent"}`;
1578
1634
  const LOCAL_COMMANDS = [
1579
1635
  { name: "model", description: "Choose the model and reasoning effort", hint: "[model]" },
1580
1636
  { name: "new", description: "Start a new session" },
1637
+ { name: "clear", description: "Clear the conversation and the screen, and start a new session" },
1581
1638
  { name: "resume", description: "Resume a saved session" },
1582
1639
  { name: "config", description: "Settings: permission mode, auto-compact, hooks and MCP trust" },
1583
1640
  { name: "skills", description: "Turn skills on or off, or start a prompt with one" },
@@ -1585,9 +1642,13 @@ const LOCAL_COMMANDS = [
1585
1642
  { name: "mcp", description: "Add, remove, trust and sign in to MCP servers" },
1586
1643
  { name: "instructions", description: "Choose which AGENTS.md and CLAUDE.md files load" },
1587
1644
  { name: "providers", description: "Sign in to providers and set API keys" },
1645
+ { name: "copy", description: "Copy the last agent reply to the clipboard" },
1646
+ { name: "export", description: "Write the conversation to a Markdown file", hint: "[path]" },
1647
+ { name: "init", description: "Ask the agent to create or update AGENTS.md for this repository" },
1588
1648
  { name: "help", description: "Show keys and commands" },
1589
1649
  { name: "exit", description: "Quit" }
1590
1650
  ];
1651
+ const INIT_PROMPT = "Inspect this repository and create or update AGENTS.md at its root. Document the build, test and lint commands, the project layout, and the coding conventions an agent needs to work here. Keep existing content that is still accurate.";
1591
1652
  const picker = (overlay) => overlay?.kind === "models" || overlay?.kind === "sessions" ? overlay : void 0;
1592
1653
  function overlayItems(state) {
1593
1654
  const overlay = state.overlay;
@@ -1611,6 +1672,8 @@ class Controller extends PanelController {
1611
1672
  fileTimer;
1612
1673
  files = /* @__PURE__ */ new Map();
1613
1674
  lastInterrupt = 0;
1675
+ /** Set by the fullscreen transcript view; reads the current layout. */
1676
+ viewport;
1614
1677
  /** Server commands, then local ones; a panel command shows the server command's hint, since arguments go to the server. */
1615
1678
  commands() {
1616
1679
  const server = this.state.status.commands;
@@ -1631,6 +1694,9 @@ class Controller extends PanelController {
1631
1694
  this.dismissed = void 0;
1632
1695
  this.set({ draft: emptyDraft, completion: void 0 });
1633
1696
  if (text.startsWith("/") && this.local(text)) return;
1697
+ this.prompt(text);
1698
+ }
1699
+ prompt(text) {
1634
1700
  if (this.state.running) this.set({ queued: [...this.state.queued, text] });
1635
1701
  else void this.send(text);
1636
1702
  }
@@ -1645,6 +1711,18 @@ class Controller extends PanelController {
1645
1711
  case "new":
1646
1712
  void this.open(void 0, "New session");
1647
1713
  return true;
1714
+ case "clear":
1715
+ void this.clear();
1716
+ return true;
1717
+ case "copy":
1718
+ this.copyReply();
1719
+ return true;
1720
+ case "export":
1721
+ void this.exportTo(args);
1722
+ return true;
1723
+ case "init":
1724
+ this.prompt(INIT_PROMPT);
1725
+ return true;
1648
1726
  case "resume":
1649
1727
  if (!this.connection.listSessions && !args) return false;
1650
1728
  if (args) void this.open(args, "Resumed session");
@@ -1772,6 +1850,49 @@ class Controller extends PanelController {
1772
1850
  const next = items[Math.min(items.length - 1, Math.max(0, at + step - (overlay?.kind === "details" ? 0 : 1)))];
1773
1851
  this.set({ overlay: { kind: "details", id: next.id } });
1774
1852
  }
1853
+ /** /clear: empties the transcript, queue and screen, then opens a new session. */
1854
+ async clear() {
1855
+ if (this.state.running) return this.notice("Wait for the agent to finish, or press Esc to stop it", "warn");
1856
+ this.options.screen?.clear();
1857
+ this.set({ transcript: emptyTranscript(), queued: [], overlay: void 0, completion: void 0, scroll: void 0, generation: this.state.generation + 1 });
1858
+ await this.open();
1859
+ }
1860
+ /** Ctrl+L: clears the terminal and draws the current frame again at the newest output. */
1861
+ redraw() {
1862
+ this.options.screen?.clear();
1863
+ this.set({ scroll: void 0 });
1864
+ }
1865
+ copyReply() {
1866
+ const { flushed, live } = this.state.transcript;
1867
+ const reply = lastReply([...flushed, ...live]);
1868
+ if (!reply) return this.flash("There is no agent reply to copy yet");
1869
+ this.options.screen?.write(osc52(reply));
1870
+ this.flash("Copied the last reply to the clipboard");
1871
+ }
1872
+ /** Writes the conversation as Markdown; an existing file is left alone. */
1873
+ async exportTo(path) {
1874
+ const { transcript: { flushed, live }, status: { sessionId, title } } = this.state;
1875
+ const target = resolve(this.options.cwd, path.replace(/^~(?=$|\/)/, homedir()) || `oxbit-session-${sessionId?.slice(0, 8) ?? "new"}.md`);
1876
+ try {
1877
+ await writeFile(target, toMarkdown([...flushed, ...live], title, sessionId), { flag: "wx" });
1878
+ this.notice(`Exported the conversation to ${target}`);
1879
+ } catch (error) {
1880
+ this.notice(error.code === "EEXIST" ? `${target} already exists. Choose another path.` : message(error), "error");
1881
+ }
1882
+ }
1883
+ /** PageUp and PageDown in fullscreen; reaching the bottom follows the newest output again. */
1884
+ scrollBy(pages) {
1885
+ const viewport = this.viewport?.();
1886
+ if (!viewport) return;
1887
+ const { total, height, first } = viewport;
1888
+ const max = Math.max(0, total - height);
1889
+ const scroll = this.state.scroll;
1890
+ const top = Math.min(max, Math.max(0, Math.min(scroll?.top ?? max, max) + pages * Math.max(1, height - 2)));
1891
+ this.set({ scroll: top >= max ? void 0 : { top, from: scroll?.from ?? first } });
1892
+ }
1893
+ scrollEnd() {
1894
+ if (this.state.scroll) this.set({ scroll: void 0 });
1895
+ }
1775
1896
  /** Ctrl+C: stop the turn, close or clear, and exit on a second press within two seconds. */
1776
1897
  interrupt() {
1777
1898
  const now = Date.now();
@@ -1797,6 +1918,8 @@ const KEYS = [
1797
1918
  ["ctrl+o", "show tool output, diffs and thinking in full"],
1798
1919
  ["ctrl+p, /model", "choose the model"],
1799
1920
  ["/ and @", "complete commands and files"],
1921
+ ["pgup pgdn, end", "scroll the transcript, then return to the latest (fullscreen)"],
1922
+ ["ctrl+l", "clear and redraw the screen"],
1800
1923
  ["ctrl+a e u k w", "line editing"]
1801
1924
  ];
1802
1925
  function windowOf(items, index, size) {
@@ -1879,7 +2002,7 @@ function OverlayView({ state, width: width2 }) {
1879
2002
  ] }, mode.id)),
1880
2003
  /* @__PURE__ */ jsxs(Text, { color: theme.muted, children: [
1881
2004
  "\n",
1882
- "Type / to list commands. esc closes this help."
2005
+ "Type / to list commands: /clear, /copy, /export [path], /init and more. esc closes this help."
1883
2006
  ] })
1884
2007
  ] });
1885
2008
  }
@@ -1972,8 +2095,11 @@ function handleKey(controller, input, key) {
1972
2095
  const draft = state.draft;
1973
2096
  const edit = controller.setDraft.bind(controller);
1974
2097
  if (key.ctrl && input === "c") return controller.interrupt();
2098
+ if (key.ctrl && input === "l") return controller.redraw();
1975
2099
  if (state.permission) return permissionKey(controller, input, key);
1976
2100
  if (state.overlay) return overlayKey(controller, input, key);
2101
+ if (key.pageUp || key.pageDown) return controller.scrollBy(key.pageUp ? -1 : 1);
2102
+ if (key.end && !draft.text && state.scroll) return controller.scrollEnd();
1977
2103
  if (key.tab && key.shift) return controller.cycleMode();
1978
2104
  if (key.ctrl && input === "o") return controller.showDetails();
1979
2105
  if (key.ctrl && input === "p") return controller.openModels();
@@ -2191,7 +2317,7 @@ function StatusLine({ state, width: width$1 }) {
2191
2317
  status.loop.interval ? ` · every ${status.loop.interval}` : ""
2192
2318
  ].join("");
2193
2319
  const left2 = fitSegments([
2194
- hint ? { text: hint, color: theme.warning, priority: 0 } : { text: `${MODE_LABELS[status.mode] ?? status.mode}${width$1 >= 80 ? " (shift+tab)" : ""}`, color: theme.modes[status.mode], priority: 0 },
2320
+ hint ? { text: hint, color: theme.warning, priority: 0 } : state.scroll ? { text: "↑ scrolled up · pgdn or end returns to the latest", color: theme.warning, priority: 0 } : { text: `${MODE_LABELS[status.mode] ?? status.mode}${width$1 >= 80 ? " (shift+tab)" : ""}`, color: theme.modes[status.mode], priority: 0 },
2195
2321
  ...status.goal ? [{ text: `◎ goal ${status.goal.status}`, color: theme.accent, priority: 1 }] : [],
2196
2322
  ...loopText ? [{ text: loopText, color: theme.accent, priority: 2 }] : []
2197
2323
  ], budget, " ");
@@ -2216,17 +2342,46 @@ function StatusLine({ state, width: width$1 }) {
2216
2342
  ] }, index)) })
2217
2343
  ] });
2218
2344
  }
2219
- function Screen({ controller, version }) {
2220
- const state = useSyncExternalStore(controller.subscribe, controller.getState);
2345
+ const TRANSCRIPT_LIMIT = 200;
2346
+ const same = (a, b) => a.item === b.item && a.live === b.live && Object.keys(a.context).every((key) => a.context[key] === b.context[key]);
2347
+ const Entry = memo(function Entry2({ item, live, context }) {
2348
+ return /* @__PURE__ */ jsx(Box, { width: context.width, flexDirection: "column", flexShrink: 0, children: /* @__PURE__ */ jsx(ItemView, { item, live, context }) });
2349
+ }, same);
2350
+ const heightOf = (ref) => ref.current ? measureElement(ref.current).height : 0;
2351
+ function Viewport({ controller, state, context }) {
2352
+ const outer = useRef(null);
2353
+ const inner = useRef(null);
2354
+ const { flushed, live } = state.transcript;
2355
+ const items = [...flushed, ...live];
2356
+ const { scroll } = state;
2357
+ const pinned = scroll ? items.findIndex((item) => item.id === scroll.from) : -1;
2358
+ const start = pinned >= 0 ? pinned : Math.max(0, items.length - TRANSCRIPT_LIMIT);
2359
+ const first = items[start]?.id ?? 0;
2360
+ useEffect(() => {
2361
+ controller.viewport = () => ({ total: heightOf(inner), height: heightOf(outer), first });
2362
+ });
2363
+ useEffect(() => () => {
2364
+ controller.viewport = void 0;
2365
+ }, [controller]);
2366
+ const max = inner.current && outer.current ? Math.max(0, heightOf(inner) - heightOf(outer)) : Infinity;
2367
+ return /* @__PURE__ */ jsx(
2368
+ Box,
2369
+ {
2370
+ ref: outer,
2371
+ flexDirection: "column",
2372
+ flexGrow: 1,
2373
+ flexShrink: 1,
2374
+ overflow: "hidden",
2375
+ justifyContent: scroll ? "flex-start" : "flex-end",
2376
+ contentOffsetY: scroll ? Math.min(scroll.top, max) : 0,
2377
+ children: /* @__PURE__ */ jsx(Box, { ref: inner, flexDirection: "column", flexGrow: 1, flexShrink: 0, children: items.slice(start).map((item, at) => /* @__PURE__ */ jsx(Entry, { item, live: start + at >= flushed.length, context }, item.id)) })
2378
+ }
2379
+ );
2380
+ }
2381
+ function Bottom({ controller, state, context }) {
2221
2382
  const theme = useTheme();
2222
- const { columns, rows } = useWindowSize();
2223
- const width2 = Math.max(20, columns);
2224
- useInput((input, key) => handleKey(controller, input, key));
2225
- usePaste((text) => controller.paste(text));
2226
- const context = { width: width2, rows, expanded: false, cwd: state.cwd, version, awaiting: state.permission?.request.toolCall.toolCallId };
2227
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
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) }),
2229
- state.transcript.live.map((item) => /* @__PURE__ */ jsx(ItemView, { item, live: true, context }, item.id)),
2383
+ const { width: width2 } = context;
2384
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, flexShrink: 0, children: [
2230
2385
  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: [
2231
2386
  state.running ? /* @__PURE__ */ jsx(Working, { state }) : null,
2232
2387
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
@@ -2241,18 +2396,58 @@ function Screen({ controller, version }) {
2241
2396
  /* @__PURE__ */ jsx(StatusLine, { state, width: width2 })
2242
2397
  ] });
2243
2398
  }
2244
- function App({ controller, theme, version }) {
2245
- return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(Screen, { controller, version }) });
2399
+ function Screen({ controller, version, fullscreen }) {
2400
+ const state = useSyncExternalStore(controller.subscribe, controller.getState);
2401
+ const { columns, rows } = useWindowSize();
2402
+ const width2 = Math.max(20, columns);
2403
+ useInput((input, key) => handleKey(controller, input, key));
2404
+ usePaste((text) => controller.paste(text));
2405
+ const context = { width: width2, rows, expanded: false, cwd: state.cwd, version, awaiting: state.permission?.request.toolCall.toolCallId };
2406
+ if (fullscreen) {
2407
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, height: rows, children: [
2408
+ /* @__PURE__ */ jsx(Viewport, { controller, state, context }),
2409
+ /* @__PURE__ */ jsx(Bottom, { controller, state, context })
2410
+ ] });
2411
+ }
2412
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: width2, children: [
2413
+ /* @__PURE__ */ jsx(Static, { items: state.transcript.flushed, children: (item) => /* @__PURE__ */ jsx(Box, { width: width2, flexDirection: "column", children: /* @__PURE__ */ jsx(ItemView, { item, context }) }, item.id) }, state.generation),
2414
+ state.transcript.live.map((item) => /* @__PURE__ */ jsx(ItemView, { item, live: true, context }, item.id)),
2415
+ /* @__PURE__ */ jsx(Bottom, { controller, state, context })
2416
+ ] });
2417
+ }
2418
+ function App({ controller, theme, version, fullscreen = false }) {
2419
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: theme, children: /* @__PURE__ */ jsx(Screen, { controller, version, fullscreen }) });
2246
2420
  }
2421
+ const CLEAR = "\x1B[2J\x1B[H";
2422
+ const CLEAR_SCROLLBACK = "\x1B[2J\x1B[3J\x1B[H";
2247
2423
  async function run(connection, options) {
2248
- const controller = new Controller(connection, { cwd: options.cwd, history: options.history });
2249
- const exited = new Promise((resolve) => {
2250
- controller.onExit = resolve;
2424
+ const stdout = options.stdout ?? process.stdout;
2425
+ const tty = !!stdout.isTTY;
2426
+ const fullscreen = options.fullscreen ?? (tty && !options.debug);
2427
+ const screen = {
2428
+ write: (sequence) => void stdout.write(sequence),
2429
+ clear: () => {
2430
+ instance.clear();
2431
+ stdout.write(fullscreen ? CLEAR : CLEAR_SCROLLBACK);
2432
+ }
2433
+ };
2434
+ const controller = new Controller(connection, { cwd: options.cwd, history: options.history, screen });
2435
+ const exited = new Promise((resolve2) => {
2436
+ controller.onExit = resolve2;
2251
2437
  });
2438
+ let title;
2439
+ const showTitle = () => {
2440
+ const { status, running } = controller.getState();
2441
+ const next = windowTitle(status.title, running);
2442
+ if (tty && next !== title) stdout.write(oscTitle(title = next));
2443
+ };
2444
+ const unsubscribe = controller.subscribe(showTitle);
2445
+ showTitle();
2252
2446
  const streams = Object.fromEntries(Object.entries({ stdin: options.stdin, stdout: options.stdout, stderr: options.stderr }).filter(([, value]) => value));
2253
- const instance = render(/* @__PURE__ */ jsx(App, { controller, theme: resolveTheme(options.theme), version: options.version }), {
2447
+ const instance = render(/* @__PURE__ */ jsx(App, { controller, theme: resolveTheme(options.theme), version: options.version, fullscreen }), {
2254
2448
  ...streams,
2255
2449
  exitOnCtrlC: false,
2450
+ alternateScreen: fullscreen,
2256
2451
  ...options.debug ? { debug: true } : { kittyKeyboard: { mode: "auto" } }
2257
2452
  });
2258
2453
  void controller.open(options.sessionId).then(() => {
@@ -2263,8 +2458,12 @@ async function run(connection, options) {
2263
2458
  try {
2264
2459
  await Promise.race([exited, instance.waitUntilExit()]);
2265
2460
  } finally {
2461
+ unsubscribe();
2266
2462
  controller.dispose();
2267
2463
  instance.unmount();
2464
+ await instance.waitUntilExit().catch(() => {
2465
+ });
2466
+ if (title !== void 0) stdout.write(oscTitle(""));
2268
2467
  }
2269
2468
  return { sessionId: controller.getState().status.sessionId };
2270
2469
  }
@@ -2281,7 +2480,8 @@ async function runTui(options) {
2281
2480
  history: options.historyPath ? fileHistory(options.historyPath) : void 0,
2282
2481
  stdin: options.stdin,
2283
2482
  stdout: options.stdout,
2284
- stderr: options.stderr
2483
+ stderr: options.stderr,
2484
+ fullscreen: options.fullscreen
2285
2485
  });
2286
2486
  if (sessionId) (options.stderr ?? process.stderr).write(`Resume this session with: oxbit-agent -r ${sessionId}
2287
2487
  `);
@@ -2292,4 +2492,4 @@ export {
2292
2492
  run,
2293
2493
  runTui
2294
2494
  };
2295
- //# sourceMappingURL=index-hc_7c2Gx.js.map
2495
+ //# sourceMappingURL=index-Crr8fCGQ.js.map
@@ -2,12 +2,12 @@
2
2
  import { mkdirSync, appendFileSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname } from "node:path";
4
4
  import { w as walkFiles } from "../index.js";
5
+ import "node:child_process";
5
6
  import "node:util";
6
7
  import "node:readline";
7
8
  import "node:fs/promises";
8
9
  import "node:crypto";
9
10
  import "node:os";
10
- import "node:child_process";
11
11
  import "node:http";
12
12
  const FILE_LIST_TTL_MS = 1e4;
13
13
  const FILE_MATCHES = 2e3;
@@ -22,6 +22,7 @@ const subsequence = (query, path) => {
22
22
  };
23
23
  function memoryTuiConnection(initial, options = {}) {
24
24
  let connection = initial;
25
+ let { modes, model } = options;
25
26
  let info = { provider: options.provider, usingChatGPTPlan: options.usingChatGPTPlan };
26
27
  let mode = options.mode;
27
28
  const listeners = /* @__PURE__ */ new Set();
@@ -51,7 +52,8 @@ function memoryTuiConnection(initial, options = {}) {
51
52
  get connection() {
52
53
  return connection;
53
54
  },
54
- async replace(next, nextInfo) {
55
+ async replace(next, nextInfo, nextOptions = {}) {
56
+ ({ modes, model } = nextOptions);
55
57
  const previous = connection;
56
58
  for (const stop of detach) stop();
57
59
  connection = next;
@@ -61,10 +63,12 @@ function memoryTuiConnection(initial, options = {}) {
61
63
  },
62
64
  info: () => ({ ...info }),
63
65
  async newSession(cwd) {
64
- const state = await connection.newSession({ cwd });
65
- if (!mode || !state.modes || state.modes.currentModeId === mode) return state;
66
- await connection.setMode(state.sessionId, mode);
67
- return { ...state, modes: { ...state.modes, currentModeId: mode } };
66
+ let state = await connection.newSession({ cwd });
67
+ if (model) state = { ...state, ...await connection.setModel(state.sessionId, model) };
68
+ const modeId = mode && (modes?.[mode] ?? mode);
69
+ if (!modeId || !state.modes || state.modes.currentModeId === modeId) return state;
70
+ await connection.setMode(state.sessionId, modeId);
71
+ return { ...state, modes: { ...state.modes, currentModeId: modeId } };
68
72
  },
69
73
  loadSession: async (sessionId, cwd) => await connection.loadSession(sessionId, { cwd }),
70
74
  listSessions: async (cwd) => (await connection.listSessions(cwd)).sessions,
@@ -132,4 +136,4 @@ export {
132
136
  fileHistory,
133
137
  memoryTuiConnection
134
138
  };
135
- //# sourceMappingURL=memory-Bdy8hvTl.js.map
139
+ //# sourceMappingURL=memory-C3ixKimB.js.map