@ouro.bot/cli 0.1.0-alpha.253 → 0.1.0-alpha.255
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 -0
- package/dist/heart/daemon/daemon.js +48 -15
- package/dist/senses/cli/image-paste.js +162 -0
- package/dist/senses/cli/ouro-tui.js +14 -21
- 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,20 @@
|
|
|
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.255",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat(tui): session resume shows last messages as regular chat (no dimmed preview) with teal resume banner in header.",
|
|
8
|
+
"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.",
|
|
9
|
+
"refactor(tui): removed custom history-* roles and addSessionHistory (KISS/DRY)."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.254",
|
|
14
|
+
"changes": [
|
|
15
|
+
"fix(daemon): orphan-cleanup fallback no longer kills processes from sibling harness instances. On startup, `killOrphanProcesses` scans `ps` for harness entry points (`agent-entry.js`, `daemon-entry.js`, `bluebubbles/entry.js`, `teams-entry.js`) and SIGTERMs them when the pidfile is missing. Previously any matching process was fair game, so a vitest-driven harness run from a sibling worktree (or a parallel Claude Code session running the coverage gate) would terminate the production daemon's children, triggering cascading graceful shutdowns and making production slugger unavailable for seconds-to-minutes at a time. Now the fallback only flags true orphans \u2014 processes whose PPID has been reparented to init (PID 1). Sibling daemons' live-parented children are left alone. New exported helper `parseOrphanPidsFromPs` isolates the filter for direct unit coverage. Complements the test-isolation guard shipped in alpha.253 (#333): that PR stopped tests from SENDING `inner.wake testagent` commands into the production socket; this PR stops the production daemon from SIGTERMing test-spawned child processes during its own startup."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.253",
|
|
6
20
|
"changes": [
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.OuroDaemon = void 0;
|
|
37
|
+
exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
|
|
37
38
|
exports.killOrphanProcesses = killOrphanProcesses;
|
|
38
39
|
exports.writePidfile = writePidfile;
|
|
39
40
|
exports.handleAgentSenseTurn = handleAgentSenseTurn;
|
|
@@ -62,13 +63,55 @@ const outlook_types_1 = require("../outlook/outlook-types");
|
|
|
62
63
|
const outlook_read_1 = require("../outlook/outlook-read");
|
|
63
64
|
const outlook_view_1 = require("../outlook/outlook-view");
|
|
64
65
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
66
|
+
/**
|
|
67
|
+
* Scan `ps -eo pid,ppid,command` output for daemon-owned entry points whose
|
|
68
|
+
* parent has died (PPID reparented to init/PID 1). Returns the list of PIDs
|
|
69
|
+
* that are safe to SIGTERM — true orphans, not children of live sibling
|
|
70
|
+
* daemons running from worktrees, test suites, or other users of the harness.
|
|
71
|
+
*
|
|
72
|
+
* Exported so unit tests can exercise the filter without shelling out.
|
|
73
|
+
*/
|
|
74
|
+
function parseOrphanPidsFromPs(psOutput, selfPid) {
|
|
75
|
+
const orphans = [];
|
|
76
|
+
for (const line of psOutput.split("\n")) {
|
|
77
|
+
// Explicitly exclude MCP server processes — they share a harness entry
|
|
78
|
+
// point but are not daemon children and must never be killed.
|
|
79
|
+
if (line.includes("mcp-serve") || line.includes("mcp serve"))
|
|
80
|
+
continue;
|
|
81
|
+
// Match only daemon-owned JS entry points.
|
|
82
|
+
if (!line.includes("agent-entry.js")
|
|
83
|
+
&& !line.includes("daemon-entry.js")
|
|
84
|
+
&& !line.includes("bluebubbles/entry.js")
|
|
85
|
+
&& !line.includes("teams-entry.js"))
|
|
86
|
+
continue;
|
|
87
|
+
// Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
|
|
88
|
+
// Regex guarantees both groups are \d+ so parseInt can't produce NaN.
|
|
89
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s/);
|
|
90
|
+
if (!match)
|
|
91
|
+
continue;
|
|
92
|
+
const pid = parseInt(match[1], 10);
|
|
93
|
+
const ppid = parseInt(match[2], 10);
|
|
94
|
+
if (pid === selfPid)
|
|
95
|
+
continue;
|
|
96
|
+
// CRITICAL: only kill processes whose parent is init (PID 1). A live
|
|
97
|
+
// PPID means the process belongs to another daemon instance (parallel
|
|
98
|
+
// test run, sibling worktree, another user of /tmp/ouroboros-daemon.sock).
|
|
99
|
+
// Killing those will crash unrelated harnesses — we saw this in B6
|
|
100
|
+
// when a vitest worker's daemon killed slugger's production children.
|
|
101
|
+
if (ppid !== 1)
|
|
102
|
+
continue;
|
|
103
|
+
orphans.push(pid);
|
|
104
|
+
}
|
|
105
|
+
return orphans;
|
|
106
|
+
}
|
|
65
107
|
/**
|
|
66
108
|
* Kill all ouro processes from the previous daemon instance using the pidfile.
|
|
67
109
|
* On startup, reads PIDs from ~/.ouro-cli/daemon.pids, kills them all, then
|
|
68
110
|
* deletes the file. The new daemon writes its own PIDs after spawning.
|
|
69
111
|
*
|
|
70
|
-
* Falls back to ps-based scanning
|
|
71
|
-
*
|
|
112
|
+
* Falls back to ps-based scanning scoped to true orphans (PPID=1) if the
|
|
113
|
+
* pidfile doesn't exist (first run, previous daemon crashed before writing,
|
|
114
|
+
* manual cleanup). The scope is narrow on purpose — see parseOrphanPidsFromPs.
|
|
72
115
|
*/
|
|
73
116
|
/* v8 ignore start -- process lifecycle: uses kill/ps, tested via deployment @preserve */
|
|
74
117
|
function killOrphanProcesses() {
|
|
@@ -81,22 +124,12 @@ function killOrphanProcesses() {
|
|
|
81
124
|
fs.unlinkSync(PIDFILE_PATH);
|
|
82
125
|
}
|
|
83
126
|
catch {
|
|
84
|
-
// No pidfile — fall back to ps scan
|
|
127
|
+
// No pidfile — fall back to ps scan (scoped to orphans with PPID=1).
|
|
85
128
|
}
|
|
86
|
-
// Fallback: scan ps for daemon-owned processes only (not MCP servers or external tools)
|
|
87
129
|
if (pidsToKill.length === 0) {
|
|
88
130
|
try {
|
|
89
|
-
const result = (0, child_process_1.execSync)("ps -eo pid,command", { encoding: "utf-8", timeout: 5000 });
|
|
90
|
-
|
|
91
|
-
// Only match daemon-owned entry points, NOT mcp-serve or other external processes
|
|
92
|
-
if (line.includes("mcp-serve") || line.includes("mcp serve"))
|
|
93
|
-
continue;
|
|
94
|
-
if (!line.includes("agent-entry.js") && !line.includes("daemon-entry.js") && !line.includes("bluebubbles/entry.js") && !line.includes("teams-entry.js"))
|
|
95
|
-
continue;
|
|
96
|
-
const pid = parseInt(line.trim(), 10);
|
|
97
|
-
if (!isNaN(pid) && pid !== process.pid)
|
|
98
|
-
pidsToKill.push(pid);
|
|
99
|
-
}
|
|
131
|
+
const result = (0, child_process_1.execSync)("ps -eo pid,ppid,command", { encoding: "utf-8", timeout: 5000 });
|
|
132
|
+
pidsToKill = parseOrphanPidsFromPs(result, process.pid);
|
|
100
133
|
}
|
|
101
134
|
catch { /* ps failed — best effort */ }
|
|
102
135
|
}
|
|
@@ -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 characters (like `\ ` for spaces in 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)("");
|
|
@@ -270,8 +258,13 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
270
258
|
return;
|
|
271
259
|
}
|
|
272
260
|
const text = inputRef.current;
|
|
273
|
-
if (text.trim())
|
|
274
|
-
|
|
261
|
+
if (text.trim()) {
|
|
262
|
+
const { text: processedText, images } = (0, image_paste_1.processSubmitInput)(text);
|
|
263
|
+
if (images.size > 0 && onImageMap) {
|
|
264
|
+
onImageMap(images);
|
|
265
|
+
}
|
|
266
|
+
onSubmit(images.size > 0 ? processedText : text);
|
|
267
|
+
}
|
|
275
268
|
updateInput("");
|
|
276
269
|
historyIdx.current = -1;
|
|
277
270
|
return;
|
|
@@ -409,11 +402,11 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
|
|
|
409
402
|
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: ") " }), !input && queuedInputs.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "Press up to edit queued messages" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(0, cursorPos) }), cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input[cursorPos] ?? " " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(cursorPos + (cursorVisible ? 0 : 1)) })] }))] })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", agentName, model ? ` · ${model}` : "", " \u00B7 /help"] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "opt+enter for newline" })] })] }));
|
|
410
403
|
}
|
|
411
404
|
// ─── Main TUI Component ─────────────────────────────────────────────
|
|
412
|
-
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, }) {
|
|
405
|
+
function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, resumeInfo, onImageMap, }) {
|
|
413
406
|
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
407
|
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"));
|
|
408
|
+
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
409
|
}
|
|
417
410
|
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 })] }));
|
|
411
|
+
} }), (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
412
|
}
|
|
@@ -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) },
|