@ouro.bot/cli 0.1.0-alpha.251 → 0.1.0-alpha.253

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/changelog.json CHANGED
@@ -1,10 +1,23 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.253",
6
+ "changes": [
7
+ "fix(daemon): test-isolation guard \u2014 stop tests from leaking real `inner.wake` commands into the running daemon. A pattern in 36+ test files mocks `getAgentName` to the literal `\"testagent\"` but does NOT mock `socket-client`, so any code path through pondering / rest / coding feedback fires a real socket connection at /tmp/ouroboros-daemon.sock with `inner.wake testagent`. The daemon errored on every command (`Unknown managed agent 'testagent'`) and at flood volumes that contributed to a real outage on the developer's machine. The fix is in `socket-client.ts` itself: detect vitest via `process.argv` (no env vars) and convert all socket operations into safe no-ops. Tests that legitimately exercise the real transport (socket-client.test.ts, daemon-cli-defaults.test.ts) opt out of the guard via a new `__bypassVitestGuardForTests()` setter that lives on globalThis to survive `vi.resetModules()`. New nerves events: `daemon.socket_command_test_blocked`, `daemon.inner_wake_test_blocked`.",
8
+ "test(contract): new test-isolation contract test ratchets two anti-patterns: (1) test files using `name: \"testagent\"` without mocking socket-client, and (2) test files constructing write paths under the real `~/AgentBundles` via `os.homedir()`. Existing offenders are grandfathered in two allowlists; new offenders fail the build. Follow-up PRs shrink the allowlists toward zero."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.252",
13
+ "changes": [
14
+ "fix(tui): polished history preview (spacing, shadow color, showing-recent label), full-width separators, backslash+enter and ctrl+j for newlines."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.251",
6
19
  "changes": [
7
- "fix(bluebubbles): images sent via iMessage now reach the model — adds capability-gated image hydration with a MiniMax VLM fallback. Reasoning MiniMax chat models (M2/M2.1/M2.5/M2.7) silently drop OpenAI-style `image_url` content parts, so previously slugger answered image questions from fabricated context. Now, when the active chat model lacks the new `vision: true` capability flag, inbound screenshots are auto-described at ingestion via `/v1/coding_plan/vlm` and the description text replaces the `image_url` part before the turn reaches the model. Vision-capable models (claude-opus/sonnet-4-6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01) continue to see images natively via pass-through.",
20
+ "fix(bluebubbles): images sent via iMessage now reach the model \u2014 adds capability-gated image hydration with a MiniMax VLM fallback. Reasoning MiniMax chat models (M2/M2.1/M2.5/M2.7) silently drop OpenAI-style `image_url` content parts, so previously slugger answered image questions from fabricated context. Now, when the active chat model lacks the new `vision: true` capability flag, inbound screenshots are auto-described at ingestion via `/v1/coding_plan/vlm` and the description text replaces the `image_url` part before the turn reaches the model. Vision-capable models (claude-opus/sonnet-4-6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01) continue to see images natively via pass-through.",
8
21
  "feat(tools): new `describe_image` agent tool registered into the BlueBubbles tool set when the chat model lacks vision. Lets the agent re-interrogate an attachment with a targeted prompt (e.g. 'what's the flight number in the bottom-right?') after ingestion. Backed by a bounded in-memory attachment cache populated during hydration; handler re-downloads bytes and calls the same VLM client.",
9
22
  "fix(bluebubbles): `formatMessageText` now preserves the attachment marker when a message has BOTH text and attachments (B2). Previously the marker was dropped whenever text was present, hiding attachments from the agent's view of the message.",
10
23
  "feat(heart): `ModelCapabilities` gains `vision?: boolean` and `audio?: boolean` flags (B4). Vision rows populated for claude-opus-4-6, claude-sonnet-4-6, claude-opus-4.6, claude-sonnet-4.6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01. M2.1/M2.5/M2.7 intentionally left unset.",
@@ -14,7 +27,7 @@
14
27
  {
15
28
  "version": "0.1.0-alpha.250",
16
29
  "changes": [
17
- "fix(sync): surface 'bundle is not a git repo' as an actionable error instead of silently failing. Previously, enabling `sync.enabled` on a bundle that had never been `git init`'d produced a generic `git status` failure buried in nerves logs; the agent saw nothing in its start-of-turn packet and the user saw nothing in `ouro status`. Now: (1) `preTurnPull` and `postTurnPush` detect the missing `.git` directory before touching git and return an actionable error with the bundle path and the `git init` hint; (2) this error propagates via `ctx.syncFailure` into the agent's Sync warning, so the agent can offer to run `git init` or just do it; (3) `ouro status` shows a red `error` state with `not a git repo — run \\`git init\\` to enable sync` next to the offending bundle. New nerves event: `heart.sync_not_a_repo`."
30
+ "fix(sync): surface 'bundle is not a git repo' as an actionable error instead of silently failing. Previously, enabling `sync.enabled` on a bundle that had never been `git init`'d produced a generic `git status` failure buried in nerves logs; the agent saw nothing in its start-of-turn packet and the user saw nothing in `ouro status`. Now: (1) `preTurnPull` and `postTurnPush` detect the missing `.git` directory before touching git and return an actionable error with the bundle path and the `git init` hint; (2) this error propagates via `ctx.syncFailure` into the agent's Sync warning, so the agent can offer to run `git init` or just do it; (3) `ouro status` shows a red `error` state with `not a git repo \u2014 run \\`git init\\` to enable sync` next to the offending bundle. New nerves event: `heart.sync_not_a_repo`."
18
31
  ]
19
32
  },
20
33
  {
@@ -27,7 +40,7 @@
27
40
  {
28
41
  "version": "0.1.0-alpha.248",
29
42
  "changes": [
30
- "feat(daemon): `ouro status` Git Sync now resolves and shows the actual remote URL via `git remote get-url`. Three states: `origin → git@github.com:me/foo.git` when the remote resolves, `local only` when sync is enabled but no remote is configured, `disabled` when sync is off. Previously you had to `cd` into the bundle and run `git remote -v` to find out where it pushes.",
43
+ "feat(daemon): `ouro status` Git Sync now resolves and shows the actual remote URL via `git remote get-url`. Three states: `origin \u2192 git@github.com:me/foo.git` when the remote resolves, `local only` when sync is enabled but no remote is configured, `disabled` when sync is off. Previously you had to `cd` into the bundle and run `git remote -v` to find out where it pushes.",
31
44
  "fix(heart/sync): `preTurnPull` now skips the pull when no git remote is configured, mirroring the existing `postTurnPush` behavior. Closes the half-implemented 'no-remote sync' (local-only commit log) story \u2014 previously, enabling sync without a remote produced a `syncFailure` on every turn from the failing `git pull` call."
32
45
  ]
33
46
  },
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DEFAULT_DAEMON_SOCKET_PATH = void 0;
37
+ exports.__bypassVitestGuardForTests = __bypassVitestGuardForTests;
37
38
  exports.sendDaemonCommand = sendDaemonCommand;
38
39
  exports.checkDaemonSocketAlive = checkDaemonSocketAlive;
39
40
  exports.requestInnerWake = requestInnerWake;
@@ -41,7 +42,50 @@ const fs = __importStar(require("fs"));
41
42
  const net = __importStar(require("net"));
42
43
  const runtime_1 = require("../../nerves/runtime");
43
44
  exports.DEFAULT_DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
45
+ /**
46
+ * Defense-in-depth: detect if we're running under vitest. Tests that forget
47
+ * to `vi.mock("../../heart/daemon/socket-client")` would otherwise leak real
48
+ * `inner.wake` commands (with whatever literal agent name the test uses,
49
+ * commonly "testagent") into whichever real daemon happens to be running on
50
+ * the developer's machine. That has caused real outages when test loops
51
+ * flooded the production-on-localhost daemon.
52
+ *
53
+ * We detect vitest via `process.argv` (not env vars — the project bans those)
54
+ * and convert all socket operations into safe no-ops. Individual tests are
55
+ * still expected to vi.mock this module so they can assert on call counts,
56
+ * but this guard guarantees that "I forgot to mock" can never again take
57
+ * down the daemon.
58
+ *
59
+ * The socket-client's OWN tests (which exercise the real functions against a
60
+ * test socket) call `__bypassVitestGuardForTests(true)` to opt out of the
61
+ * guard. We persist that flag on globalThis so it survives `vi.resetModules`
62
+ * (which clears the module cache between tests).
63
+ */
64
+ const BYPASS_KEY = "__ouro_socket_client_bypass_vitest_guard__";
65
+ /** ONLY for socket-client.test.ts. Do not call from any other test or production code. */
66
+ function __bypassVitestGuardForTests(bypass) {
67
+ ;
68
+ globalThis[BYPASS_KEY] = bypass;
69
+ }
70
+ function isUnderVitest() {
71
+ if (globalThis[BYPASS_KEY] === true)
72
+ return false;
73
+ /* v8 ignore next -- defensive: process and process.argv always exist in node @preserve */
74
+ if (typeof process === "undefined" || !Array.isArray(process.argv))
75
+ return false;
76
+ return process.argv.some((arg) => typeof arg === "string" && arg.includes("vitest"));
77
+ }
44
78
  function sendDaemonCommand(socketPath, command) {
79
+ if (isUnderVitest()) {
80
+ (0, runtime_1.emitNervesEvent)({
81
+ level: "warn",
82
+ component: "daemon",
83
+ event: "daemon.socket_command_test_blocked",
84
+ message: "blocked socket command from leaking into real daemon under vitest",
85
+ meta: { socketPath, kind: command.kind },
86
+ });
87
+ return Promise.resolve({ ok: true, message: "test mode: socket call suppressed" });
88
+ }
45
89
  (0, runtime_1.emitNervesEvent)({
46
90
  component: "daemon",
47
91
  event: "daemon.socket_command_start",
@@ -132,6 +176,9 @@ function sendDaemonCommand(socketPath, command) {
132
176
  });
133
177
  }
134
178
  function checkDaemonSocketAlive(socketPath) {
179
+ if (isUnderVitest()) {
180
+ return Promise.resolve(false);
181
+ }
135
182
  (0, runtime_1.emitNervesEvent)({
136
183
  component: "daemon",
137
184
  event: "daemon.socket_alive_check_start",
@@ -184,6 +231,16 @@ function checkDaemonSocketAlive(socketPath) {
184
231
  });
185
232
  }
186
233
  async function requestInnerWake(agent, socketPath = exports.DEFAULT_DAEMON_SOCKET_PATH) {
234
+ if (isUnderVitest()) {
235
+ (0, runtime_1.emitNervesEvent)({
236
+ level: "warn",
237
+ component: "daemon",
238
+ event: "daemon.inner_wake_test_blocked",
239
+ message: "blocked inner wake from leaking into real daemon under vitest",
240
+ meta: { agent, socketPath },
241
+ });
242
+ return null;
243
+ }
187
244
  const socketAvailable = fs.existsSync(socketPath);
188
245
  (0, runtime_1.emitNervesEvent)({
189
246
  component: "daemon",
@@ -41,10 +41,14 @@ function ringColor(elapsedSec) {
41
41
  return OURO.scale;
42
42
  }
43
43
  // ─── Header ─────────────────────────────────────────────────────────
44
- /** Safe terminal width: capped at 200, with 2-char margin */
44
+ /** Terminal width for content (capped at 200, with 2-char margin) */
45
45
  function safeWidth() {
46
46
  return Math.min(process.stdout.columns || 80, 200) - 2;
47
47
  }
48
+ /** Full terminal width for edge-to-edge elements (separators) */
49
+ function termWidth() {
50
+ return process.stdout.columns || 80;
51
+ }
48
52
  function Header({ agentName, model, contextPercent, cwd }) {
49
53
  const showCtx = contextPercent > 0;
50
54
  const info = [agentName, model, cwd, showCtx ? `ctx ${contextPercent}%` : ""].filter(Boolean).join(" · ");
@@ -81,18 +85,18 @@ function MessageBlock({ msg }) {
81
85
  return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
82
86
  return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: visibleCalls.map((tc, i) => (0, jsx_runtime_1.jsx)(ToolResultLine, { tc: tc }, i)) }));
83
87
  }
84
- // ── Session history (dimmed replay of last exchanges) ──
88
+ // ── Session history (dimmed recap of recent exchanges) ──
85
89
  if (msg.role === "history-summary") {
86
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: msg.content }) }));
90
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: msg.content }) }));
87
91
  }
88
92
  if (msg.role === "history-user") {
89
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, bold: true, children: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, bold: true, children: msg.content })] }) }));
93
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, bold: true, children: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, bold: true, children: msg.content })] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
90
94
  }
91
95
  if (msg.role === "history-assistant") {
92
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: msg.content ? (0, jsx_runtime_1.jsx)(streaming_markdown_1.StreamingMarkdown, { text: msg.content, maxWidth: safeWidth() }) : null }));
96
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: [msg.content ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: msg.content }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
93
97
  }
94
98
  if (msg.role === "history-end") {
95
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(safeWidth() + 2) }) }));
99
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.separator, children: "─".repeat(termWidth()) }) }));
96
100
  }
97
101
  if (msg.role === "user") {
98
102
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [msg.content ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, bold: true, children: msg.content }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
@@ -171,6 +175,23 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
171
175
  const cursorRef = (0, react_1.useRef)(0);
172
176
  const historyIdx = (0, react_1.useRef)(-1);
173
177
  const savedInput = (0, react_1.useRef)("");
178
+ // Raw stdin handler for Alt+Enter (ESC + CR/LF) — Ink 3.2's useInput splits
179
+ // \x1b\r into separate events, so we catch the combined sequence here.
180
+ const { stdin } = (0, ink_1.useStdin)();
181
+ (0, react_1.useEffect)(() => {
182
+ if (!stdin)
183
+ return;
184
+ const onData = (data) => {
185
+ const str = data.toString("utf-8");
186
+ if (str === "\x1b\r" || str === "\x1b\n") {
187
+ const before = inputRef.current.slice(0, cursorRef.current);
188
+ const after = inputRef.current.slice(cursorRef.current);
189
+ updateInput(before + "\n" + after, cursorRef.current + 1);
190
+ }
191
+ };
192
+ stdin.on("data", onData);
193
+ return () => { stdin.off("data", onData); };
194
+ }, [stdin]); // eslint-disable-line react-hooks/exhaustive-deps
174
195
  const tooltipTimerRef = (0, react_1.useRef)(null);
175
196
  (0, react_1.useEffect)(() => () => {
176
197
  if (tooltipTimerRef.current)
@@ -234,13 +255,20 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
234
255
  return;
235
256
  }
236
257
  if (key.return) {
237
- if (key.meta) {
238
- // Alt+Enter: insert newline
258
+ // Alt+Enter or Shift+Enter: insert newline
259
+ if (key.meta || key.shift) {
239
260
  const before = inputRef.current.slice(0, cursorRef.current);
240
261
  const after = inputRef.current.slice(cursorRef.current);
241
262
  updateInput(before + "\n" + after, cursorRef.current + 1);
242
263
  return;
243
264
  }
265
+ // Backslash+Enter: insert newline (like Claude Code)
266
+ if (cursorRef.current > 0 && inputRef.current[cursorRef.current - 1] === "\\") {
267
+ const before = inputRef.current.slice(0, cursorRef.current - 1);
268
+ const after = inputRef.current.slice(cursorRef.current);
269
+ updateInput(before + "\n" + after, cursorRef.current);
270
+ return;
271
+ }
244
272
  const text = inputRef.current;
245
273
  if (text.trim())
246
274
  onSubmit(text);
@@ -376,17 +404,15 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
376
404
  historyIdx.current = -1;
377
405
  }
378
406
  });
379
- // Get terminal width (capped for sanity)
380
- const cols = safeWidth();
381
407
  const isMultiline = input.includes("\n");
382
408
  const inputLines = input.split("\n");
383
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols + 2) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: ") " }), !input && queuedInputs.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "Press up to edit queued messages" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(0, cursorPos) }), cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input[cursorPos] ?? " " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(cursorPos + (cursorVisible ? 0 : 1)) })] }))] })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols + 2) }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", agentName, model ? ` · ${model}` : "", " \u00B7 /help"] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "opt+enter for newline" })] })] }));
409
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: ") " }), !input && queuedInputs.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "Press up to edit queued messages" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(0, cursorPos) }), cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input[cursorPos] ?? " " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(cursorPos + (cursorVisible ? 0 : 1)) })] }))] })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", agentName, model ? ` · ${model}` : "", " \u00B7 /help"] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "opt+enter for newline" })] })] }));
384
410
  }
385
411
  // ─── Main TUI Component ─────────────────────────────────────────────
386
412
  function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, }) {
387
413
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Static, { items: [{ id: "__header__" }, ...completedMessages], children: (item, index) => {
388
414
  if (index === 0) {
389
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 2, children: [(0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(Header, { agentName: agentName, model: model, contextPercent: contextPercent, cwd: cwd }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, dimColor: true, children: " Ctrl-C twice to exit \u00b7 \u2191\u2193 history \u00b7 Esc clear \u00b7 Alt+Enter newline" })] }, "header"));
415
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 2, children: [(0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(Header, { agentName: agentName, model: model, contextPercent: contextPercent, cwd: cwd }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, dimColor: true, children: " Ctrl-C twice to exit \u00b7 \u2191\u2193 history \u00b7 Esc clear \u00b7 opt+Enter newline" })] }, "header"));
390
416
  }
391
417
  return (0, jsx_runtime_1.jsx)(MessageBlock, { msg: item }, item.id);
392
418
  } }), (live.loading || live.streamingText || live.activeTool) ? (0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }) : null, (0, jsx_runtime_1.jsx)(LiveArea, { live: live, elapsed: elapsedSeconds }), (live.loading || live.streamingText || live.activeTool) ? (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }) : null, queuedInputs.length > 0 ? (0, jsx_runtime_1.jsx)(QueuedMessages, { items: queuedInputs }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(InputArea, { onSubmit: onSubmit, onCtrlC: onCtrlC, history: inputHistory, queuedInputs: queuedInputs, onPopQueue: onPopQueue, agentName: agentName, model: model })] }));
@@ -546,7 +546,7 @@ async function runCliSession(options) {
546
546
  }
547
547
  const msgCount = messages.filter(m => m.role === "user" || m.role === "assistant").length;
548
548
  const timeAgo = options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : null;
549
- const summary = ` resuming session (${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""})`;
549
+ const summary = ` resuming session · ${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""} · showing recent`;
550
550
  tuiStore.addSessionHistory(summary, lastExchanges);
551
551
  }
552
552
  /* v8 ignore stop */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.251",
3
+ "version": "0.1.0-alpha.253",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",