@ouro.bot/cli 0.1.0-alpha.180 → 0.1.0-alpha.181

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,6 +1,15 @@
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.181",
6
+ "changes": [
7
+ "feat(cli): Option+Arrow word jumping, Ctrl+A/E home/end, cursor positioning",
8
+ "feat(cli): never suppress input \u2014 type while agent responds (TTFA)",
9
+ "fix(cli): blockquote continuation lines keep \u2502 prefix on wrap",
10
+ "feat(cli): Option+Backspace to delete word"
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.180",
6
15
  "changes": [
@@ -142,13 +142,22 @@ function LiveArea({ live, elapsed }) {
142
142
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [live.streamingText ? ((0, jsx_runtime_1.jsx)(streaming_markdown_1.StreamingMarkdown, { text: live.streamingText, maxWidth: safeWidth() })) : null, live.activeTool ? ((0, jsx_runtime_1.jsx)(ActiveToolLine, { tool: live.activeTool })) : null, live.loading ? ((0, jsx_runtime_1.jsx)(Spinner, { phrasePool: live.spinnerPhrasePool, elapsed: elapsed })) : null, live.errorMessage ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.fang, children: ["✗ ", live.errorMessage] })) : null, live.kickMessage ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.gold, children: ["↻ ", live.kickMessage] })) : null] }));
143
143
  }
144
144
  // ─── Input ──────────────────────────────────────────────────────────
145
- function InputArea({ onSubmit, suppressed, onCtrlC, agentName, model, history }) {
145
+ function InputArea({ onSubmit, onCtrlC, agentName, model, history }) {
146
146
  const [input, setInput] = (0, react_1.useState)("");
147
+ const [cursorPos, setCursorPos] = (0, react_1.useState)(0); // cursor position within input
147
148
  const [tooltip, setTooltip] = (0, react_1.useState)("");
148
149
  const [cursorVisible, setCursorVisible] = (0, react_1.useState)(true);
149
150
  const inputRef = (0, react_1.useRef)("");
150
- const historyIdx = (0, react_1.useRef)(-1); // -1 = not browsing history
151
- const savedInput = (0, react_1.useRef)(""); // saves current input when entering history
151
+ const cursorRef = (0, react_1.useRef)(0);
152
+ const historyIdx = (0, react_1.useRef)(-1);
153
+ const savedInput = (0, react_1.useRef)("");
154
+ // Helper: update input and cursor together
155
+ const updateInput = (text, pos) => {
156
+ inputRef.current = text;
157
+ cursorRef.current = pos ?? text.length;
158
+ setInput(text);
159
+ setCursorPos(cursorRef.current);
160
+ };
152
161
  // Blinking cursor
153
162
  (0, react_1.useEffect)(() => {
154
163
  const iv = setInterval(() => setCursorVisible(v => !v), 530);
@@ -174,14 +183,12 @@ function InputArea({ onSubmit, suppressed, onCtrlC, agentName, model, history })
174
183
  handleCtrlC();
175
184
  return;
176
185
  }
177
- if (suppressed)
178
- return;
186
+ // Input is NEVER blocked — user can type while agent responds (TTFA)
179
187
  // Any non-Ctrl-C input clears tooltip
180
188
  setTooltip("");
181
189
  if (key.escape) {
182
190
  if (inputRef.current) {
183
- inputRef.current = "";
184
- setInput("");
191
+ updateInput("");
185
192
  historyIdx.current = -1;
186
193
  setTooltip("Esc again to clear");
187
194
  setTimeout(() => setTooltip(""), 2000);
@@ -192,71 +199,118 @@ function InputArea({ onSubmit, suppressed, onCtrlC, agentName, model, history })
192
199
  return;
193
200
  }
194
201
  if (key.return) {
195
- // Alt+Enter: insert newline instead of submitting
196
202
  if (key.meta) {
197
- inputRef.current += "\n";
198
- setInput(inputRef.current);
203
+ // Alt+Enter: insert newline
204
+ const before = inputRef.current.slice(0, cursorRef.current);
205
+ const after = inputRef.current.slice(cursorRef.current);
206
+ updateInput(before + "\n" + after, cursorRef.current + 1);
199
207
  return;
200
208
  }
201
209
  const text = inputRef.current;
202
210
  if (text.trim())
203
211
  onSubmit(text);
204
- inputRef.current = "";
205
- setInput("");
212
+ updateInput("");
206
213
  historyIdx.current = -1;
207
214
  return;
208
215
  }
209
216
  if (key.backspace || key.delete) {
210
- inputRef.current = inputRef.current.slice(0, -1);
211
- setInput(inputRef.current);
217
+ if (cursorRef.current > 0) {
218
+ // Option+Backspace: delete word
219
+ if (key.meta) {
220
+ const before = inputRef.current.slice(0, cursorRef.current);
221
+ const after = inputRef.current.slice(cursorRef.current);
222
+ const wordStart = before.replace(/\s*\S+\s*$/, "");
223
+ updateInput(wordStart + after, wordStart.length);
224
+ }
225
+ else {
226
+ const before = inputRef.current.slice(0, cursorRef.current - 1);
227
+ const after = inputRef.current.slice(cursorRef.current);
228
+ updateInput(before + after, cursorRef.current - 1);
229
+ }
230
+ }
212
231
  historyIdx.current = -1;
213
232
  return;
214
233
  }
215
- // Up arrow: browse history (newest first)
234
+ // Left/right arrow: move cursor
235
+ if (key.leftArrow) {
236
+ if (key.meta) {
237
+ // Option+Left: jump to previous word boundary
238
+ const before = inputRef.current.slice(0, cursorRef.current);
239
+ const match = before.match(/(?:^|\s)\S+\s*$/);
240
+ const newPos = match ? cursorRef.current - match[0].length + (match[0].startsWith(" ") ? 1 : 0) : 0;
241
+ cursorRef.current = Math.max(0, newPos);
242
+ setCursorPos(cursorRef.current);
243
+ }
244
+ else {
245
+ cursorRef.current = Math.max(0, cursorRef.current - 1);
246
+ setCursorPos(cursorRef.current);
247
+ }
248
+ return;
249
+ }
250
+ if (key.rightArrow) {
251
+ if (key.meta) {
252
+ // Option+Right: jump to next word boundary
253
+ const after = inputRef.current.slice(cursorRef.current);
254
+ const match = after.match(/^\s*\S+/);
255
+ const newPos = match ? cursorRef.current + match[0].length : inputRef.current.length;
256
+ cursorRef.current = Math.min(inputRef.current.length, newPos);
257
+ setCursorPos(cursorRef.current);
258
+ }
259
+ else {
260
+ cursorRef.current = Math.min(inputRef.current.length, cursorRef.current + 1);
261
+ setCursorPos(cursorRef.current);
262
+ }
263
+ return;
264
+ }
265
+ // Up/Down: history
216
266
  if (key.upArrow && history.length > 0) {
217
267
  if (historyIdx.current === -1) {
218
- // Entering history — save current input
219
268
  savedInput.current = inputRef.current;
220
269
  historyIdx.current = history.length - 1;
221
270
  }
222
271
  else if (historyIdx.current > 0) {
223
272
  historyIdx.current--;
224
273
  }
225
- inputRef.current = history[historyIdx.current];
226
- setInput(inputRef.current);
274
+ updateInput(history[historyIdx.current]);
227
275
  return;
228
276
  }
229
- // Down arrow: browse forward or restore saved input
230
277
  if (key.downArrow) {
231
278
  if (historyIdx.current >= 0) {
232
279
  if (historyIdx.current < history.length - 1) {
233
280
  historyIdx.current++;
234
- inputRef.current = history[historyIdx.current];
281
+ updateInput(history[historyIdx.current]);
235
282
  }
236
283
  else {
237
- // Past end of history — restore saved input
238
284
  historyIdx.current = -1;
239
- inputRef.current = savedInput.current;
285
+ updateInput(savedInput.current);
240
286
  }
241
- setInput(inputRef.current);
242
287
  }
243
288
  return;
244
289
  }
290
+ // Ctrl+A / Ctrl+E: home / end
291
+ if (key.ctrl && inputChar === "a") {
292
+ cursorRef.current = 0;
293
+ setCursorPos(0);
294
+ return;
295
+ }
296
+ if (key.ctrl && inputChar === "e") {
297
+ cursorRef.current = inputRef.current.length;
298
+ setCursorPos(inputRef.current.length);
299
+ return;
300
+ }
301
+ // Regular character: insert at cursor position
245
302
  if (!key.ctrl && !key.meta && inputChar) {
246
- inputRef.current += inputChar;
247
- setInput(inputRef.current);
248
- historyIdx.current = -1; // typing resets history browsing
303
+ const before = inputRef.current.slice(0, cursorRef.current);
304
+ const after = inputRef.current.slice(cursorRef.current);
305
+ updateInput(before + inputChar + after, cursorRef.current + 1);
306
+ historyIdx.current = -1;
249
307
  }
250
308
  });
251
309
  // Get terminal width (capped for sanity)
252
310
  const cols = safeWidth();
253
- if (suppressed) {
254
- // During model generation: show status bar but no input
255
- 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) }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [agentName, model ? ` · ${model}` : ""] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols) })] }));
256
- }
257
311
  const isMultiline = input.includes("\n");
258
312
  const inputLines = input.split("\n");
259
- 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) }), 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: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input }), cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })] })), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [agentName, model ? ` · ${model}` : ""] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : null, isMultiline ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: " (multi-line)" }) : null] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols) })] }));
313
+ 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) }), 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: ") " }), (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.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [agentName, model ? ` · ${model}` : ""] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : null, isMultiline ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: " (multi-line)" }) : null] }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols) })] }));
260
314
  }
261
315
  // ─── Main TUI Component ─────────────────────────────────────────────
262
316
  function OuroTui({ agentName, model, completedMessages, inputHistory, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, cwd, }) {
@@ -265,5 +319,5 @@ function OuroTui({ agentName, model, completedMessages, inputHistory, live, elap
265
319
  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"));
266
320
  }
267
321
  return (0, jsx_runtime_1.jsx)(MessageBlock, { msg: item }, item.id);
268
- } }), (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, (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, suppressed: live.inputSuppressed, onCtrlC: onCtrlC, agentName: agentName, model: model, history: inputHistory })] }));
322
+ } }), (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, (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, agentName: agentName, model: model, history: inputHistory })] }));
269
323
  }
@@ -453,7 +453,7 @@ function TableRenderer({ lines }) {
453
453
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [renderRow(headers, true), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: separatorRow }), rows.map((row, i) => ((0, jsx_runtime_1.jsx)(react_1.default.Fragment, { children: renderRow(row, false) }, i)))] }));
454
454
  }
455
455
  // ─── Block Renderer ────────────────────────────────────────────────
456
- function BlockRenderer({ block, isFirst, isLast }) {
456
+ function BlockRenderer({ block, isFirst, isLast, maxWidth }) {
457
457
  switch (block.type) {
458
458
  case "blank":
459
459
  return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
@@ -476,7 +476,29 @@ function BlockRenderer({ block, isFirst, isLast }) {
476
476
  case "blockquote": {
477
477
  const depth = block.level ?? 1;
478
478
  const barPrefix = "\u2502 ".repeat(depth);
479
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), block.lines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: barPrefix }), (0, jsx_runtime_1.jsx)(ink_1.Text, { italic: true, children: (0, jsx_runtime_1.jsx)(InlineLine, { text: line }) })] }, i))), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
479
+ const barLen = barPrefix.length;
480
+ const wrapWidth = Math.max(40, (maxWidth || 80) - barLen);
481
+ // Pre-wrap each line so continuation lines also get the bar prefix
482
+ const wrappedLines = [];
483
+ for (const line of block.lines) {
484
+ if (line.length <= wrapWidth) {
485
+ wrappedLines.push(line);
486
+ }
487
+ else {
488
+ // Simple word wrap
489
+ let remaining = line;
490
+ while (remaining.length > wrapWidth) {
491
+ let breakAt = remaining.lastIndexOf(" ", wrapWidth);
492
+ if (breakAt <= 0)
493
+ breakAt = wrapWidth;
494
+ wrappedLines.push(remaining.slice(0, breakAt));
495
+ remaining = remaining.slice(breakAt).trimStart();
496
+ }
497
+ if (remaining)
498
+ wrappedLines.push(remaining);
499
+ }
500
+ }
501
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [!isFirst && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }), wrappedLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: barPrefix }), (0, jsx_runtime_1.jsx)(ink_1.Text, { italic: true, children: (0, jsx_runtime_1.jsx)(InlineLine, { text: line }) })] }, i))), !isLast && (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" })] }));
480
502
  }
481
503
  case "paragraph": {
482
504
  // Check for task list items and render them specially
@@ -499,5 +521,5 @@ function StreamingMarkdown({ text, maxWidth }) {
499
521
  if (blocks.length === 0) {
500
522
  return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
501
523
  }
502
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: blocks.map((block, i) => ((0, jsx_runtime_1.jsx)(BlockRenderer, { block: block, isFirst: i === 0, isLast: i === blocks.length - 1 }, i))) }));
524
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: blocks.map((block, i) => ((0, jsx_runtime_1.jsx)(BlockRenderer, { block: block, isFirst: i === 0, isLast: i === blocks.length - 1, maxWidth: maxWidth }, i))) }));
503
525
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.180",
3
+ "version": "0.1.0-alpha.181",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",