@ouro.bot/cli 0.1.0-alpha.254 → 0.1.0-alpha.256
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 +16 -0
- package/dist/senses/cli/image-paste.js +162 -0
- package/dist/senses/cli/ouro-tui.js +26 -41
- package/dist/senses/cli/tui-store.js +11 -11
- package/dist/senses/cli.js +29 -11
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
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.256",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(tui): cursor renders as inverse character (not inserted block) \u2014 matches standard terminal cursor behavior.",
|
|
8
|
+
"fix(tui): Alt+Enter via ESC-timing (50ms window) instead of raw stdin handler that interfered with arrow keys.",
|
|
9
|
+
"fix(tui): image path regex now matches backslash-escaped spaces from macOS drag-drop."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.255",
|
|
14
|
+
"changes": [
|
|
15
|
+
"feat(tui): session resume shows last messages as regular chat (no dimmed preview) with teal resume banner in header.",
|
|
16
|
+
"feat(tui): image/file drag-and-drop \u2014 detects image paths in pasted text, reads to base64, inserts [Image #N] references, sends as image_url content blocks to model.",
|
|
17
|
+
"refactor(tui): removed custom history-* roles and addSessionHistory (KISS/DRY)."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.254",
|
|
6
22
|
"changes": [
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.IMAGE_EXTENSION_REGEX = void 0;
|
|
37
|
+
exports.isImagePath = isImagePath;
|
|
38
|
+
exports.unescapePath = unescapePath;
|
|
39
|
+
exports.tryReadImage = tryReadImage;
|
|
40
|
+
exports.formatImageRef = formatImageRef;
|
|
41
|
+
exports.replacePathsWithRefs = replacePathsWithRefs;
|
|
42
|
+
exports.processSubmitInput = processSubmitInput;
|
|
43
|
+
exports.resolveImageContent = resolveImageContent;
|
|
44
|
+
/**
|
|
45
|
+
* Image paste detection and resolution for the TUI.
|
|
46
|
+
*
|
|
47
|
+
* When a user drags an image file onto the terminal, macOS pastes the absolute
|
|
48
|
+
* path (often with backslash-escaped spaces). This module detects image paths
|
|
49
|
+
* in the input text, replaces them with `[Image #N]` references, reads the
|
|
50
|
+
* files, and produces OpenAI-compatible `image_url` content blocks.
|
|
51
|
+
*/
|
|
52
|
+
const promises_1 = require("fs/promises");
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
55
|
+
/** Matches image file extensions (case-insensitive) */
|
|
56
|
+
exports.IMAGE_EXTENSION_REGEX = /\.(png|jpe?g|gif|webp)$/i;
|
|
57
|
+
/** Check whether a string (possibly backslash-escaped) ends with an image extension */
|
|
58
|
+
function isImagePath(str) {
|
|
59
|
+
return exports.IMAGE_EXTENSION_REGEX.test(unescapePath(str));
|
|
60
|
+
}
|
|
61
|
+
/** Convert backslash-escaped spaces to plain spaces (macOS drag-drop format) */
|
|
62
|
+
function unescapePath(str) {
|
|
63
|
+
return str.replace(/\\ /g, " ");
|
|
64
|
+
}
|
|
65
|
+
/** Map file extension to MIME media type */
|
|
66
|
+
function extensionToMediaType(ext) {
|
|
67
|
+
switch (ext.toLowerCase()) {
|
|
68
|
+
case ".jpg":
|
|
69
|
+
case ".jpeg":
|
|
70
|
+
return "image/jpeg";
|
|
71
|
+
case ".png":
|
|
72
|
+
return "image/png";
|
|
73
|
+
case ".gif":
|
|
74
|
+
return "image/gif";
|
|
75
|
+
case ".webp":
|
|
76
|
+
return "image/webp";
|
|
77
|
+
default:
|
|
78
|
+
return "image/png";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Read an image file and return base64 + media type.
|
|
83
|
+
* Returns null on any error (file not found, permission denied, etc.)
|
|
84
|
+
*/
|
|
85
|
+
async function tryReadImage(absolutePath) {
|
|
86
|
+
try {
|
|
87
|
+
const buffer = await (0, promises_1.readFile)(absolutePath);
|
|
88
|
+
const ext = path.extname(absolutePath);
|
|
89
|
+
return {
|
|
90
|
+
base64: Buffer.from(buffer).toString("base64"),
|
|
91
|
+
mediaType: extensionToMediaType(ext),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
(0, runtime_1.emitNervesEvent)({
|
|
96
|
+
component: "senses",
|
|
97
|
+
event: "senses.image_read_error",
|
|
98
|
+
message: `Failed to read image: ${absolutePath}`,
|
|
99
|
+
meta: { path: absolutePath },
|
|
100
|
+
});
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Format an image reference placeholder */
|
|
105
|
+
function formatImageRef(n) {
|
|
106
|
+
return `[Image #${n}]`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Scan input for absolute path tokens ending with image extensions.
|
|
110
|
+
* Matches `/` followed by non-whitespace chars (allowing `\ ` for escaped spaces)
|
|
111
|
+
* ending with an image extension.
|
|
112
|
+
*/
|
|
113
|
+
function replacePathsWithRefs(input) {
|
|
114
|
+
const images = new Map();
|
|
115
|
+
// Match absolute paths starting with / that end with an image extension.
|
|
116
|
+
// Allow backslash-escaped spaces (like `\ ` from macOS drag-drop).
|
|
117
|
+
const pathRegex = /(\/(?:[^\s]|\\ )+\.(?:png|jpe?g|gif|webp))/gi;
|
|
118
|
+
let counter = 0;
|
|
119
|
+
const text = input.replace(pathRegex, (match) => {
|
|
120
|
+
counter++;
|
|
121
|
+
images.set(counter, unescapePath(match));
|
|
122
|
+
return formatImageRef(counter);
|
|
123
|
+
});
|
|
124
|
+
return { text, images };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Process submit input: detect image paths, replace with refs, return map.
|
|
128
|
+
* This is the entry point called by InputArea on submit.
|
|
129
|
+
*/
|
|
130
|
+
function processSubmitInput(text) {
|
|
131
|
+
return replacePathsWithRefs(text);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build OpenAI content parts from text + image map.
|
|
135
|
+
* For each image in the map, reads the file and creates an `image_url` content part.
|
|
136
|
+
* Images that fail to read are silently skipped.
|
|
137
|
+
* Returns `[{ type: "text", text }]` when no images resolve.
|
|
138
|
+
*/
|
|
139
|
+
async function resolveImageContent(text, images) {
|
|
140
|
+
if (images.size === 0) {
|
|
141
|
+
return [{ type: "text", text }];
|
|
142
|
+
}
|
|
143
|
+
const parts = [];
|
|
144
|
+
// Read all images in parallel
|
|
145
|
+
const entries = Array.from(images.entries());
|
|
146
|
+
const results = await Promise.all(entries.map(async ([, absolutePath]) => tryReadImage(absolutePath)));
|
|
147
|
+
for (const result of results) {
|
|
148
|
+
if (result) {
|
|
149
|
+
parts.push({
|
|
150
|
+
type: "image_url",
|
|
151
|
+
image_url: { url: `data:${result.mediaType};base64,${result.base64}` },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Always include the text part
|
|
156
|
+
parts.push({ type: "text", text });
|
|
157
|
+
// If no images resolved, return just text
|
|
158
|
+
if (parts.length === 1) {
|
|
159
|
+
return [{ type: "text", text }];
|
|
160
|
+
}
|
|
161
|
+
return parts;
|
|
162
|
+
}
|
|
@@ -17,6 +17,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
17
17
|
const react_1 = require("react");
|
|
18
18
|
const ink_1 = require("ink");
|
|
19
19
|
const streaming_markdown_1 = require("./streaming-markdown");
|
|
20
|
+
const image_paste_1 = require("./image-paste");
|
|
20
21
|
// ─── Ouroboros Brand Palette (ANSI RGB) ─────────────────────────────
|
|
21
22
|
// From packages/outlook-ui/src/style.css and ouroboros.bot
|
|
22
23
|
const OURO = {
|
|
@@ -49,7 +50,7 @@ function safeWidth() {
|
|
|
49
50
|
function termWidth() {
|
|
50
51
|
return process.stdout.columns || 80;
|
|
51
52
|
}
|
|
52
|
-
function Header({ agentName, model, contextPercent, cwd }) {
|
|
53
|
+
function Header({ agentName, model, contextPercent, cwd, resumeInfo }) {
|
|
53
54
|
const showCtx = contextPercent > 0;
|
|
54
55
|
const info = [agentName, model, cwd, showCtx ? `ctx ${contextPercent}%` : ""].filter(Boolean).join(" · ");
|
|
55
56
|
// 3-segment snake: TAIL (fixed) + MIDDLE (stretches) + HEAD (fixed)
|
|
@@ -66,7 +67,7 @@ function Header({ agentName, model, contextPercent, cwd }) {
|
|
|
66
67
|
const line1 = TAIL1 + " ".repeat(middleLen) + HEAD1;
|
|
67
68
|
const line2 = TAIL2 + "_".repeat(middleLen) + HEAD2;
|
|
68
69
|
const line3text = (info.length > middleLen ? info.slice(0, middleLen) : info) + "_".repeat(textPad);
|
|
69
|
-
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: line1 }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: line2 }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.scale, children: [TAIL3, (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.glow, children: line3text }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: HEAD3 })] })] }));
|
|
70
|
+
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: line1 }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: line2 }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.scale, children: [TAIL3, (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.glow, children: line3text }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: HEAD3 })] }), resumeInfo ? ((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: OURO.teal, children: [" resuming \u00b7 ", resumeInfo.messageCount, " messages \u00b7 last active ", resumeInfo.timeAgo] })) : null] }));
|
|
70
71
|
}
|
|
71
72
|
// ─── Message Rendering ──────────────────────────────────────────────
|
|
72
73
|
// Flow control tools are invisible to the user — they are internal agent mechanics
|
|
@@ -85,19 +86,6 @@ function MessageBlock({ msg }) {
|
|
|
85
86
|
return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
|
|
86
87
|
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)) }));
|
|
87
88
|
}
|
|
88
|
-
// ── Session history (dimmed recap of recent exchanges) ──
|
|
89
|
-
if (msg.role === "history-summary") {
|
|
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 }) }));
|
|
91
|
-
}
|
|
92
|
-
if (msg.role === "history-user") {
|
|
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: "" }) })] }));
|
|
94
|
-
}
|
|
95
|
-
if (msg.role === "history-assistant") {
|
|
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: "" }) })] }));
|
|
97
|
-
}
|
|
98
|
-
if (msg.role === "history-end") {
|
|
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()) }) }));
|
|
100
|
-
}
|
|
101
89
|
if (msg.role === "user") {
|
|
102
90
|
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: "" }) })] }));
|
|
103
91
|
}
|
|
@@ -166,7 +154,7 @@ function QueuedMessages({ items }) {
|
|
|
166
154
|
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))) }));
|
|
167
155
|
}
|
|
168
156
|
// ─── Input ──────────────────────────────────────────────────────────
|
|
169
|
-
function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agentName, model }) {
|
|
157
|
+
function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agentName, model, onImageMap }) {
|
|
170
158
|
const [input, setInput] = (0, react_1.useState)("");
|
|
171
159
|
const [cursorPos, setCursorPos] = (0, react_1.useState)(0); // cursor position within input
|
|
172
160
|
const [tooltip, setTooltip] = (0, react_1.useState)("");
|
|
@@ -175,23 +163,10 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
175
163
|
const cursorRef = (0, react_1.useRef)(0);
|
|
176
164
|
const historyIdx = (0, react_1.useRef)(-1);
|
|
177
165
|
const savedInput = (0, react_1.useRef)("");
|
|
178
|
-
//
|
|
179
|
-
// \x1b\r into separate
|
|
180
|
-
|
|
181
|
-
(0, react_1.
|
|
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
|
|
166
|
+
// Track last ESC timestamp for Alt+Enter detection.
|
|
167
|
+
// Ink 3.2 splits \x1b\r into separate escape + return events.
|
|
168
|
+
// We detect the pattern by checking if Return arrives within 50ms of Escape.
|
|
169
|
+
const lastEscTime = (0, react_1.useRef)(0);
|
|
195
170
|
const tooltipTimerRef = (0, react_1.useRef)(null);
|
|
196
171
|
(0, react_1.useEffect)(() => () => {
|
|
197
172
|
if (tooltipTimerRef.current)
|
|
@@ -235,6 +210,7 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
235
210
|
// Any non-Ctrl-C input clears tooltip
|
|
236
211
|
setTooltip("");
|
|
237
212
|
if (key.escape) {
|
|
213
|
+
lastEscTime.current = Date.now();
|
|
238
214
|
if (inputRef.current) {
|
|
239
215
|
updateInput("");
|
|
240
216
|
historyIdx.current = -1;
|
|
@@ -255,8 +231,10 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
255
231
|
return;
|
|
256
232
|
}
|
|
257
233
|
if (key.return) {
|
|
258
|
-
// Alt+Enter
|
|
259
|
-
|
|
234
|
+
// Alt+Enter: detect via key.meta OR recent ESC (within 50ms — Ink splits \x1b\r)
|
|
235
|
+
const recentEsc = (Date.now() - lastEscTime.current) < 50;
|
|
236
|
+
if (key.meta || key.shift || recentEsc) {
|
|
237
|
+
lastEscTime.current = 0;
|
|
260
238
|
const before = inputRef.current.slice(0, cursorRef.current);
|
|
261
239
|
const after = inputRef.current.slice(cursorRef.current);
|
|
262
240
|
updateInput(before + "\n" + after, cursorRef.current + 1);
|
|
@@ -270,8 +248,13 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
270
248
|
return;
|
|
271
249
|
}
|
|
272
250
|
const text = inputRef.current;
|
|
273
|
-
if (text.trim())
|
|
274
|
-
|
|
251
|
+
if (text.trim()) {
|
|
252
|
+
const { text: processedText, images } = (0, image_paste_1.processSubmitInput)(text);
|
|
253
|
+
if (images.size > 0 && onImageMap) {
|
|
254
|
+
onImageMap(images);
|
|
255
|
+
}
|
|
256
|
+
onSubmit(images.size > 0 ? processedText : text);
|
|
257
|
+
}
|
|
275
258
|
updateInput("");
|
|
276
259
|
historyIdx.current = -1;
|
|
277
260
|
return;
|
|
@@ -406,14 +389,16 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
406
389
|
});
|
|
407
390
|
const isMultiline = input.includes("\n");
|
|
408
391
|
const inputLines = input.split("\n");
|
|
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, {
|
|
392
|
+
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, { inverse: true, 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
|
|
393
|
+
? (0, jsx_runtime_1.jsx)(ink_1.Text, { inverse: true, children: input[cursorPos] ?? " " })
|
|
394
|
+
: (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" })] })] }));
|
|
410
395
|
}
|
|
411
396
|
// ─── Main TUI Component ─────────────────────────────────────────────
|
|
412
|
-
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, }) {
|
|
397
|
+
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, resumeInfo, onImageMap, }) {
|
|
413
398
|
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) => {
|
|
414
399
|
if (index === 0) {
|
|
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"));
|
|
400
|
+
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"));
|
|
416
401
|
}
|
|
417
402
|
return (0, jsx_runtime_1.jsx)(MessageBlock, { msg: item }, item.id);
|
|
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 })] }));
|
|
403
|
+
} }), (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 })] }));
|
|
419
404
|
}
|
|
@@ -173,17 +173,17 @@ class TuiStore {
|
|
|
173
173
|
seedHistory(texts) {
|
|
174
174
|
this._inputHistory.push(...texts);
|
|
175
175
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
/** Push previous session exchanges as normal user/assistant messages (for resume display) */
|
|
177
|
+
addResumeMessages(exchanges) {
|
|
178
|
+
if (exchanges.length === 0) {
|
|
179
|
+
this.notify();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const msgs = exchanges.map((ex, i) => ({
|
|
183
|
+
id: `resume-${i}`,
|
|
184
|
+
role: ex.role,
|
|
185
|
+
content: ex.content,
|
|
186
|
+
}));
|
|
187
187
|
this._completed = [...msgs, ...this._completed];
|
|
188
188
|
this.notify();
|
|
189
189
|
}
|
package/dist/senses/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.InputQueue = exports.MarkdownStreamer = exports.InputController = exports.Spinner = exports.StreamingWordWrapper = exports.wrapCliText = exports.formatEchoedInputSummary = void 0;
|
|
37
37
|
exports.formatPendingPrefix = formatPendingPrefix;
|
|
38
38
|
exports.getCliContinuityIngressTexts = getCliContinuityIngressTexts;
|
|
39
|
+
exports.formatTimeAgo = formatTimeAgo;
|
|
39
40
|
exports.writeCliAsyncAssistantMessage = writeCliAsyncAssistantMessage;
|
|
40
41
|
exports.pauseActiveSpinner = pauseActiveSpinner;
|
|
41
42
|
exports.resumeActiveSpinner = resumeActiveSpinner;
|
|
@@ -75,6 +76,7 @@ const bundle_meta_1 = require("../heart/daemon/hooks/bundle-meta");
|
|
|
75
76
|
const agent_config_v2_1 = require("../heart/daemon/hooks/agent-config-v2");
|
|
76
77
|
const bundle_manifest_1 = require("../mind/bundle-manifest");
|
|
77
78
|
const cli_layout_1 = require("./cli-layout");
|
|
79
|
+
const image_paste_1 = require("./cli/image-paste");
|
|
78
80
|
const spinner_imperative_1 = require("./cli/spinner-imperative");
|
|
79
81
|
const tool_display_1 = require("./cli/tool-display");
|
|
80
82
|
var cli_layout_2 = require("./cli-layout");
|
|
@@ -513,6 +515,8 @@ async function runCliSession(options) {
|
|
|
513
515
|
const useTui = !options._testInputSource && process.stdin.isTTY === true;
|
|
514
516
|
let currentAbort = null;
|
|
515
517
|
let closed = false;
|
|
518
|
+
// eslint-disable-next-line prefer-const -- set by onImageMap callback during input
|
|
519
|
+
let pendingImages = null;
|
|
516
520
|
let cliCallbacks;
|
|
517
521
|
let tuiStore = null;
|
|
518
522
|
let inkRef = null;
|
|
@@ -535,8 +539,7 @@ async function runCliSession(options) {
|
|
|
535
539
|
.filter((msg) => msg.role === "user" && typeof msg.content === "string")
|
|
536
540
|
.map(msg => msg.content);
|
|
537
541
|
tuiStore.seedHistory(prevUserMsgs);
|
|
538
|
-
// Show session resume context:
|
|
539
|
-
/* v8 ignore start -- session resume display: integration-only, tested visually @preserve */
|
|
542
|
+
// Show session resume context: last 2 exchanges as normal messages
|
|
540
543
|
if (messages.length > 1) {
|
|
541
544
|
const userAssistantMsgs = messages.filter((m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && m.content.trim().length > 0);
|
|
542
545
|
// Extract last 2 exchanges (up to 4 messages)
|
|
@@ -544,12 +547,15 @@ async function runCliSession(options) {
|
|
|
544
547
|
for (let i = userAssistantMsgs.length - 1; i >= 0 && lastExchanges.length < 4; i--) {
|
|
545
548
|
lastExchanges.unshift({ role: userAssistantMsgs[i].role, content: userAssistantMsgs[i].content });
|
|
546
549
|
}
|
|
547
|
-
|
|
548
|
-
const timeAgo = options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : null;
|
|
549
|
-
const summary = ` resuming session · ${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""} · showing recent`;
|
|
550
|
-
tuiStore.addSessionHistory(summary, lastExchanges);
|
|
550
|
+
tuiStore.addResumeMessages(lastExchanges);
|
|
551
551
|
}
|
|
552
|
-
|
|
552
|
+
// Compute resumeInfo for header banner
|
|
553
|
+
const resumeInfo = messages.length > 1
|
|
554
|
+
? {
|
|
555
|
+
messageCount: messages.filter(m => m.role === "user" || m.role === "assistant").length,
|
|
556
|
+
timeAgo: options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : "unknown",
|
|
557
|
+
}
|
|
558
|
+
: undefined;
|
|
553
559
|
// Ctrl-C state machine (Claude Code behavior):
|
|
554
560
|
// During generation: abort current request
|
|
555
561
|
// Idle with text: clear input
|
|
@@ -610,6 +616,8 @@ async function runCliSession(options) {
|
|
|
610
616
|
onPopQueue: () => { const items = storeRef.popAllQueuedForEditing(); inputQueue.drainAll(); return items; },
|
|
611
617
|
headerShown: storeRef.headerShown,
|
|
612
618
|
cwd: process.cwd().replace(process.env.HOME ?? "", "~"),
|
|
619
|
+
resumeInfo,
|
|
620
|
+
onImageMap: (images) => { pendingImages = images; },
|
|
613
621
|
});
|
|
614
622
|
}
|
|
615
623
|
inkRef = ink.render(React.createElement(TuiRoot), { exitOnCtrlC: false, patchConsole: false });
|
|
@@ -811,12 +819,19 @@ async function runCliSession(options) {
|
|
|
811
819
|
if (tuiStore)
|
|
812
820
|
tuiStore.suppressInput();
|
|
813
821
|
inputCtrl?.suppress(() => { currentAbort?.abort(); });
|
|
822
|
+
// Resolve pending image content before the turn executes
|
|
823
|
+
let contentParts = null;
|
|
824
|
+
const currentImages = pendingImages;
|
|
825
|
+
if (currentImages && currentImages.size > 0) {
|
|
826
|
+
contentParts = await (0, image_paste_1.resolveImageContent)(input, currentImages);
|
|
827
|
+
pendingImages = null;
|
|
828
|
+
}
|
|
814
829
|
let result;
|
|
815
830
|
try {
|
|
816
831
|
if (options.runTurn) {
|
|
817
832
|
// Pipeline-based turn: the runTurn callback handles user message assembly,
|
|
818
833
|
// pending drain, trust gate, runAgent, postTurn, and token accumulation.
|
|
819
|
-
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext);
|
|
834
|
+
result = await options.runTurn(messages, input, cliCallbacks, currentAbort.signal, effectiveToolContext, contentParts ?? undefined);
|
|
820
835
|
// Handle pipeline-intercepted commands with loop-control side effects
|
|
821
836
|
if (result?.turnOutcome === "command") {
|
|
822
837
|
if (result.commandAction === "exit") {
|
|
@@ -838,7 +853,10 @@ async function runCliSession(options) {
|
|
|
838
853
|
else {
|
|
839
854
|
// Legacy path: inline runAgent (used by serpent guide and tests)
|
|
840
855
|
const prefix = options.getContentPrefix?.();
|
|
841
|
-
|
|
856
|
+
const userContent = contentParts
|
|
857
|
+
? contentParts
|
|
858
|
+
: (prefix ? `${prefix}\n\n${input}` : input);
|
|
859
|
+
messages.push({ role: "user", content: userContent });
|
|
842
860
|
const traceId = (0, nerves_1.createTraceId)();
|
|
843
861
|
result = await (0, core_1.runAgent)(messages, cliCallbacks, options.skipSystemPromptRefresh ? undefined : "cli", currentAbort.signal, {
|
|
844
862
|
toolChoiceRequired: getEffectiveToolChoiceRequired(),
|
|
@@ -973,7 +991,7 @@ async function main(agentName, options) {
|
|
|
973
991
|
onAsyncAssistantMessage: async (messages, _assistantMessage) => {
|
|
974
992
|
(0, context_1.postTurn)(messages, sessPath, undefined, undefined, sessionState);
|
|
975
993
|
},
|
|
976
|
-
runTurn: async (messages, userInput, callbacks, signal, toolContext) => {
|
|
994
|
+
runTurn: async (messages, userInput, callbacks, signal, toolContext, userContent) => {
|
|
977
995
|
// Run the full per-turn pipeline: resolve -> gate -> session -> drain -> runAgent -> postTurn -> tokens
|
|
978
996
|
// User message passed via input.messages so the pipeline can prepend pending messages to it.
|
|
979
997
|
const failoverState = cliFailoverState;
|
|
@@ -997,7 +1015,7 @@ async function main(agentName, options) {
|
|
|
997
1015
|
channel: "cli",
|
|
998
1016
|
sessionKey: "session",
|
|
999
1017
|
capabilities: cliCapabilities,
|
|
1000
|
-
messages: [{ role: "user", content: userInput }],
|
|
1018
|
+
messages: [{ role: "user", content: userContent ?? userInput }],
|
|
1001
1019
|
continuityIngressTexts: getCliContinuityIngressTexts(userInput),
|
|
1002
1020
|
callbacks: failoverAwareCallbacks,
|
|
1003
1021
|
friendResolver: { resolve: () => Promise.resolve(resolvedContext) },
|