@ouro.bot/cli 0.1.0-alpha.260 → 0.1.0-alpha.262
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 +14 -2
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/input-keys.js +168 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/ouro-tui.js +180 -15
- package/dist/senses/cli/tui-store.js +4 -0
- package/dist/senses/cli.js +1 -0
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,11 +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.262",
|
|
6
|
+
"changes": [
|
|
7
|
+
"test(daemon): inject explicit `vi.mock(\"...heart/daemon/socket-client\", ...)` blocks into all 40 grandfathered test files in `TESTAGENT_NO_MOCK_ALLOWLIST`. The runtime guard in `socket-client.ts` already prevents real socket leaks under vitest, but the explicit mocks let those tests assert call counts cleanly and shrink the contract-test allowlist to zero. Both the testagent and the bundle-write allowlists in `test-isolation.contract.test.ts` are now empty Sets \u2014 any new offender fails the build."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.261",
|
|
12
|
+
"changes": [
|
|
13
|
+
"feat(tui): full input parity with Claude Code \u2014 kill ring, emacs nav, Home/End, Ctrl+D, forward delete, Esc history, bracketed paste, clipboard image, token deletion, chip navigation. 156 new tests."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.260",
|
|
6
18
|
"changes": [
|
|
7
|
-
"fix(daemon): MCP bridge empty-response bug for long-running commands. `sendDaemonCommand` and `checkDaemonSocketAlive` were calling `client.end()` immediately after writing, which half-closed the TCP connection. The daemon server's `net.createServer()` uses the default `allowHalfOpen: false`, so when it saw the client's FIN it auto-closed its own writable side
|
|
8
|
-
"fix(daemon): tighten pidfile trust
|
|
19
|
+
"fix(daemon): MCP bridge empty-response bug for long-running commands. `sendDaemonCommand` and `checkDaemonSocketAlive` were calling `client.end()` immediately after writing, which half-closed the TCP connection. The daemon server's `net.createServer()` uses the default `allowHalfOpen: false`, so when it saw the client's FIN it auto-closed its own writable side \u2014 and any response the server tried to write after processing a long-running command (like `agent.senseTurn`, which runs a full LLM turn) was dropped on the floor. Verified via direct socket repro: with `client.end()`, `agent.senseTurn` returned empty in ~149ms; without it, the same call returned a real response in ~5.8s. This was a silent regression of the fix in #303 (commit `253e4b1f` titled \"socket half-close fix\" actually *added* the half-close back). Fix: drop the `client.end()` calls from both sites AND set `allowHalfOpen: true` on the daemon's `net.createServer(...)` as defense-in-depth so future clients calling `end()` don't silently break again.",
|
|
20
|
+
"fix(daemon): tighten pidfile trust \u2014 `killOrphanProcesses` now verifies each pidfile PID is an actual orphan (PPID reparented to init/PID 1) before SIGTERMing it. Previously a polluted pidfile (written by a crashed daemon whose PIDs have since been reused by the OS for unrelated processes) could cause mass-kill of unrelated apps. New exported helper `filterPidfilePidsToActualOrphans` provides direct unit coverage.",
|
|
9
21
|
"fix(daemon, repertoire): rename six `_stop` nerves events to `_end` so they pair correctly with their `_start` counterparts under the nerves audit's start/end pairing rule. Affected: `daemon.thoughts_follow_stop`, `daemon.server_stop`, `daemon.update_checker_stop`, `daemon.mcp_server_stop`, `daemon.habit_scheduler_stop`, `mcp.manager_stop`. The naming was semantically fine (`stop` pairs with `start`), but the audit specifically looks for `_end`/`_error` suffixes. Was producing intermittent audit failures whenever any test run exercised these teardown paths (seen today as flakes on `daemon-socket-errors.test.ts` and `MarkdownStreamer` tests).",
|
|
10
22
|
"fix(providers): drop the harness-imposed MiniMax VLM timeout entirely. Previously the module defaulted to a 60-second AbortSignal; E2E validation of the BB image fix hit a real VLM request that took >60s to return (same bytes returned in 9.5s on the immediate retry). Raising to 120s would have been arbitrary too \u2014 the correct answer, per the same reasoning as PR #322 for LLM providers, is to not impose a harness ceiling at all. When `timeoutMs` isn't provided, `fetch()` now runs without an AbortSignal and undici's own defaults (headersTimeout + bodyTimeout, both 5 minutes) are the ceiling. Callers that want a tighter bound can still pass an explicit `timeoutMs`. The AbortError path is kept for that case, and the error message adapts to say \"underlying stack default\" when there was no harness-set value."
|
|
11
23
|
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BracketedPasteHandler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Bracketed paste mode handler for the TUI.
|
|
6
|
+
*
|
|
7
|
+
* Detects \x1b[200~ (paste start) and \x1b[201~ (paste end) markers in
|
|
8
|
+
* raw stdin data. Buffers paste content between markers and emits it as
|
|
9
|
+
* a single "paste" event. Non-paste data is emitted as "data" unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Works alongside Ink's stdin handling by intercepting raw data events.
|
|
12
|
+
*/
|
|
13
|
+
const events_1 = require("events");
|
|
14
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
15
|
+
const PASTE_START = "\x1b[200~";
|
|
16
|
+
const PASTE_END = "\x1b[201~";
|
|
17
|
+
class BracketedPasteHandler extends events_1.EventEmitter {
|
|
18
|
+
stdin;
|
|
19
|
+
stdout;
|
|
20
|
+
pasting = false;
|
|
21
|
+
buffer = "";
|
|
22
|
+
dataHandler;
|
|
23
|
+
constructor(stdin, stdout) {
|
|
24
|
+
super();
|
|
25
|
+
this.stdin = stdin;
|
|
26
|
+
this.stdout = stdout;
|
|
27
|
+
// Enable bracketed paste mode
|
|
28
|
+
stdout.write("\x1b[?2004h");
|
|
29
|
+
this.dataHandler = (chunk) => {
|
|
30
|
+
this.processChunk(chunk.toString());
|
|
31
|
+
};
|
|
32
|
+
stdin.prependListener("data", this.dataHandler);
|
|
33
|
+
}
|
|
34
|
+
processChunk(data) {
|
|
35
|
+
let remaining = data;
|
|
36
|
+
while (remaining.length > 0) {
|
|
37
|
+
if (this.pasting) {
|
|
38
|
+
// Look for end marker in buffer + remaining
|
|
39
|
+
const combined = this.buffer + remaining;
|
|
40
|
+
const endIdx = combined.indexOf(PASTE_END);
|
|
41
|
+
if (endIdx !== -1) {
|
|
42
|
+
// Found end marker -- emit paste content
|
|
43
|
+
const pasteContent = combined.slice(0, endIdx);
|
|
44
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.bracketed_paste", message: "bracketed paste received", meta: { length: pasteContent.length } });
|
|
45
|
+
this.emit("paste", pasteContent);
|
|
46
|
+
this.buffer = "";
|
|
47
|
+
this.pasting = false;
|
|
48
|
+
// Process remaining data after end marker
|
|
49
|
+
remaining = combined.slice(endIdx + PASTE_END.length);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// No end marker yet -- buffer everything so far
|
|
53
|
+
this.buffer = combined;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Not pasting -- look for start marker
|
|
57
|
+
const startIdx = remaining.indexOf(PASTE_START);
|
|
58
|
+
if (startIdx !== -1) {
|
|
59
|
+
// Emit any data before the start marker
|
|
60
|
+
const before = remaining.slice(0, startIdx);
|
|
61
|
+
if (before.length > 0) {
|
|
62
|
+
this.emit("data", before);
|
|
63
|
+
}
|
|
64
|
+
// Enter paste mode
|
|
65
|
+
this.pasting = true;
|
|
66
|
+
this.buffer = "";
|
|
67
|
+
remaining = remaining.slice(startIdx + PASTE_START.length);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// No start marker -- emit as regular data
|
|
71
|
+
this.emit("data", remaining);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Disable bracketed paste mode and remove listeners. */
|
|
76
|
+
destroy() {
|
|
77
|
+
this.stdout.write("\x1b[?2004l");
|
|
78
|
+
this.stdin.removeListener("data", this.dataHandler);
|
|
79
|
+
this.removeAllListeners();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.BracketedPasteHandler = BracketedPasteHandler;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.imageRefEndingAt = imageRefEndingAt;
|
|
4
|
+
exports.imageRefStartingAt = imageRefStartingAt;
|
|
5
|
+
exports.deleteTokenBefore = deleteTokenBefore;
|
|
6
|
+
exports.deleteTokenAfter = deleteTokenAfter;
|
|
7
|
+
/**
|
|
8
|
+
* Token-aware navigation and deletion for [Image #N] references.
|
|
9
|
+
*
|
|
10
|
+
* Image refs are treated as atomic "chips" — arrow keys hop over them,
|
|
11
|
+
* backspace/delete removes them atomically, and word-kill treats them
|
|
12
|
+
* as single words.
|
|
13
|
+
*/
|
|
14
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
15
|
+
const IMAGE_REF_PATTERN = /\[Image #\d+\]/g;
|
|
16
|
+
/**
|
|
17
|
+
* If an [Image #N] ref ends exactly at the given position, return its start index.
|
|
18
|
+
* Otherwise return undefined.
|
|
19
|
+
*/
|
|
20
|
+
function imageRefEndingAt(text, pos) {
|
|
21
|
+
// Search backward from pos for a potential image ref
|
|
22
|
+
// Max length of [Image #NNN] is ~14 chars, search a window
|
|
23
|
+
const windowStart = Math.max(0, pos - 20);
|
|
24
|
+
const window = text.slice(windowStart, pos);
|
|
25
|
+
IMAGE_REF_PATTERN.lastIndex = 0;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = IMAGE_REF_PATTERN.exec(window)) !== null) {
|
|
28
|
+
const matchEnd = windowStart + match.index + match[0].length;
|
|
29
|
+
if (matchEnd === pos) {
|
|
30
|
+
return windowStart + match.index;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* If an [Image #N] ref starts exactly at the given position, return its end index.
|
|
37
|
+
* Otherwise return undefined.
|
|
38
|
+
*/
|
|
39
|
+
function imageRefStartingAt(text, pos) {
|
|
40
|
+
if (pos >= text.length)
|
|
41
|
+
return undefined;
|
|
42
|
+
// Check if text at pos matches an image ref
|
|
43
|
+
const window = text.slice(pos, pos + 20);
|
|
44
|
+
IMAGE_REF_PATTERN.lastIndex = 0;
|
|
45
|
+
const match = IMAGE_REF_PATTERN.exec(window);
|
|
46
|
+
if (match && match.index === 0) {
|
|
47
|
+
return pos + match[0].length;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* If cursor is right after an [Image #N], remove the entire ref.
|
|
53
|
+
* Returns { text, pos } with the ref removed, or null if no ref found.
|
|
54
|
+
*/
|
|
55
|
+
function deleteTokenBefore(text, pos) {
|
|
56
|
+
const start = imageRefEndingAt(text, pos);
|
|
57
|
+
if (start === undefined)
|
|
58
|
+
return null;
|
|
59
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.image_ref_delete", message: "deleted image ref token", meta: { pos } });
|
|
60
|
+
const before = text.slice(0, start);
|
|
61
|
+
const after = text.slice(pos);
|
|
62
|
+
return { text: before + after, pos: start };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* If cursor is right before an [Image #N], remove the entire ref.
|
|
66
|
+
* Returns { text, pos } with the ref removed, or null if no ref found.
|
|
67
|
+
*/
|
|
68
|
+
function deleteTokenAfter(text, pos) {
|
|
69
|
+
const end = imageRefStartingAt(text, pos);
|
|
70
|
+
if (end === undefined)
|
|
71
|
+
return null;
|
|
72
|
+
const before = text.slice(0, pos);
|
|
73
|
+
const after = text.slice(end);
|
|
74
|
+
return { text: before + after, pos };
|
|
75
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleKillToEnd = handleKillToEnd;
|
|
4
|
+
exports.handleKillToStart = handleKillToStart;
|
|
5
|
+
exports.handleKillWordBack = handleKillWordBack;
|
|
6
|
+
exports.handleYank = handleYank;
|
|
7
|
+
exports.handleCursorLeft = handleCursorLeft;
|
|
8
|
+
exports.handleCursorRight = handleCursorRight;
|
|
9
|
+
exports.handleBackspace = handleBackspace;
|
|
10
|
+
exports.handleForwardDelete = handleForwardDelete;
|
|
11
|
+
exports.handleHome = handleHome;
|
|
12
|
+
exports.handleEnd = handleEnd;
|
|
13
|
+
exports.classifyEscapeSequence = classifyEscapeSequence;
|
|
14
|
+
exports.handleYankPop = handleYankPop;
|
|
15
|
+
exports.handleEmptyPaste = handleEmptyPaste;
|
|
16
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
17
|
+
/**
|
|
18
|
+
* Ctrl+K: kill from cursor to end of line.
|
|
19
|
+
* Pushes killed text to ring with "append" direction.
|
|
20
|
+
*/
|
|
21
|
+
function handleKillToEnd(text, cursorPos, ring) {
|
|
22
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.input_kill", message: "kill to end", meta: { direction: "end" } });
|
|
23
|
+
if (cursorPos >= text.length) {
|
|
24
|
+
return { text, cursorPos };
|
|
25
|
+
}
|
|
26
|
+
const killed = text.slice(cursorPos);
|
|
27
|
+
ring.push(killed, "append");
|
|
28
|
+
return { text: text.slice(0, cursorPos), cursorPos };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ctrl+U: kill from start of line to cursor.
|
|
32
|
+
* Pushes killed text to ring with "prepend" direction.
|
|
33
|
+
*/
|
|
34
|
+
function handleKillToStart(text, cursorPos, ring) {
|
|
35
|
+
if (cursorPos === 0) {
|
|
36
|
+
return { text, cursorPos };
|
|
37
|
+
}
|
|
38
|
+
const killed = text.slice(0, cursorPos);
|
|
39
|
+
ring.push(killed, "prepend");
|
|
40
|
+
return { text: text.slice(cursorPos), cursorPos: 0 };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ctrl+W / Meta+Backspace: kill word before cursor.
|
|
44
|
+
* Pushes killed word to ring with "prepend" direction.
|
|
45
|
+
*/
|
|
46
|
+
function handleKillWordBack(text, cursorPos, ring) {
|
|
47
|
+
if (cursorPos === 0) {
|
|
48
|
+
return { text, cursorPos };
|
|
49
|
+
}
|
|
50
|
+
const before = text.slice(0, cursorPos);
|
|
51
|
+
const after = text.slice(cursorPos);
|
|
52
|
+
const wordStart = before.replace(/\s*\S+\s*$/, "");
|
|
53
|
+
const killed = before.slice(wordStart.length);
|
|
54
|
+
ring.push(killed, "prepend");
|
|
55
|
+
return { text: wordStart + after, cursorPos: wordStart.length };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Ctrl+Y: yank (paste) from kill ring at cursor.
|
|
59
|
+
* Returns null if ring is empty.
|
|
60
|
+
*/
|
|
61
|
+
function handleYank(text, cursorPos, ring) {
|
|
62
|
+
const yanked = ring.yank();
|
|
63
|
+
if (yanked === undefined)
|
|
64
|
+
return null;
|
|
65
|
+
const before = text.slice(0, cursorPos);
|
|
66
|
+
const after = text.slice(cursorPos);
|
|
67
|
+
return { text: before + yanked + after, cursorPos: cursorPos + yanked.length };
|
|
68
|
+
}
|
|
69
|
+
// ─── Emacs Cursor Navigation ─────────────────────────────────────
|
|
70
|
+
/** Ctrl+B: move cursor left by 1. Returns new cursor position. */
|
|
71
|
+
function handleCursorLeft(_text, cursorPos) {
|
|
72
|
+
return Math.max(0, cursorPos - 1);
|
|
73
|
+
}
|
|
74
|
+
/** Ctrl+F: move cursor right by 1. Returns new cursor position. */
|
|
75
|
+
function handleCursorRight(text, cursorPos) {
|
|
76
|
+
return Math.min(text.length, cursorPos + 1);
|
|
77
|
+
}
|
|
78
|
+
/** Ctrl+H: backspace (delete char before cursor). */
|
|
79
|
+
function handleBackspace(text, cursorPos) {
|
|
80
|
+
if (cursorPos === 0)
|
|
81
|
+
return { text, cursorPos };
|
|
82
|
+
const before = text.slice(0, cursorPos - 1);
|
|
83
|
+
const after = text.slice(cursorPos);
|
|
84
|
+
return { text: before + after, cursorPos: cursorPos - 1 };
|
|
85
|
+
}
|
|
86
|
+
/** Forward delete: delete char at cursor position. */
|
|
87
|
+
function handleForwardDelete(text, cursorPos) {
|
|
88
|
+
if (cursorPos >= text.length)
|
|
89
|
+
return { text, cursorPos };
|
|
90
|
+
const before = text.slice(0, cursorPos);
|
|
91
|
+
const after = text.slice(cursorPos + 1);
|
|
92
|
+
return { text: before + after, cursorPos };
|
|
93
|
+
}
|
|
94
|
+
// ─── Home / End ──────────────────────────────────────────────────
|
|
95
|
+
/** Home key: move cursor to position 0. Returns new cursor position. */
|
|
96
|
+
function handleHome(_text, _cursorPos) {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
/** End key: move cursor to end of text. Returns new cursor position. */
|
|
100
|
+
function handleEnd(text, _cursorPos) {
|
|
101
|
+
return text.length;
|
|
102
|
+
}
|
|
103
|
+
// ─── Escape Sequence Classification ──────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Classify raw escape sequences that Ink passes through as regular characters.
|
|
106
|
+
* Returns:
|
|
107
|
+
* "home" — Home key sequences
|
|
108
|
+
* "end" — End key sequences
|
|
109
|
+
* "ignore" — PageUp, PageDown, mouse wheel (should be suppressed)
|
|
110
|
+
* null — not a recognized escape sequence
|
|
111
|
+
*/
|
|
112
|
+
function classifyEscapeSequence(inputChar) {
|
|
113
|
+
if (!inputChar.startsWith("\x1b["))
|
|
114
|
+
return null;
|
|
115
|
+
const seq = inputChar.slice(2); // after \x1b[
|
|
116
|
+
// Home: \x1b[H or \x1b[1~
|
|
117
|
+
if (seq === "H" || seq === "1~")
|
|
118
|
+
return "home";
|
|
119
|
+
// End: \x1b[F or \x1b[4~
|
|
120
|
+
if (seq === "F" || seq === "4~")
|
|
121
|
+
return "end";
|
|
122
|
+
// PageUp: \x1b[5~, PageDown: \x1b[6~
|
|
123
|
+
if (seq === "5~" || seq === "6~")
|
|
124
|
+
return "ignore";
|
|
125
|
+
// SGR mouse wheel: \x1b[<64;... or \x1b[<65;...
|
|
126
|
+
if (seq.startsWith("<64;") || seq.startsWith("<65;"))
|
|
127
|
+
return "ignore";
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Alt+Y: yank-pop -- replace previously yanked text with next ring entry.
|
|
132
|
+
* Returns null if not in yanking state or ring is empty.
|
|
133
|
+
*/
|
|
134
|
+
function handleYankPop(text, cursorPos, ring) {
|
|
135
|
+
const lastYanked = ring.lastYankedText;
|
|
136
|
+
if (!ring.isYanking || lastYanked === undefined)
|
|
137
|
+
return null;
|
|
138
|
+
const popped = ring.yankPop();
|
|
139
|
+
if (popped === undefined)
|
|
140
|
+
return null;
|
|
141
|
+
// Find where the last yanked text ends (at current cursor position)
|
|
142
|
+
// and starts (cursorPos - lastYanked.length)
|
|
143
|
+
const yankStart = cursorPos - lastYanked.length;
|
|
144
|
+
const before = text.slice(0, yankStart);
|
|
145
|
+
const after = text.slice(cursorPos);
|
|
146
|
+
return { text: before + popped + after, cursorPos: yankStart + popped.length };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Handle an empty paste event by checking clipboard for images (macOS only).
|
|
150
|
+
* Returns null if no image found, not on macOS, or clipboard empty.
|
|
151
|
+
*/
|
|
152
|
+
async function handleEmptyPaste(text, cursorPos, currentImageCount, platform, clipboardReader) {
|
|
153
|
+
if (platform !== "darwin")
|
|
154
|
+
return null;
|
|
155
|
+
const imageData = await clipboardReader();
|
|
156
|
+
if (!imageData)
|
|
157
|
+
return null;
|
|
158
|
+
const refNum = currentImageCount + 1;
|
|
159
|
+
const ref = `[Image #${refNum}]`;
|
|
160
|
+
const before = text.slice(0, cursorPos);
|
|
161
|
+
const after = text.slice(cursorPos);
|
|
162
|
+
return {
|
|
163
|
+
text: before + ref + after,
|
|
164
|
+
cursorPos: cursorPos + ref.length,
|
|
165
|
+
imageRef: refNum,
|
|
166
|
+
imageData,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KillRing = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Kill ring — emacs-style cut buffer with accumulation and yank-pop cycling.
|
|
6
|
+
*
|
|
7
|
+
* Stores up to MAX_SIZE killed text entries. Consecutive kills in the same
|
|
8
|
+
* direction accumulate into a single entry. Yank retrieves the most recent
|
|
9
|
+
* entry; yank-pop cycles backward through the ring.
|
|
10
|
+
*/
|
|
11
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
12
|
+
const MAX_SIZE = 10;
|
|
13
|
+
class KillRing {
|
|
14
|
+
entries = [];
|
|
15
|
+
accumulating = false;
|
|
16
|
+
yankIndex = -1;
|
|
17
|
+
_isYanking = false;
|
|
18
|
+
_lastYankedText = undefined;
|
|
19
|
+
/** Whether the ring is currently in yank-cycling mode. */
|
|
20
|
+
get isYanking() {
|
|
21
|
+
return this._isYanking;
|
|
22
|
+
}
|
|
23
|
+
/** The text that was last returned by yank() or yankPop(). */
|
|
24
|
+
get lastYankedText() {
|
|
25
|
+
return this._lastYankedText;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Push killed text onto the ring.
|
|
29
|
+
*
|
|
30
|
+
* If accumulating (consecutive kills without a reset), the text is merged
|
|
31
|
+
* with the top entry: "append" concatenates to the end, "prepend" prepends
|
|
32
|
+
* to the start. Otherwise a new entry is created, evicting the oldest if
|
|
33
|
+
* the ring is full.
|
|
34
|
+
*/
|
|
35
|
+
push(text, direction) {
|
|
36
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.kill_ring_push", message: "killed text to ring", meta: { direction, length: text.length } });
|
|
37
|
+
if (this.accumulating && this.entries.length > 0) {
|
|
38
|
+
const top = this.entries[this.entries.length - 1];
|
|
39
|
+
this.entries[this.entries.length - 1] =
|
|
40
|
+
direction === "append" ? top + text : text + top;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (this.entries.length >= MAX_SIZE) {
|
|
44
|
+
this.entries.shift();
|
|
45
|
+
}
|
|
46
|
+
this.entries.push(text);
|
|
47
|
+
}
|
|
48
|
+
this.accumulating = true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Return the most recent kill ring entry and enter yanking mode.
|
|
52
|
+
* Returns undefined if the ring is empty.
|
|
53
|
+
*/
|
|
54
|
+
yank() {
|
|
55
|
+
if (this.entries.length === 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
this.yankIndex = this.entries.length - 1;
|
|
58
|
+
this._isYanking = true;
|
|
59
|
+
this._lastYankedText = this.entries[this.yankIndex];
|
|
60
|
+
return this._lastYankedText;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cycle to the next older entry in the ring (wrapping around).
|
|
64
|
+
* Must be called after yank(). Returns undefined if ring is empty
|
|
65
|
+
* or not in yanking state.
|
|
66
|
+
*/
|
|
67
|
+
yankPop() {
|
|
68
|
+
if (!this._isYanking || this.entries.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
this.yankIndex =
|
|
71
|
+
(this.yankIndex - 1 + this.entries.length) % this.entries.length;
|
|
72
|
+
this._lastYankedText = this.entries[this.yankIndex];
|
|
73
|
+
return this._lastYankedText;
|
|
74
|
+
}
|
|
75
|
+
/** Stop consecutive-kill accumulation (called on non-kill keystrokes). */
|
|
76
|
+
resetAccumulation() {
|
|
77
|
+
this.accumulating = false;
|
|
78
|
+
}
|
|
79
|
+
/** Exit yank-cycling mode (called on non-yank keystrokes). */
|
|
80
|
+
resetYankState() {
|
|
81
|
+
this._isYanking = false;
|
|
82
|
+
this._lastYankedText = undefined;
|
|
83
|
+
this.yankIndex = -1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.KillRing = KillRing;
|
|
@@ -18,6 +18,9 @@ const react_1 = require("react");
|
|
|
18
18
|
const ink_1 = require("ink");
|
|
19
19
|
const streaming_markdown_1 = require("./streaming-markdown");
|
|
20
20
|
const image_paste_1 = require("./image-paste");
|
|
21
|
+
const kill_ring_1 = require("./kill-ring");
|
|
22
|
+
const input_keys_1 = require("./input-keys");
|
|
23
|
+
const image_ref_navigation_1 = require("./image-ref-navigation");
|
|
21
24
|
// ─── Ouroboros Brand Palette (ANSI RGB) ─────────────────────────────
|
|
22
25
|
// From packages/outlook-ui/src/style.css and ouroboros.bot
|
|
23
26
|
const OURO = {
|
|
@@ -153,8 +156,10 @@ function QueuedMessages({ items }) {
|
|
|
153
156
|
return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
|
|
154
157
|
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: items.map((text, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "\u231B queued: " }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.mist, children: ["\"", text, "\""] })] }, i))) }));
|
|
155
158
|
}
|
|
159
|
+
// ─── Kill Ring (session-scoped) ─────────────────────────────────────
|
|
160
|
+
const killRing = new kill_ring_1.KillRing();
|
|
156
161
|
// ─── Input ──────────────────────────────────────────────────────────
|
|
157
|
-
function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agentName, model, onImageMap }) {
|
|
162
|
+
function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agentName, model, onImageMap, onHistoryAdd }) {
|
|
158
163
|
const [input, setInput] = (0, react_1.useState)("");
|
|
159
164
|
const [cursorPos, setCursorPos] = (0, react_1.useState)(0); // cursor position within input
|
|
160
165
|
const [tooltip, setTooltip] = (0, react_1.useState)("");
|
|
@@ -219,6 +224,9 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
219
224
|
escTimerRef.current = setTimeout(() => {
|
|
220
225
|
escTimerRef.current = null;
|
|
221
226
|
if (inputRef.current) {
|
|
227
|
+
// Save to history before clearing (so Up arrow can retrieve it)
|
|
228
|
+
if (onHistoryAdd)
|
|
229
|
+
onHistoryAdd(inputRef.current);
|
|
222
230
|
updateInput("");
|
|
223
231
|
historyIdx.current = -1;
|
|
224
232
|
setTooltip("Esc again to clear");
|
|
@@ -242,6 +250,9 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
242
250
|
clearTimeout(escTimerRef.current);
|
|
243
251
|
escTimerRef.current = null;
|
|
244
252
|
}
|
|
253
|
+
// PageUp/PageDown: suppress (no text insertion, no action)
|
|
254
|
+
if (key.pageUp || key.pageDown)
|
|
255
|
+
return;
|
|
245
256
|
if (key.return) {
|
|
246
257
|
// Alt+Enter: detect via key.meta OR recent ESC (within 50ms — Ink splits \x1b\r)
|
|
247
258
|
const recentEsc = (Date.now() - lastEscTime.current) < 50;
|
|
@@ -271,19 +282,43 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
271
282
|
historyIdx.current = -1;
|
|
272
283
|
return;
|
|
273
284
|
}
|
|
274
|
-
|
|
285
|
+
// Forward delete key (fn+Backspace on macOS): delete character at cursor
|
|
286
|
+
if (key.delete && !key.backspace) {
|
|
287
|
+
if (key.meta) {
|
|
288
|
+
// Meta+Delete: kill to end of line (push to ring)
|
|
289
|
+
const result = (0, input_keys_1.handleKillToEnd)(inputRef.current, cursorRef.current, killRing);
|
|
290
|
+
updateInput(result.text, result.cursorPos);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Token-aware: check for image ref chip at cursor
|
|
294
|
+
const chip = (0, image_ref_navigation_1.deleteTokenAfter)(inputRef.current, cursorRef.current);
|
|
295
|
+
if (chip) {
|
|
296
|
+
updateInput(chip.text, chip.pos);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const result = (0, input_keys_1.handleForwardDelete)(inputRef.current, cursorRef.current);
|
|
300
|
+
updateInput(result.text, result.cursorPos);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
historyIdx.current = -1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Backspace: delete character before cursor
|
|
307
|
+
if (key.backspace) {
|
|
275
308
|
if (cursorRef.current > 0) {
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
309
|
+
// Token-aware: check for image ref chip before cursor
|
|
310
|
+
const chip = (0, image_ref_navigation_1.deleteTokenBefore)(inputRef.current, cursorRef.current);
|
|
311
|
+
if (chip) {
|
|
312
|
+
updateInput(chip.text, chip.pos);
|
|
313
|
+
}
|
|
314
|
+
else if (key.meta) {
|
|
315
|
+
// Option+Backspace: delete word (also pushes to kill ring)
|
|
316
|
+
const result = (0, input_keys_1.handleKillWordBack)(inputRef.current, cursorRef.current, killRing);
|
|
317
|
+
updateInput(result.text, result.cursorPos);
|
|
282
318
|
}
|
|
283
319
|
else {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
updateInput(before + after, cursorRef.current - 1);
|
|
320
|
+
const result = (0, input_keys_1.handleBackspace)(inputRef.current, cursorRef.current);
|
|
321
|
+
updateInput(result.text, result.cursorPos);
|
|
287
322
|
}
|
|
288
323
|
}
|
|
289
324
|
historyIdx.current = -1;
|
|
@@ -300,7 +335,9 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
300
335
|
setCursorPos(cursorRef.current);
|
|
301
336
|
}
|
|
302
337
|
else {
|
|
303
|
-
|
|
338
|
+
// Token-aware: hop over image ref chips
|
|
339
|
+
const chipStart = (0, image_ref_navigation_1.imageRefEndingAt)(inputRef.current, cursorRef.current);
|
|
340
|
+
cursorRef.current = chipStart !== undefined ? chipStart : Math.max(0, cursorRef.current - 1);
|
|
304
341
|
setCursorPos(cursorRef.current);
|
|
305
342
|
}
|
|
306
343
|
return;
|
|
@@ -315,7 +352,9 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
315
352
|
setCursorPos(cursorRef.current);
|
|
316
353
|
}
|
|
317
354
|
else {
|
|
318
|
-
|
|
355
|
+
// Token-aware: hop over image ref chips
|
|
356
|
+
const chipEnd = (0, image_ref_navigation_1.imageRefStartingAt)(inputRef.current, cursorRef.current);
|
|
357
|
+
cursorRef.current = chipEnd !== undefined ? chipEnd : Math.min(inputRef.current.length, cursorRef.current + 1);
|
|
319
358
|
setCursorPos(cursorRef.current);
|
|
320
359
|
}
|
|
321
360
|
return;
|
|
@@ -391,8 +430,134 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
391
430
|
setCursorPos(inputRef.current.length);
|
|
392
431
|
return;
|
|
393
432
|
}
|
|
433
|
+
// ─── Emacs Navigation ─────────────────────────────────────────
|
|
434
|
+
// Ctrl+B: cursor left
|
|
435
|
+
if (key.ctrl && inputChar === "b") {
|
|
436
|
+
cursorRef.current = (0, input_keys_1.handleCursorLeft)(inputRef.current, cursorRef.current);
|
|
437
|
+
setCursorPos(cursorRef.current);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Ctrl+F: cursor right
|
|
441
|
+
if (key.ctrl && inputChar === "f") {
|
|
442
|
+
cursorRef.current = (0, input_keys_1.handleCursorRight)(inputRef.current, cursorRef.current);
|
|
443
|
+
setCursorPos(cursorRef.current);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Ctrl+P: history up (same as up arrow)
|
|
447
|
+
if (key.ctrl && inputChar === "p") {
|
|
448
|
+
if (historyIdx.current === -1 && queuedInputs.length > 0) {
|
|
449
|
+
const items = onPopQueue();
|
|
450
|
+
updateInput(items.join("\n"));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (history.length > 0) {
|
|
454
|
+
if (historyIdx.current === -1) {
|
|
455
|
+
savedInput.current = inputRef.current;
|
|
456
|
+
historyIdx.current = history.length - 1;
|
|
457
|
+
}
|
|
458
|
+
else if (historyIdx.current > 0) {
|
|
459
|
+
historyIdx.current--;
|
|
460
|
+
}
|
|
461
|
+
updateInput(history[historyIdx.current]);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Ctrl+N: history down (same as down arrow)
|
|
466
|
+
if (key.ctrl && inputChar === "n") {
|
|
467
|
+
if (historyIdx.current >= 0) {
|
|
468
|
+
if (historyIdx.current < history.length - 1) {
|
|
469
|
+
historyIdx.current++;
|
|
470
|
+
updateInput(history[historyIdx.current]);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
historyIdx.current = -1;
|
|
474
|
+
updateInput(savedInput.current);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// Ctrl+H: token-aware backspace
|
|
480
|
+
if (key.ctrl && inputChar === "h") {
|
|
481
|
+
const chip = (0, image_ref_navigation_1.deleteTokenBefore)(inputRef.current, cursorRef.current);
|
|
482
|
+
if (chip) {
|
|
483
|
+
updateInput(chip.text, chip.pos);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const result = (0, input_keys_1.handleBackspace)(inputRef.current, cursorRef.current);
|
|
487
|
+
updateInput(result.text, result.cursorPos);
|
|
488
|
+
}
|
|
489
|
+
historyIdx.current = -1;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Ctrl+D: forward-delete when input present, exit when empty
|
|
493
|
+
if (key.ctrl && inputChar === "d") {
|
|
494
|
+
if (inputRef.current.length === 0) {
|
|
495
|
+
handleCtrlC();
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
const result = (0, input_keys_1.handleForwardDelete)(inputRef.current, cursorRef.current);
|
|
499
|
+
updateInput(result.text, result.cursorPos);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
// ─── Kill Ring Keybindings ─────────────────────────────────────
|
|
504
|
+
// Ctrl+K: kill from cursor to end of line
|
|
505
|
+
if (key.ctrl && inputChar === "k") {
|
|
506
|
+
const result = (0, input_keys_1.handleKillToEnd)(inputRef.current, cursorRef.current, killRing);
|
|
507
|
+
updateInput(result.text, result.cursorPos);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Ctrl+U: kill from start to cursor
|
|
511
|
+
if (key.ctrl && inputChar === "u") {
|
|
512
|
+
const result = (0, input_keys_1.handleKillToStart)(inputRef.current, cursorRef.current, killRing);
|
|
513
|
+
updateInput(result.text, result.cursorPos);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// Ctrl+W: kill word before cursor (token-aware)
|
|
517
|
+
if (key.ctrl && inputChar === "w") {
|
|
518
|
+
const chip = (0, image_ref_navigation_1.deleteTokenBefore)(inputRef.current, cursorRef.current);
|
|
519
|
+
if (chip) {
|
|
520
|
+
updateInput(chip.text, chip.pos);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
const result = (0, input_keys_1.handleKillWordBack)(inputRef.current, cursorRef.current, killRing);
|
|
524
|
+
updateInput(result.text, result.cursorPos);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Ctrl+Y: yank from kill ring
|
|
529
|
+
if (key.ctrl && inputChar === "y") {
|
|
530
|
+
const result = (0, input_keys_1.handleYank)(inputRef.current, cursorRef.current, killRing);
|
|
531
|
+
if (result)
|
|
532
|
+
updateInput(result.text, result.cursorPos);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Alt+Y: yank-pop (cycle kill ring)
|
|
536
|
+
if (key.meta && inputChar === "y") {
|
|
537
|
+
const result = (0, input_keys_1.handleYankPop)(inputRef.current, cursorRef.current, killRing);
|
|
538
|
+
if (result)
|
|
539
|
+
updateInput(result.text, result.cursorPos);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// ─── Non-kill/non-yank keystroke resets ────────────────────────
|
|
543
|
+
killRing.resetAccumulation();
|
|
544
|
+
killRing.resetYankState();
|
|
394
545
|
// Regular character: insert at cursor position
|
|
395
546
|
if (!key.ctrl && !key.meta && inputChar) {
|
|
547
|
+
// Detect raw escape sequences before inserting text
|
|
548
|
+
const escClass = (0, input_keys_1.classifyEscapeSequence)(inputChar);
|
|
549
|
+
if (escClass === "home") {
|
|
550
|
+
cursorRef.current = (0, input_keys_1.handleHome)(inputRef.current, cursorRef.current);
|
|
551
|
+
setCursorPos(cursorRef.current);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (escClass === "end") {
|
|
555
|
+
cursorRef.current = (0, input_keys_1.handleEnd)(inputRef.current, cursorRef.current);
|
|
556
|
+
setCursorPos(cursorRef.current);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (escClass === "ignore")
|
|
560
|
+
return; // PageUp/PageDown/mouse wheel fallback
|
|
396
561
|
const before = inputRef.current.slice(0, cursorRef.current);
|
|
397
562
|
const after = inputRef.current.slice(cursorRef.current);
|
|
398
563
|
updateInput(before + inputChar + after, cursorRef.current + 1);
|
|
@@ -406,11 +571,11 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
406
571
|
: (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 + 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" })] })] }));
|
|
407
572
|
}
|
|
408
573
|
// ─── Main TUI Component ─────────────────────────────────────────────
|
|
409
|
-
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, resumeInfo, onImageMap, }) {
|
|
574
|
+
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, resumeInfo, onImageMap, onHistoryAdd, }) {
|
|
410
575
|
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) => {
|
|
411
576
|
if (index === 0) {
|
|
412
577
|
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, resumeInfo: resumeInfo }), (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"));
|
|
413
578
|
}
|
|
414
579
|
return (0, jsx_runtime_1.jsx)(MessageBlock, { msg: item }, item.id);
|
|
415
|
-
} }), (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, onImageMap: onImageMap })] }));
|
|
580
|
+
} }), (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, onImageMap: onImageMap, onHistoryAdd: onHistoryAdd })] }));
|
|
416
581
|
}
|
|
@@ -169,6 +169,10 @@ class TuiStore {
|
|
|
169
169
|
this._inputHistory.push(text);
|
|
170
170
|
this.notify();
|
|
171
171
|
}
|
|
172
|
+
/** Add text to input history only (no completed message, no notify). Used by ESC-to-clear. */
|
|
173
|
+
addToHistoryOnly(text) {
|
|
174
|
+
this._inputHistory.push(text);
|
|
175
|
+
}
|
|
172
176
|
/** Seed input history from previous session messages (display only, no rendering) */
|
|
173
177
|
seedHistory(texts) {
|
|
174
178
|
this._inputHistory.push(...texts);
|
package/dist/senses/cli.js
CHANGED
|
@@ -618,6 +618,7 @@ async function runCliSession(options) {
|
|
|
618
618
|
cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
|
|
619
619
|
resumeInfo,
|
|
620
620
|
onImageMap: (images) => { pendingImages = images; },
|
|
621
|
+
onHistoryAdd: (text) => { storeRef.addToHistoryOnly(text); },
|
|
621
622
|
});
|
|
622
623
|
}
|
|
623
624
|
inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
|