pzero-operator 0.1.6 → 0.1.8
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/dist/core/agent-session.js +3 -3
- package/dist/core/keybindings.js +1 -0
- package/dist/core/sdk.js +3 -3
- package/dist/core/system-prompt.js +5 -1
- package/dist/core/tools/bash.js +28 -3
- package/dist/core/tools/index.js +12 -2
- package/dist/core/tools/web-search.js +162 -0
- package/dist/migrations.js +36 -2
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +5 -6
- package/dist/modes/interactive/interactive-mode.js +353 -74
- package/dist/utils/shell.js +70 -39
- package/node_modules/@operator/ai/dist/providers/openai-completions.js +6 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@operator/tui";
|
|
9
|
+
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, truncateToWidth, visibleWidth, } from "@operator/tui";
|
|
10
10
|
import { spawn, spawnSync } from "child_process";
|
|
11
11
|
import { APP_NAME, getAgentDir, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
|
|
12
12
|
import { parseSkillBlock } from "../../core/agent-session.js";
|
|
@@ -24,6 +24,7 @@ import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/cha
|
|
|
24
24
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
25
25
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
26
26
|
import { parseGitUrl } from "../../utils/git.js";
|
|
27
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
27
28
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
|
28
29
|
import { ensureTool } from "../../utils/tools-manager.js";
|
|
29
30
|
import { ArminComponent } from "./components/armin.js";
|
|
@@ -89,6 +90,133 @@ class StartupHeroText extends Text {
|
|
|
89
90
|
clearInterval(this.interval);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
93
|
+
function fitLine(line, width) {
|
|
94
|
+
const fitted = truncateToWidth(line ?? "", Math.max(0, width), theme.fg("dim", "..."));
|
|
95
|
+
const padding = Math.max(0, width - visibleWidth(fitted));
|
|
96
|
+
return fitted + " ".repeat(padding);
|
|
97
|
+
}
|
|
98
|
+
function framedLines(lines, width, title = " OPERATOR ") {
|
|
99
|
+
if (width < 8)
|
|
100
|
+
return lines;
|
|
101
|
+
const innerWidth = Math.max(1, width - 4);
|
|
102
|
+
const titleText = truncateToWidth(title, Math.max(0, innerWidth - 2), "");
|
|
103
|
+
const topFill = Math.max(0, width - 3 - visibleWidth(titleText));
|
|
104
|
+
const top = theme.fg("borderAccent", "┌") + theme.bold(theme.fg("accent", titleText)) + theme.fg("borderAccent", "─".repeat(topFill) + "┐");
|
|
105
|
+
const bottom = theme.fg("borderAccent", "└" + "─".repeat(width - 2) + "┘");
|
|
106
|
+
return [
|
|
107
|
+
top,
|
|
108
|
+
...lines.map((line) => theme.fg("borderAccent", "│ ") + fitLine(line, innerWidth) + theme.fg("borderAccent", " │")),
|
|
109
|
+
bottom,
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
function formatTokensCompact(count) {
|
|
113
|
+
if (!count)
|
|
114
|
+
return "0";
|
|
115
|
+
if (count < 1000)
|
|
116
|
+
return String(count);
|
|
117
|
+
if (count < 10000)
|
|
118
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
119
|
+
if (count < 1000000)
|
|
120
|
+
return `${Math.round(count / 1000)}k`;
|
|
121
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
122
|
+
}
|
|
123
|
+
function renderGauge(value, width) {
|
|
124
|
+
const pct = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
|
|
125
|
+
const slots = Math.max(6, Math.min(18, width - 10));
|
|
126
|
+
const filled = Math.round((slots * pct) / 100);
|
|
127
|
+
return `[${theme.fg("borderAccent", "█".repeat(filled))}${theme.fg("dim", "░".repeat(slots - filled))}] ${pct.toFixed(1)}%`;
|
|
128
|
+
}
|
|
129
|
+
function getSpinnerFrame(frame) {
|
|
130
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
131
|
+
return frames[frame % frames.length] || "•";
|
|
132
|
+
}
|
|
133
|
+
function formatReasoningLevel(level) {
|
|
134
|
+
const normalized = level === "low" || level === "medium" || level === "high" ? level : "off";
|
|
135
|
+
if (normalized === "low")
|
|
136
|
+
return `${theme.fg("success", "[LOW]")} ${theme.fg("success", "•")} ${theme.fg("dim", "light analysis")}`;
|
|
137
|
+
if (normalized === "medium")
|
|
138
|
+
return `${theme.fg("warning", "[MEDIUM]")} ${theme.fg("warning", "••")} ${theme.fg("dim", "balanced depth")}`;
|
|
139
|
+
if (normalized === "high")
|
|
140
|
+
return `${theme.fg("error", "[HIGH]")} ${theme.fg("error", "•••")} ${theme.fg("dim", "deep reasoning")}`;
|
|
141
|
+
return `${theme.fg("accent", "[FAST]")} ${theme.fg("accent", "⚡")} ${theme.fg("dim", "direct output")}`;
|
|
142
|
+
}
|
|
143
|
+
function formatReasoningLegend() {
|
|
144
|
+
return [
|
|
145
|
+
`${theme.fg("accent", "FAST")} ${theme.fg("dim", "⚡")}`,
|
|
146
|
+
`${theme.fg("success", "LOW")} ${theme.fg("success", "•")}`,
|
|
147
|
+
`${theme.fg("warning", "MED")} ${theme.fg("warning", "••")}`,
|
|
148
|
+
`${theme.fg("error", "HIGH")} ${theme.fg("error", "•••")}`,
|
|
149
|
+
].join(theme.fg("dim", " "));
|
|
150
|
+
}
|
|
151
|
+
function displayThinkingLevel(level) {
|
|
152
|
+
return level === "off" ? "fast" : level;
|
|
153
|
+
}
|
|
154
|
+
class OperatorWorkspaceLayout {
|
|
155
|
+
mode;
|
|
156
|
+
frame = 0;
|
|
157
|
+
interval;
|
|
158
|
+
constructor(mode) {
|
|
159
|
+
this.mode = mode;
|
|
160
|
+
this.interval = setInterval(() => {
|
|
161
|
+
this.frame = (this.frame + 1) % 24;
|
|
162
|
+
this.mode.ui.requestRender();
|
|
163
|
+
}, 380);
|
|
164
|
+
this.interval.unref?.();
|
|
165
|
+
}
|
|
166
|
+
dispose() {
|
|
167
|
+
clearInterval(this.interval);
|
|
168
|
+
}
|
|
169
|
+
render(width) {
|
|
170
|
+
const terminalHeight = this.mode.ui.terminal.rows || 30;
|
|
171
|
+
const headerHeight = this.mode.headerContainer.render(width).length;
|
|
172
|
+
const outerFrame = width >= 72;
|
|
173
|
+
const contentWidth = outerFrame ? Math.max(1, width - 4) : width;
|
|
174
|
+
const maxRows = Math.max(8, terminalHeight - headerHeight - (outerFrame ? 2 : 0));
|
|
175
|
+
const renderBody = (bodyLines) => outerFrame ? framedLines(bodyLines, width, this.mode.fullHistoryView ? " OPERATOR / FULL CHAT " : " OPERATOR / WORKSPACE ") : bodyLines;
|
|
176
|
+
if (contentWidth < 96) {
|
|
177
|
+
const leftLines = this.clampLeftLines(this.renderLeft(contentWidth), maxRows);
|
|
178
|
+
return renderBody([
|
|
179
|
+
...leftLines,
|
|
180
|
+
theme.fg("borderMuted", "─".repeat(Math.max(0, contentWidth))),
|
|
181
|
+
...this.mode.renderOperatorSidebar(contentWidth).slice(0, Math.max(0, maxRows - leftLines.length - 1)),
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
const gutter = 3;
|
|
185
|
+
const leftWidth = Math.max(64, Math.floor(contentWidth * 0.65) - 1);
|
|
186
|
+
const rightWidth = Math.max(28, contentWidth - leftWidth - gutter);
|
|
187
|
+
const separator = theme.fg("borderMuted", " │ ");
|
|
188
|
+
const leftLines = this.clampLeftLines(this.renderLeft(leftWidth), maxRows);
|
|
189
|
+
const rightLines = this.mode.renderOperatorSidebar(rightWidth).slice(0, maxRows);
|
|
190
|
+
const rowCount = this.mode.fullHistoryView ? Math.max(maxRows, leftLines.length) : maxRows;
|
|
191
|
+
const rightStart = Math.max(0, rowCount - maxRows);
|
|
192
|
+
const lines = [];
|
|
193
|
+
for (let i = 0; i < rowCount; i++) {
|
|
194
|
+
const rightIndex = this.mode.fullHistoryView ? i - rightStart : i;
|
|
195
|
+
lines.push(fitLine(leftLines[i] ?? "", leftWidth) + separator + fitLine(rightLines[rightIndex] ?? "", rightWidth));
|
|
196
|
+
}
|
|
197
|
+
return renderBody(lines);
|
|
198
|
+
}
|
|
199
|
+
renderLeft(width) {
|
|
200
|
+
const components = [
|
|
201
|
+
this.mode.chatContainer,
|
|
202
|
+
this.mode.pendingMessagesContainer,
|
|
203
|
+
this.mode.statusContainer,
|
|
204
|
+
this.mode.widgetContainerAbove,
|
|
205
|
+
this.mode.editorDeckContainer,
|
|
206
|
+
this.mode.editorContainer,
|
|
207
|
+
this.mode.widgetContainerBelow,
|
|
208
|
+
];
|
|
209
|
+
return components.flatMap((component) => component.render(width));
|
|
210
|
+
}
|
|
211
|
+
clampLeftLines(lines, maxRows) {
|
|
212
|
+
if (this.mode.fullHistoryView)
|
|
213
|
+
return lines;
|
|
214
|
+
if (lines.length <= maxRows)
|
|
215
|
+
return lines;
|
|
216
|
+
const marker = theme.fg("dim", `... ${lines.length - maxRows + 1} earlier lines hidden in current view ...`);
|
|
217
|
+
return [marker, ...lines.slice(-(maxRows - 1))];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
92
220
|
function isUnknownModel(model) {
|
|
93
221
|
return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
|
|
94
222
|
}
|
|
@@ -126,6 +254,7 @@ export class InteractiveMode {
|
|
|
126
254
|
fdPath;
|
|
127
255
|
editorDeckContainer;
|
|
128
256
|
editorContainer;
|
|
257
|
+
workspaceLayout;
|
|
129
258
|
footer;
|
|
130
259
|
footerDataProvider;
|
|
131
260
|
// Stored so the same manager can be injected into custom editors, selectors, and extension UI.
|
|
@@ -155,6 +284,7 @@ export class InteractiveMode {
|
|
|
155
284
|
isFirstUserMessage = true;
|
|
156
285
|
// Tool output expansion state
|
|
157
286
|
toolOutputExpanded = false;
|
|
287
|
+
fullHistoryView = false;
|
|
158
288
|
// Thinking block visibility state
|
|
159
289
|
hideThinkingBlock = false;
|
|
160
290
|
// Skill commands: command name -> skill file path
|
|
@@ -177,6 +307,7 @@ export class InteractiveMode {
|
|
|
177
307
|
retryEscapeHandler;
|
|
178
308
|
// Messages queued while compaction is running
|
|
179
309
|
compactionQueuedMessages = [];
|
|
310
|
+
pendingImageAttachments = [];
|
|
180
311
|
// Shutdown state
|
|
181
312
|
shutdownRequested = false;
|
|
182
313
|
// Extension UI state
|
|
@@ -238,6 +369,7 @@ export class InteractiveMode {
|
|
|
238
369
|
this.editorDeckContainer = new Container();
|
|
239
370
|
this.editorContainer = new Container();
|
|
240
371
|
this.editorContainer.addChild(this.editor);
|
|
372
|
+
this.workspaceLayout = new OperatorWorkspaceLayout(this);
|
|
241
373
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
|
242
374
|
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
|
243
375
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
@@ -472,15 +604,8 @@ export class InteractiveMode {
|
|
|
472
604
|
this.builtInHeader = new Text("", 0, 0);
|
|
473
605
|
this.headerContainer.addChild(this.builtInHeader);
|
|
474
606
|
}
|
|
475
|
-
this.ui.addChild(this.chatContainer);
|
|
476
|
-
this.ui.addChild(this.pendingMessagesContainer);
|
|
477
|
-
this.ui.addChild(this.statusContainer);
|
|
478
607
|
this.renderWidgets(); // Initialize with default spacer
|
|
479
|
-
this.ui.addChild(this.
|
|
480
|
-
this.ui.addChild(this.editorDeckContainer);
|
|
481
|
-
this.ui.addChild(this.editorContainer);
|
|
482
|
-
this.ui.addChild(this.widgetContainerBelow);
|
|
483
|
-
this.ui.addChild(this.footer);
|
|
608
|
+
this.ui.addChild(this.workspaceLayout);
|
|
484
609
|
this.ui.setFocus(this.editor);
|
|
485
610
|
this.setupKeyHandlers();
|
|
486
611
|
this.setupEditorSubmitHandler();
|
|
@@ -591,7 +716,9 @@ export class InteractiveMode {
|
|
|
591
716
|
continue;
|
|
592
717
|
}
|
|
593
718
|
this.dismissStartupHero();
|
|
594
|
-
|
|
719
|
+
const text = typeof userInput === "string" ? userInput : userInput.text;
|
|
720
|
+
const images = typeof userInput === "string" ? undefined : userInput.images;
|
|
721
|
+
await this.session.prompt(text, { images });
|
|
595
722
|
}
|
|
596
723
|
catch (error) {
|
|
597
724
|
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
@@ -1512,22 +1639,12 @@ export class InteractiveMode {
|
|
|
1512
1639
|
if (this.customFooter?.dispose) {
|
|
1513
1640
|
this.customFooter.dispose();
|
|
1514
1641
|
}
|
|
1515
|
-
// Remove current footer from UI
|
|
1516
|
-
if (this.customFooter) {
|
|
1517
|
-
this.ui.removeChild(this.customFooter);
|
|
1518
|
-
}
|
|
1519
|
-
else {
|
|
1520
|
-
this.ui.removeChild(this.footer);
|
|
1521
|
-
}
|
|
1522
1642
|
if (factory) {
|
|
1523
|
-
//
|
|
1643
|
+
// Custom footer is rendered inside the right sidebar.
|
|
1524
1644
|
this.customFooter = factory(this.ui, theme, this.footerDataProvider);
|
|
1525
|
-
this.ui.addChild(this.customFooter);
|
|
1526
1645
|
}
|
|
1527
1646
|
else {
|
|
1528
|
-
// Restore built-in footer
|
|
1529
1647
|
this.customFooter = undefined;
|
|
1530
|
-
this.ui.addChild(this.footer);
|
|
1531
1648
|
}
|
|
1532
1649
|
this.ui.requestRender();
|
|
1533
1650
|
}
|
|
@@ -1981,6 +2098,7 @@ export class InteractiveMode {
|
|
|
1981
2098
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
1982
2099
|
this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
|
|
1983
2100
|
this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
|
|
2101
|
+
this.defaultEditor.onAction("app.history.full", () => this.toggleFullHistoryView());
|
|
1984
2102
|
this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
|
|
1985
2103
|
this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
|
|
1986
2104
|
this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
|
|
@@ -2007,14 +2125,15 @@ export class InteractiveMode {
|
|
|
2007
2125
|
if (!image) {
|
|
2008
2126
|
return;
|
|
2009
2127
|
}
|
|
2010
|
-
// Write to temp file
|
|
2011
|
-
const tmpDir = os.tmpdir();
|
|
2012
2128
|
const ext = extensionForImageMimeType(image.mimeType) ?? "png";
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2129
|
+
this.pendingImageAttachments.push({
|
|
2130
|
+
type: "image",
|
|
2131
|
+
data: Buffer.from(image.bytes).toString("base64"),
|
|
2132
|
+
mimeType: image.mimeType,
|
|
2133
|
+
filename: `clipboard-${crypto.randomUUID()}.${ext}`,
|
|
2134
|
+
});
|
|
2135
|
+
this.showStatus(`Image attached (${this.pendingImageAttachments.length})`);
|
|
2136
|
+
this.updateEditorDeck();
|
|
2018
2137
|
this.ui.requestRender();
|
|
2019
2138
|
}
|
|
2020
2139
|
catch {
|
|
@@ -2024,7 +2143,9 @@ export class InteractiveMode {
|
|
|
2024
2143
|
setupEditorSubmitHandler() {
|
|
2025
2144
|
this.defaultEditor.onSubmit = async (text) => {
|
|
2026
2145
|
text = text.trim();
|
|
2027
|
-
|
|
2146
|
+
const { text: normalizedText, images } = await this.preparePromptPayload(text);
|
|
2147
|
+
text = normalizedText;
|
|
2148
|
+
if (!text && !images?.length)
|
|
2028
2149
|
return;
|
|
2029
2150
|
if (DISABLED_SLASH_COMMANDS.has(text) || Array.from(DISABLED_SLASH_COMMANDS).some((command) => text.startsWith(`${command} `))) {
|
|
2030
2151
|
this.editor.setText("");
|
|
@@ -2059,28 +2180,30 @@ export class InteractiveMode {
|
|
|
2059
2180
|
}
|
|
2060
2181
|
const value = text.startsWith("/thinking ") ? text.slice(10).trim().toLowerCase() : "";
|
|
2061
2182
|
if (!value) {
|
|
2062
|
-
const selected = await this.showExtensionSelector("Select analysis depth", ["low", "medium", "high"]);
|
|
2183
|
+
const selected = await this.showExtensionSelector("Select analysis depth", ["fast", "low", "medium", "high"]);
|
|
2063
2184
|
if (!selected) {
|
|
2064
2185
|
this.showStatus("Analysis depth selection cancelled");
|
|
2065
2186
|
return;
|
|
2066
2187
|
}
|
|
2067
|
-
this.session.setThinkingLevel(selected);
|
|
2188
|
+
this.session.setThinkingLevel(selected === "fast" ? "off" : selected);
|
|
2068
2189
|
const after = this.session.thinkingLevel || "off";
|
|
2069
2190
|
this.footer.invalidate();
|
|
2070
2191
|
this.updateEditorBorderColor();
|
|
2071
|
-
this.showStatus(`Analysis depth: ${after}`);
|
|
2192
|
+
this.showStatus(`Analysis depth: ${displayThinkingLevel(after)}`);
|
|
2072
2193
|
return;
|
|
2073
2194
|
}
|
|
2074
|
-
if (!["low", "medium", "high"].includes(value)) {
|
|
2075
|
-
this.showStatus("Use /thinking low, /thinking medium, or /thinking high");
|
|
2195
|
+
if (!["fast", "low", "medium", "high"].includes(value)) {
|
|
2196
|
+
this.showStatus("Use /thinking fast, /thinking low, /thinking medium, or /thinking high");
|
|
2076
2197
|
return;
|
|
2077
2198
|
}
|
|
2078
2199
|
const before = this.session.thinkingLevel || "off";
|
|
2079
|
-
this.session.setThinkingLevel(value);
|
|
2200
|
+
this.session.setThinkingLevel(value === "fast" ? "off" : value);
|
|
2080
2201
|
const after = this.session.thinkingLevel || "off";
|
|
2081
2202
|
this.footer.invalidate();
|
|
2082
2203
|
this.updateEditorBorderColor();
|
|
2083
|
-
this.showStatus(before === after
|
|
2204
|
+
this.showStatus(before === after
|
|
2205
|
+
? `Analysis depth unchanged: ${displayThinkingLevel(after)}`
|
|
2206
|
+
: `Analysis depth: ${displayThinkingLevel(after)}`);
|
|
2084
2207
|
return;
|
|
2085
2208
|
}
|
|
2086
2209
|
if (text === "/copy") {
|
|
@@ -2145,7 +2268,7 @@ export class InteractiveMode {
|
|
|
2145
2268
|
const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
|
|
2146
2269
|
if (command) {
|
|
2147
2270
|
if (this.session.isBashRunning) {
|
|
2148
|
-
this.showWarning("A
|
|
2271
|
+
this.showWarning("A shell command is already running. Press Ctrl+C to cancel it first.");
|
|
2149
2272
|
this.editor.setText(text);
|
|
2150
2273
|
return;
|
|
2151
2274
|
}
|
|
@@ -2167,10 +2290,10 @@ export class InteractiveMode {
|
|
|
2167
2290
|
if (this.isExtensionCommand(text)) {
|
|
2168
2291
|
this.editor.addToHistory?.(text);
|
|
2169
2292
|
this.editor.setText("");
|
|
2170
|
-
await this.session.prompt(text);
|
|
2293
|
+
await this.session.prompt(text, { images });
|
|
2171
2294
|
}
|
|
2172
2295
|
else {
|
|
2173
|
-
this.queueCompactionMessage(text, "steer");
|
|
2296
|
+
this.queueCompactionMessage(text, "steer", images);
|
|
2174
2297
|
}
|
|
2175
2298
|
return;
|
|
2176
2299
|
}
|
|
@@ -2179,7 +2302,7 @@ export class InteractiveMode {
|
|
|
2179
2302
|
if (this.session.isStreaming) {
|
|
2180
2303
|
this.editor.addToHistory?.(text);
|
|
2181
2304
|
this.editor.setText("");
|
|
2182
|
-
await this.session.prompt(text, { streamingBehavior: "steer" });
|
|
2305
|
+
await this.session.prompt(text, { streamingBehavior: "steer", images });
|
|
2183
2306
|
this.updatePendingMessagesDisplay();
|
|
2184
2307
|
this.ui.requestRender();
|
|
2185
2308
|
return;
|
|
@@ -2188,9 +2311,11 @@ export class InteractiveMode {
|
|
|
2188
2311
|
// First, move any pending bash components to chat
|
|
2189
2312
|
this.flushPendingBashComponents();
|
|
2190
2313
|
if (this.onInputCallback) {
|
|
2191
|
-
this.onInputCallback(text);
|
|
2314
|
+
this.onInputCallback({ text, images });
|
|
2192
2315
|
}
|
|
2193
2316
|
this.editor.addToHistory?.(text);
|
|
2317
|
+
this.pendingImageAttachments = [];
|
|
2318
|
+
this.updateEditorDeck();
|
|
2194
2319
|
};
|
|
2195
2320
|
}
|
|
2196
2321
|
subscribeToAgent() {
|
|
@@ -2206,6 +2331,7 @@ export class InteractiveMode {
|
|
|
2206
2331
|
switch (event.type) {
|
|
2207
2332
|
case "agent_start":
|
|
2208
2333
|
this.ui.terminal.setProgress(true);
|
|
2334
|
+
this.fullHistoryView = false;
|
|
2209
2335
|
// Restore main escape handler if retry handler is still active
|
|
2210
2336
|
// (retry success event fires later, but we need main handler now)
|
|
2211
2337
|
if (this.retryEscapeHandler) {
|
|
@@ -2483,7 +2609,11 @@ export class InteractiveMode {
|
|
|
2483
2609
|
const textBlocks = typeof message.content === "string"
|
|
2484
2610
|
? [{ type: "text", text: message.content }]
|
|
2485
2611
|
: message.content.filter((c) => c.type === "text");
|
|
2486
|
-
|
|
2612
|
+
const imageCount = typeof message.content === "string"
|
|
2613
|
+
? 0
|
|
2614
|
+
: message.content.filter((c) => c.type === "image").length;
|
|
2615
|
+
const text = textBlocks.map((c) => c.text).join("");
|
|
2616
|
+
return imageCount > 0 ? `${text}\n\n[Attached images: ${imageCount}]` : text;
|
|
2487
2617
|
}
|
|
2488
2618
|
/**
|
|
2489
2619
|
* Show a status message in the chat.
|
|
@@ -2670,6 +2800,50 @@ export class InteractiveMode {
|
|
|
2670
2800
|
};
|
|
2671
2801
|
});
|
|
2672
2802
|
}
|
|
2803
|
+
async buildImageAttachmentsFromPaths(text) {
|
|
2804
|
+
const attachments = [];
|
|
2805
|
+
const keptLines = [];
|
|
2806
|
+
const lines = text.split("\n");
|
|
2807
|
+
for (const line of lines) {
|
|
2808
|
+
const candidate = line.trim();
|
|
2809
|
+
if (!candidate) {
|
|
2810
|
+
keptLines.push(line);
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
const resolvedPath = path.isAbsolute(candidate)
|
|
2814
|
+
? candidate
|
|
2815
|
+
: path.resolve(this.sessionManager.getCwd(), candidate);
|
|
2816
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
2817
|
+
keptLines.push(line);
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(resolvedPath).catch(() => null);
|
|
2821
|
+
if (!mimeType) {
|
|
2822
|
+
keptLines.push(line);
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
attachments.push({
|
|
2826
|
+
type: "image",
|
|
2827
|
+
data: fs.readFileSync(resolvedPath).toString("base64"),
|
|
2828
|
+
mimeType,
|
|
2829
|
+
filename: path.basename(resolvedPath),
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
text: keptLines.join("\n").trim(),
|
|
2834
|
+
images: attachments,
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
async preparePromptPayload(text) {
|
|
2838
|
+
const pathAttachments = await this.buildImageAttachmentsFromPaths(text);
|
|
2839
|
+
const images = [...this.pendingImageAttachments, ...pathAttachments.images];
|
|
2840
|
+
this.pendingImageAttachments = [];
|
|
2841
|
+
this.updateEditorDeck();
|
|
2842
|
+
return {
|
|
2843
|
+
text: pathAttachments.text || text,
|
|
2844
|
+
images: images.length > 0 ? images : undefined,
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2673
2847
|
rebuildChatFromMessages() {
|
|
2674
2848
|
this.chatContainer.clear();
|
|
2675
2849
|
const context = this.sessionManager.buildSessionContext();
|
|
@@ -2779,33 +2953,36 @@ export class InteractiveMode {
|
|
|
2779
2953
|
}
|
|
2780
2954
|
async handleFollowUp() {
|
|
2781
2955
|
const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
|
|
2782
|
-
if (!text)
|
|
2956
|
+
if (!text && this.pendingImageAttachments.length === 0)
|
|
2957
|
+
return;
|
|
2958
|
+
if (!this.session.isStreaming && !this.session.isCompacting) {
|
|
2959
|
+
if (this.editor.onSubmit) {
|
|
2960
|
+
this.editor.onSubmit(text);
|
|
2961
|
+
}
|
|
2783
2962
|
return;
|
|
2963
|
+
}
|
|
2964
|
+
const payload = await this.preparePromptPayload(text);
|
|
2784
2965
|
// Queue input during compaction (extension commands execute immediately)
|
|
2785
2966
|
if (this.session.isCompacting) {
|
|
2786
|
-
if (this.isExtensionCommand(text)) {
|
|
2787
|
-
this.editor.addToHistory?.(text);
|
|
2967
|
+
if (this.isExtensionCommand(payload.text)) {
|
|
2968
|
+
this.editor.addToHistory?.(payload.text);
|
|
2788
2969
|
this.editor.setText("");
|
|
2789
|
-
await this.session.prompt(text);
|
|
2970
|
+
await this.session.prompt(payload.text, { images: payload.images });
|
|
2790
2971
|
}
|
|
2791
2972
|
else {
|
|
2792
|
-
this.queueCompactionMessage(text, "followUp");
|
|
2973
|
+
this.queueCompactionMessage(payload.text, "followUp", payload.images);
|
|
2793
2974
|
}
|
|
2794
2975
|
return;
|
|
2795
2976
|
}
|
|
2796
2977
|
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
|
2797
2978
|
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
2798
2979
|
if (this.session.isStreaming) {
|
|
2799
|
-
this.editor.addToHistory?.(text);
|
|
2980
|
+
this.editor.addToHistory?.(payload.text);
|
|
2800
2981
|
this.editor.setText("");
|
|
2801
|
-
await this.session.prompt(text, { streamingBehavior: "followUp" });
|
|
2982
|
+
await this.session.prompt(payload.text, { streamingBehavior: "followUp", images: payload.images });
|
|
2802
2983
|
this.updatePendingMessagesDisplay();
|
|
2803
2984
|
this.ui.requestRender();
|
|
2804
2985
|
}
|
|
2805
|
-
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
|
2806
|
-
else if (this.editor.onSubmit) {
|
|
2807
|
-
this.editor.onSubmit(text);
|
|
2808
|
-
}
|
|
2809
2986
|
}
|
|
2810
2987
|
handleDequeue() {
|
|
2811
2988
|
const restored = this.restoreQueuedMessagesToEditor();
|
|
@@ -2829,17 +3006,105 @@ export class InteractiveMode {
|
|
|
2829
3006
|
}
|
|
2830
3007
|
updateEditorDeck() {
|
|
2831
3008
|
this.editorDeckContainer.clear();
|
|
2832
|
-
const modeLabel = this.isBashMode ? "shell
|
|
2833
|
-
const stateLabel = this.isInterruptibleWorkActive()
|
|
2834
|
-
|
|
2835
|
-
|
|
3009
|
+
const modeLabel = this.isBashMode ? "shell" : "prompt";
|
|
3010
|
+
const stateLabel = this.isInterruptibleWorkActive() ? "active" : "ready";
|
|
3011
|
+
const imageLabel = this.pendingImageAttachments.length > 0
|
|
3012
|
+
? theme.fg("warning", `${this.pendingImageAttachments.length} image${this.pendingImageAttachments.length > 1 ? "s" : ""}`)
|
|
3013
|
+
: "";
|
|
2836
3014
|
const hintLabel = this.isBashMode
|
|
2837
|
-
? `! shell
|
|
2838
|
-
:
|
|
2839
|
-
|
|
2840
|
-
|
|
3015
|
+
? theme.fg("dim", `! shell | ${keyText("app.clear")} stop`)
|
|
3016
|
+
: theme.fg("dim", `/ cmd | ${keyText("app.history.full")} full | ${keyText("app.thinking.cycle")} `) +
|
|
3017
|
+
formatReasoningLegend() +
|
|
3018
|
+
theme.fg("dim", ` | ${keyText("app.model.select")} model`);
|
|
3019
|
+
this.editorDeckContainer.addChild(new TruncatedText(theme.bold(theme.fg(this.isBashMode ? "warning" : "accent", `INPUT ${modeLabel}`)) +
|
|
3020
|
+
theme.fg("dim", " | ") +
|
|
3021
|
+
theme.fg(this.isInterruptibleWorkActive() ? "warning" : "success", stateLabel) +
|
|
3022
|
+
(imageLabel ? theme.fg("dim", " | ") + imageLabel : "") +
|
|
3023
|
+
theme.fg("dim", " | ") +
|
|
3024
|
+
hintLabel, 0, 0));
|
|
2841
3025
|
this.editorDeckContainer.addChild(new Spacer(1));
|
|
2842
3026
|
}
|
|
3027
|
+
renderOperatorSidebar(width) {
|
|
3028
|
+
const state = this.session.state;
|
|
3029
|
+
const contextUsage = this.session.getContextUsage();
|
|
3030
|
+
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
3031
|
+
const contextPercent = contextUsage?.percent ?? 0;
|
|
3032
|
+
let totalInput = 0;
|
|
3033
|
+
let totalOutput = 0;
|
|
3034
|
+
let totalCacheRead = 0;
|
|
3035
|
+
for (const entry of this.session.sessionManager.getEntries()) {
|
|
3036
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
3037
|
+
totalInput += entry.message.usage.input;
|
|
3038
|
+
totalOutput += entry.message.usage.output;
|
|
3039
|
+
totalCacheRead += entry.message.usage.cacheRead;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
const cwd = this.session.sessionManager.getCwd().replace(process.env.HOME || "", "~");
|
|
3043
|
+
const sessionName = this.session.sessionManager.getSessionName() || "unnamed";
|
|
3044
|
+
const modelName = state.model?.id || "no-model";
|
|
3045
|
+
const providerName = state.model?.provider || "none";
|
|
3046
|
+
const thinking = state.model?.reasoning ? this.session.thinkingLevel || "off" : "off";
|
|
3047
|
+
const reasoningText = formatReasoningLevel(thinking);
|
|
3048
|
+
const active = this.isInterruptibleWorkActive();
|
|
3049
|
+
const activeTools = this.pendingTools.size;
|
|
3050
|
+
const queued = this.session.pendingMessageCount || 0;
|
|
3051
|
+
const activeToolNames = this.session.getActiveToolNames?.() || [];
|
|
3052
|
+
const toolText = activeToolNames.length ? activeToolNames.slice(0, 6).join(", ") : "read, bash, edit, write";
|
|
3053
|
+
const pulse = active ? ["●", "●", "●", "○"][Math.floor((this.workspaceLayout.frame % 24) / 6)] : "●";
|
|
3054
|
+
const loadingMessage = this.loadingAnimation?.message || this.defaultWorkingMessage;
|
|
3055
|
+
const spinner = getSpinnerFrame(this.workspaceLayout.frame);
|
|
3056
|
+
const rule = theme.fg("borderMuted", "─".repeat(Math.max(0, width)));
|
|
3057
|
+
const section = (name) => fitLine(theme.bold(theme.fg("accent", `▌ ${name}`)) + theme.fg("borderMuted", " " + "─".repeat(Math.max(0, width - visibleWidth(name) - 4))), width);
|
|
3058
|
+
const label = (name, value, color = "text") => fitLine(theme.fg("dim", `${name.padEnd(10)} `) + theme.fg(color, value), width);
|
|
3059
|
+
const rawLabel = (name, value) => fitLine(theme.fg("dim", `${name.padEnd(10)} `) + value, width);
|
|
3060
|
+
const lines = [
|
|
3061
|
+
theme.bold(fitLine(theme.fg("accent", "OPERATOR CONTROL"), width)),
|
|
3062
|
+
fitLine(theme.fg("dim", "agent loop dashboard"), width),
|
|
3063
|
+
rule,
|
|
3064
|
+
section("RUN"),
|
|
3065
|
+
label("state", `${pulse} ${active ? "RUNNING" : "READY"}`, active ? "warning" : "success"),
|
|
3066
|
+
label("phase", active ? "working / verify" : "waiting input", active ? "accent" : "muted"),
|
|
3067
|
+
label("queue", `${queued} pending`, queued ? "warning" : "text"),
|
|
3068
|
+
...(active
|
|
3069
|
+
? [
|
|
3070
|
+
rawLabel("activity", `${theme.fg("accent", spinner)} ${theme.fg("text", loadingMessage)}`),
|
|
3071
|
+
"",
|
|
3072
|
+
]
|
|
3073
|
+
: []),
|
|
3074
|
+
...(!active ? [""] : []),
|
|
3075
|
+
section("CONTEXT"),
|
|
3076
|
+
fitLine(renderGauge(contextPercent, width), width),
|
|
3077
|
+
label("window", `${formatTokensCompact(contextWindow)} tokens`),
|
|
3078
|
+
label("usage", `↑${formatTokensCompact(totalInput)} ↓${formatTokensCompact(totalOutput)} R${formatTokensCompact(totalCacheRead)}`),
|
|
3079
|
+
label("history", this.fullHistoryView ? "full view" : "compact", this.fullHistoryView ? "warning" : "text"),
|
|
3080
|
+
"",
|
|
3081
|
+
section("ENGINE"),
|
|
3082
|
+
label("model", modelName),
|
|
3083
|
+
rawLabel("reason", reasoningText),
|
|
3084
|
+
label("provider", providerName),
|
|
3085
|
+
"",
|
|
3086
|
+
section("WORKSPACE"),
|
|
3087
|
+
label("path", cwd),
|
|
3088
|
+
label("thread", sessionName),
|
|
3089
|
+
"",
|
|
3090
|
+
section("TOOLS"),
|
|
3091
|
+
label("active", `${activeTools} running`, activeTools ? "warning" : "text"),
|
|
3092
|
+
fitLine(theme.fg("text", toolText), width),
|
|
3093
|
+
"",
|
|
3094
|
+
rule,
|
|
3095
|
+
fitLine(theme.fg("dim", `${keyText("app.clear")} stop ${keyText("app.history.full")} full chat`), width),
|
|
3096
|
+
fitLine(theme.fg("dim", `${keyText("app.thinking.cycle")} reason: `) + formatReasoningLegend(), width),
|
|
3097
|
+
fitLine(theme.fg("dim", `${keyText("app.model.select")} model ${keyText("app.editor.external")} editor`), width),
|
|
3098
|
+
];
|
|
3099
|
+
const extensionStatuses = this.footerDataProvider.getExtensionStatuses();
|
|
3100
|
+
if (extensionStatuses.size > 0) {
|
|
3101
|
+
lines.splice(lines.length - 3, 0, "", fitLine(theme.fg("accent", "EXTENSIONS"), width), ...Array.from(extensionStatuses.values()).slice(0, 4).map((value) => fitLine(theme.fg("text", String(value).replace(/[\r\n\t]/g, " ")), width)));
|
|
3102
|
+
}
|
|
3103
|
+
if (this.customFooter) {
|
|
3104
|
+
lines.splice(lines.length - 3, 0, "", ...this.customFooter.render(width).map((line) => fitLine(line, width)));
|
|
3105
|
+
}
|
|
3106
|
+
return lines;
|
|
3107
|
+
}
|
|
2843
3108
|
cycleThinkingLevel() {
|
|
2844
3109
|
if (this.isBusyForModelOrThinkingChange()) {
|
|
2845
3110
|
this.showBusyModelOrThinkingStatus();
|
|
@@ -2852,7 +3117,7 @@ export class InteractiveMode {
|
|
|
2852
3117
|
else {
|
|
2853
3118
|
this.footer.invalidate();
|
|
2854
3119
|
this.updateEditorBorderColor();
|
|
2855
|
-
this.showStatus(`Analysis depth: ${newLevel}`);
|
|
3120
|
+
this.showStatus(`Analysis depth: ${displayThinkingLevel(newLevel)}`);
|
|
2856
3121
|
}
|
|
2857
3122
|
}
|
|
2858
3123
|
async cycleModel(direction) {
|
|
@@ -2880,6 +3145,19 @@ export class InteractiveMode {
|
|
|
2880
3145
|
toggleToolOutputExpansion() {
|
|
2881
3146
|
this.setToolsExpanded(!this.toolOutputExpanded);
|
|
2882
3147
|
}
|
|
3148
|
+
toggleFullHistoryView() {
|
|
3149
|
+
if (this.isInterruptibleWorkActive()) {
|
|
3150
|
+
this.fullHistoryView = false;
|
|
3151
|
+
this.showStatus("Full conversation view is available after the current run finishes");
|
|
3152
|
+
this.ui.requestRender();
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
this.fullHistoryView = !this.fullHistoryView;
|
|
3156
|
+
this.showStatus(this.fullHistoryView
|
|
3157
|
+
? "Full conversation view enabled. Press Ctrl+R again to return to compact view."
|
|
3158
|
+
: "Compact conversation view enabled.");
|
|
3159
|
+
this.ui.requestRender();
|
|
3160
|
+
}
|
|
2883
3161
|
setToolsExpanded(expanded) {
|
|
2884
3162
|
this.toolOutputExpanded = expanded;
|
|
2885
3163
|
const activeHeader = this.customHeader ?? this.builtInHeader;
|
|
@@ -3044,8 +3322,8 @@ export class InteractiveMode {
|
|
|
3044
3322
|
}
|
|
3045
3323
|
return allQueued.length;
|
|
3046
3324
|
}
|
|
3047
|
-
queueCompactionMessage(text, mode) {
|
|
3048
|
-
this.compactionQueuedMessages.push({ text, mode });
|
|
3325
|
+
queueCompactionMessage(text, mode, images) {
|
|
3326
|
+
this.compactionQueuedMessages.push({ text, mode, images });
|
|
3049
3327
|
this.editor.addToHistory?.(text);
|
|
3050
3328
|
this.editor.setText("");
|
|
3051
3329
|
this.updatePendingMessagesDisplay();
|
|
@@ -3077,13 +3355,13 @@ export class InteractiveMode {
|
|
|
3077
3355
|
// When retry is pending, queue messages for the retry turn
|
|
3078
3356
|
for (const message of queuedMessages) {
|
|
3079
3357
|
if (this.isExtensionCommand(message.text)) {
|
|
3080
|
-
await this.session.prompt(message.text);
|
|
3358
|
+
await this.session.prompt(message.text, { images: message.images });
|
|
3081
3359
|
}
|
|
3082
3360
|
else if (message.mode === "followUp") {
|
|
3083
|
-
await this.session.followUp(message.text);
|
|
3361
|
+
await this.session.followUp(message.text, message.images);
|
|
3084
3362
|
}
|
|
3085
3363
|
else {
|
|
3086
|
-
await this.session.steer(message.text);
|
|
3364
|
+
await this.session.steer(message.text, message.images);
|
|
3087
3365
|
}
|
|
3088
3366
|
}
|
|
3089
3367
|
this.updatePendingMessagesDisplay();
|
|
@@ -3094,7 +3372,7 @@ export class InteractiveMode {
|
|
|
3094
3372
|
if (firstPromptIndex === -1) {
|
|
3095
3373
|
// All extension commands - execute them all
|
|
3096
3374
|
for (const message of queuedMessages) {
|
|
3097
|
-
await this.session.prompt(message.text);
|
|
3375
|
+
await this.session.prompt(message.text, { images: message.images });
|
|
3098
3376
|
}
|
|
3099
3377
|
return;
|
|
3100
3378
|
}
|
|
@@ -3103,22 +3381,22 @@ export class InteractiveMode {
|
|
|
3103
3381
|
const firstPrompt = queuedMessages[firstPromptIndex];
|
|
3104
3382
|
const rest = queuedMessages.slice(firstPromptIndex + 1);
|
|
3105
3383
|
for (const message of preCommands) {
|
|
3106
|
-
await this.session.prompt(message.text);
|
|
3384
|
+
await this.session.prompt(message.text, { images: message.images });
|
|
3107
3385
|
}
|
|
3108
3386
|
// Send first prompt (starts streaming)
|
|
3109
|
-
const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
|
|
3387
|
+
const promptPromise = this.session.prompt(firstPrompt.text, { images: firstPrompt.images }).catch((error) => {
|
|
3110
3388
|
restoreQueue(error);
|
|
3111
3389
|
});
|
|
3112
3390
|
// Queue remaining messages
|
|
3113
3391
|
for (const message of rest) {
|
|
3114
3392
|
if (this.isExtensionCommand(message.text)) {
|
|
3115
|
-
await this.session.prompt(message.text);
|
|
3393
|
+
await this.session.prompt(message.text, { images: message.images });
|
|
3116
3394
|
}
|
|
3117
3395
|
else if (message.mode === "followUp") {
|
|
3118
|
-
await this.session.followUp(message.text);
|
|
3396
|
+
await this.session.followUp(message.text, message.images);
|
|
3119
3397
|
}
|
|
3120
3398
|
else {
|
|
3121
|
-
await this.session.steer(message.text);
|
|
3399
|
+
await this.session.steer(message.text, message.images);
|
|
3122
3400
|
}
|
|
3123
3401
|
}
|
|
3124
3402
|
this.updatePendingMessagesDisplay();
|
|
@@ -4320,7 +4598,7 @@ export class InteractiveMode {
|
|
|
4320
4598
|
if (this.bashComponent) {
|
|
4321
4599
|
this.bashComponent.setComplete(undefined, false);
|
|
4322
4600
|
}
|
|
4323
|
-
this.showError(`
|
|
4601
|
+
this.showError(`Shell command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
4324
4602
|
}
|
|
4325
4603
|
this.bashComponent = undefined;
|
|
4326
4604
|
this.ui.requestRender();
|
|
@@ -4351,6 +4629,7 @@ export class InteractiveMode {
|
|
|
4351
4629
|
this.loadingAnimation.stop();
|
|
4352
4630
|
this.loadingAnimation = undefined;
|
|
4353
4631
|
}
|
|
4632
|
+
this.workspaceLayout.dispose();
|
|
4354
4633
|
this.clearExtensionTerminalInputListeners();
|
|
4355
4634
|
this.footer.dispose();
|
|
4356
4635
|
this.footerDataProvider.dispose();
|