inferoa 0.1.0
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/LICENSE +176 -0
- package/README.md +154 -0
- package/dist/src/app.d.ts +16 -0
- package/dist/src/app.js +17 -0
- package/dist/src/app.js.map +1 -0
- package/dist/src/autoresearch/state.d.ts +106 -0
- package/dist/src/autoresearch/state.js +469 -0
- package/dist/src/autoresearch/state.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +415 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/code-intelligence/codegraph-engine.d.ts +55 -0
- package/dist/src/code-intelligence/codegraph-engine.js +593 -0
- package/dist/src/code-intelligence/codegraph-engine.js.map +1 -0
- package/dist/src/code-intelligence/hub.d.ts +37 -0
- package/dist/src/code-intelligence/hub.js +65 -0
- package/dist/src/code-intelligence/hub.js.map +1 -0
- package/dist/src/config/config.d.ts +12 -0
- package/dist/src/config/config.js +229 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/config/defaults.d.ts +2 -0
- package/dist/src/config/defaults.js +44 -0
- package/dist/src/config/defaults.js.map +1 -0
- package/dist/src/config/secret-vault.d.ts +3 -0
- package/dist/src/config/secret-vault.js +106 -0
- package/dist/src/config/secret-vault.js.map +1 -0
- package/dist/src/context/compressor.d.ts +33 -0
- package/dist/src/context/compressor.js +501 -0
- package/dist/src/context/compressor.js.map +1 -0
- package/dist/src/context/prompt.d.ts +26 -0
- package/dist/src/context/prompt.js +572 -0
- package/dist/src/context/prompt.js.map +1 -0
- package/dist/src/daemon/serve.d.ts +2 -0
- package/dist/src/daemon/serve.js +11 -0
- package/dist/src/daemon/serve.js.map +1 -0
- package/dist/src/daemon/supervisor.d.ts +33 -0
- package/dist/src/daemon/supervisor.js +252 -0
- package/dist/src/daemon/supervisor.js.map +1 -0
- package/dist/src/goals/state.d.ts +105 -0
- package/dist/src/goals/state.js +736 -0
- package/dist/src/goals/state.js.map +1 -0
- package/dist/src/model/endpoint-signals.d.ts +15 -0
- package/dist/src/model/endpoint-signals.js +186 -0
- package/dist/src/model/endpoint-signals.js.map +1 -0
- package/dist/src/model/gateway.d.ts +11 -0
- package/dist/src/model/gateway.js +455 -0
- package/dist/src/model/gateway.js.map +1 -0
- package/dist/src/plans/state.d.ts +28 -0
- package/dist/src/plans/state.js +123 -0
- package/dist/src/plans/state.js.map +1 -0
- package/dist/src/runtime.d.ts +92 -0
- package/dist/src/runtime.js +757 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/session/store.d.ts +84 -0
- package/dist/src/session/store.js +593 -0
- package/dist/src/session/store.js.map +1 -0
- package/dist/src/session/workspace.d.ts +2 -0
- package/dist/src/session/workspace.js +14 -0
- package/dist/src/session/workspace.js.map +1 -0
- package/dist/src/skills/registry.d.ts +24 -0
- package/dist/src/skills/registry.js +203 -0
- package/dist/src/skills/registry.js.map +1 -0
- package/dist/src/tools/autoresearch-tools.d.ts +6 -0
- package/dist/src/tools/autoresearch-tools.js +412 -0
- package/dist/src/tools/autoresearch-tools.js.map +1 -0
- package/dist/src/tools/clarify-tool.d.ts +3 -0
- package/dist/src/tools/clarify-tool.js +107 -0
- package/dist/src/tools/clarify-tool.js.map +1 -0
- package/dist/src/tools/code-intelligence.d.ts +15 -0
- package/dist/src/tools/code-intelligence.js +391 -0
- package/dist/src/tools/code-intelligence.js.map +1 -0
- package/dist/src/tools/context.d.ts +11 -0
- package/dist/src/tools/context.js +2 -0
- package/dist/src/tools/context.js.map +1 -0
- package/dist/src/tools/goal-tools.d.ts +3 -0
- package/dist/src/tools/goal-tools.js +279 -0
- package/dist/src/tools/goal-tools.js.map +1 -0
- package/dist/src/tools/omni-tools.d.ts +8 -0
- package/dist/src/tools/omni-tools.js +349 -0
- package/dist/src/tools/omni-tools.js.map +1 -0
- package/dist/src/tools/permissions.d.ts +11 -0
- package/dist/src/tools/permissions.js +74 -0
- package/dist/src/tools/permissions.js.map +1 -0
- package/dist/src/tools/plan-tools.d.ts +3 -0
- package/dist/src/tools/plan-tools.js +314 -0
- package/dist/src/tools/plan-tools.js.map +1 -0
- package/dist/src/tools/process-tools.d.ts +6 -0
- package/dist/src/tools/process-tools.js +199 -0
- package/dist/src/tools/process-tools.js.map +1 -0
- package/dist/src/tools/registry.d.ts +20 -0
- package/dist/src/tools/registry.js +187 -0
- package/dist/src/tools/registry.js.map +1 -0
- package/dist/src/tools/schemas.d.ts +3 -0
- package/dist/src/tools/schemas.js +500 -0
- package/dist/src/tools/schemas.js.map +1 -0
- package/dist/src/tools/skill-tools.d.ts +6 -0
- package/dist/src/tools/skill-tools.js +124 -0
- package/dist/src/tools/skill-tools.js.map +1 -0
- package/dist/src/tools/text-args.d.ts +5 -0
- package/dist/src/tools/text-args.js +22 -0
- package/dist/src/tools/text-args.js.map +1 -0
- package/dist/src/tools/web-search.d.ts +5 -0
- package/dist/src/tools/web-search.js +602 -0
- package/dist/src/tools/web-search.js.map +1 -0
- package/dist/src/tools/workspace-tools.d.ts +17 -0
- package/dist/src/tools/workspace-tools.js +561 -0
- package/dist/src/tools/workspace-tools.js.map +1 -0
- package/dist/src/tui/activity.d.ts +11 -0
- package/dist/src/tui/activity.js +75 -0
- package/dist/src/tui/activity.js.map +1 -0
- package/dist/src/tui/ansi.d.ts +24 -0
- package/dist/src/tui/ansi.js +131 -0
- package/dist/src/tui/ansi.js.map +1 -0
- package/dist/src/tui/app.d.ts +163 -0
- package/dist/src/tui/app.js +4204 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/tui/cache-footer.d.ts +21 -0
- package/dist/src/tui/cache-footer.js +75 -0
- package/dist/src/tui/cache-footer.js.map +1 -0
- package/dist/src/tui/clarify.d.ts +14 -0
- package/dist/src/tui/clarify.js +187 -0
- package/dist/src/tui/clarify.js.map +1 -0
- package/dist/src/tui/composer.d.ts +79 -0
- package/dist/src/tui/composer.js +592 -0
- package/dist/src/tui/composer.js.map +1 -0
- package/dist/src/tui/event-view.d.ts +5 -0
- package/dist/src/tui/event-view.js +392 -0
- package/dist/src/tui/event-view.js.map +1 -0
- package/dist/src/tui/home.d.ts +7 -0
- package/dist/src/tui/home.js +92 -0
- package/dist/src/tui/home.js.map +1 -0
- package/dist/src/tui/markdown.d.ts +18 -0
- package/dist/src/tui/markdown.js +271 -0
- package/dist/src/tui/markdown.js.map +1 -0
- package/dist/src/tui/mode-footer.d.ts +9 -0
- package/dist/src/tui/mode-footer.js +62 -0
- package/dist/src/tui/mode-footer.js.map +1 -0
- package/dist/src/tui/plan-view.d.ts +8 -0
- package/dist/src/tui/plan-view.js +45 -0
- package/dist/src/tui/plan-view.js.map +1 -0
- package/dist/src/tui/prompt-queue.d.ts +18 -0
- package/dist/src/tui/prompt-queue.js +27 -0
- package/dist/src/tui/prompt-queue.js.map +1 -0
- package/dist/src/tui/resize.d.ts +7 -0
- package/dist/src/tui/resize.js +15 -0
- package/dist/src/tui/resize.js.map +1 -0
- package/dist/src/tui/session-picker.d.ts +10 -0
- package/dist/src/tui/session-picker.js +17 -0
- package/dist/src/tui/session-picker.js.map +1 -0
- package/dist/src/tui/session-transcript.d.ts +2 -0
- package/dist/src/tui/session-transcript.js +44 -0
- package/dist/src/tui/session-transcript.js.map +1 -0
- package/dist/src/tui/slash-notice.d.ts +2 -0
- package/dist/src/tui/slash-notice.js +9 -0
- package/dist/src/tui/slash-notice.js.map +1 -0
- package/dist/src/tui/slash.d.ts +21 -0
- package/dist/src/tui/slash.js +103 -0
- package/dist/src/tui/slash.js.map +1 -0
- package/dist/src/tui/splash.d.ts +4 -0
- package/dist/src/tui/splash.js +64 -0
- package/dist/src/tui/splash.js.map +1 -0
- package/dist/src/tui/tool-renderer.d.ts +6 -0
- package/dist/src/tui/tool-renderer.js +1024 -0
- package/dist/src/tui/tool-renderer.js.map +1 -0
- package/dist/src/tui/transcript-spacing.d.ts +1 -0
- package/dist/src/tui/transcript-spacing.js +4 -0
- package/dist/src/tui/transcript-spacing.js.map +1 -0
- package/dist/src/types.d.ts +220 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/util/abort.d.ts +3 -0
- package/dist/src/util/abort.js +19 -0
- package/dist/src/util/abort.js.map +1 -0
- package/dist/src/util/clock.d.ts +2 -0
- package/dist/src/util/clock.js +7 -0
- package/dist/src/util/clock.js.map +1 -0
- package/dist/src/util/fs.d.ts +13 -0
- package/dist/src/util/fs.js +75 -0
- package/dist/src/util/fs.js.map +1 -0
- package/dist/src/util/hash.d.ts +6 -0
- package/dist/src/util/hash.js +50 -0
- package/dist/src/util/hash.js.map +1 -0
- package/dist/src/util/limit.d.ts +11 -0
- package/dist/src/util/limit.js +29 -0
- package/dist/src/util/limit.js.map +1 -0
- package/dist/src/util/types.d.ts +22 -0
- package/dist/src/util/types.js +33 -0
- package/dist/src/util/types.js.map +1 -0
- package/dist/src/validation/acceptance.d.ts +12 -0
- package/dist/src/validation/acceptance.js +251 -0
- package/dist/src/validation/acceptance.js.map +1 -0
- package/dist/src/validation/milestone.d.ts +2 -0
- package/dist/src/validation/milestone.js +141 -0
- package/dist/src/validation/milestone.js.map +1 -0
- package/docs/final-acceptance-task.md +193 -0
- package/docs/public-source-hygiene.md +21 -0
- package/docs/roadmap.md +265 -0
- package/docs/tui-product-design.md +270 -0
- package/package.json +67 -0
- package/skills/coding-workflow/SKILL.md +16 -0
|
@@ -0,0 +1,4204 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { stdin, stdout } from "node:process";
|
|
5
|
+
import { saveUserConfig, userConfigPath } from "../config/config.js";
|
|
6
|
+
import { readSecret, secretRef, writeSecret } from "../config/secret-vault.js";
|
|
7
|
+
import { EndpointSignals } from "../model/endpoint-signals.js";
|
|
8
|
+
import { authHeaders } from "../model/endpoint-signals.js";
|
|
9
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
10
|
+
import { SkillRegistry } from "../skills/registry.js";
|
|
11
|
+
import { attachDaemonJob, cancelDaemonJob, daemonStatus, detachDaemonJob, queueDaemonRun, startDaemon } from "../daemon/supervisor.js";
|
|
12
|
+
import { runFinalAcceptance } from "../validation/acceptance.js";
|
|
13
|
+
import { attachGoalPlanSnapshot, cloneGoalState, createGoalState, incompleteGoalPlanningMessage, goalPlanningProgressSummary, readGoalState, recordGoalCompletionReport, validateTokenBudget, writeGoalState, } from "../goals/state.js";
|
|
14
|
+
import { readAutoresearchState, setAutoresearchMode, summarizeAutoresearchProgress } from "../autoresearch/state.js";
|
|
15
|
+
import { clonePlanState, createPlanState, planApprovalBlockMessage, readPlanState, writePlanState } from "../plans/state.js";
|
|
16
|
+
import { randomId } from "../util/hash.js";
|
|
17
|
+
import { isAbortError } from "../util/abort.js";
|
|
18
|
+
import { ansi, bgLine, bg256, centerBlock, fg256, frame, padRight, terminalHeight, terminalWidth, truncateToWidth, visibleWidth } from "./ansi.js";
|
|
19
|
+
import { parseSlashCommand, slashCommandWithSubcommands, slashSubcommands, SLASH_COMMANDS } from "./slash.js";
|
|
20
|
+
import { renderActivityLine, renderActivityRecordLine } from "./activity.js";
|
|
21
|
+
import { cacheTurnKind, formatDuration, renderCacheFooter, renderCacheReportTurn } from "./cache-footer.js";
|
|
22
|
+
import { renderCompactEventLine, renderSessionActivityLines, renderTodoEventLines } from "./event-view.js";
|
|
23
|
+
import { renderModeMetadataRight } from "./mode-footer.js";
|
|
24
|
+
import { renderPlanDocumentSurface } from "./plan-view.js";
|
|
25
|
+
import { composerEraseRowsForResize } from "./resize.js";
|
|
26
|
+
import { RESUME_SESSION_PAGE_SIZE, resumeSessionPage } from "./session-picker.js";
|
|
27
|
+
import { renderSessionTranscript } from "./session-transcript.js";
|
|
28
|
+
import { renderUnknownSlashCommandNotice } from "./slash-notice.js";
|
|
29
|
+
import { renderToolCards } from "./tool-renderer.js";
|
|
30
|
+
import { withConversationGap } from "./transcript-spacing.js";
|
|
31
|
+
import { MarkdownStreamRenderer } from "./markdown.js";
|
|
32
|
+
import { renderHomeFrame } from "./home.js";
|
|
33
|
+
import { backspaceComposer, adjustComposerCompactRanges, compactRangeBeforeCursor, composerPlainPasteFallback, insertComposerPaste, insertComposerText, moveComposerCursorEnd, moveComposerCursorHome, moveComposerCursorLeft, moveComposerCursorRight, compactModelLabel, normalizeComposerPastedInput, renderComposerActivityLine, renderComposerSurface, renderWelcomeComposerSurface, } from "./composer.js";
|
|
34
|
+
import { createPromptQueueState, enqueuePromptForSubmission, promptQueuePreviewLines, shiftPromptForSubmission, } from "./prompt-queue.js";
|
|
35
|
+
import { applyClarifyInputToken, createClarifyInputState, renderClarifyComposerPanel } from "./clarify.js";
|
|
36
|
+
const CLEAR_TO_END = "\x1b[J";
|
|
37
|
+
const CLEAR_LINE = "\x1b[2K";
|
|
38
|
+
const BRACKETED_PASTE_ENABLE = "\x1b[?2004h";
|
|
39
|
+
const BRACKETED_PASTE_DISABLE = "\x1b[?2004l";
|
|
40
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
41
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
42
|
+
const PASTE_TOKEN_PREFIX = "\u{e000}paste:";
|
|
43
|
+
const SETUP_TOTAL_STEPS = 6;
|
|
44
|
+
export const TUI_OMNI_SETUP_CAPABILITIES = [
|
|
45
|
+
{ name: "vision", label: "Vision understanding", requiredForAcceptance: true },
|
|
46
|
+
{ name: "image_generation", label: "Image generation", requiredForAcceptance: true },
|
|
47
|
+
{ name: "video_understanding", label: "Video understanding", requiredForAcceptance: false },
|
|
48
|
+
{ name: "video_generation", label: "Video generation", requiredForAcceptance: true },
|
|
49
|
+
{ name: "audio_understanding", label: "Audio understanding", requiredForAcceptance: false },
|
|
50
|
+
{ name: "audio_generation", label: "Audio generation", requiredForAcceptance: false },
|
|
51
|
+
];
|
|
52
|
+
export const PREFIX_CACHE_REPORT_TITLE = "Prefix Cache Report";
|
|
53
|
+
export class TuiApp {
|
|
54
|
+
app;
|
|
55
|
+
options;
|
|
56
|
+
#sessionId;
|
|
57
|
+
#rl;
|
|
58
|
+
#running = true;
|
|
59
|
+
#inlineMode = false;
|
|
60
|
+
#inlineRenderedLines = 0;
|
|
61
|
+
#inlinePanelStartRow;
|
|
62
|
+
#toolTraceMode = "compact";
|
|
63
|
+
#composerFooter;
|
|
64
|
+
#composerActivity;
|
|
65
|
+
#composerQueue;
|
|
66
|
+
#composerPanel;
|
|
67
|
+
#inputModalActive = false;
|
|
68
|
+
#hasTranscript = false;
|
|
69
|
+
#activeComposerErase;
|
|
70
|
+
#activeComposerRedraw;
|
|
71
|
+
#activeComposerActivityRedraw;
|
|
72
|
+
#activeWelcomeCodeIntelligenceRedraw;
|
|
73
|
+
#promptQueue = createPromptQueueState();
|
|
74
|
+
#promptWorker;
|
|
75
|
+
#promptWorkerScheduled = false;
|
|
76
|
+
#activeAbort;
|
|
77
|
+
#welcomeCodeIntelligenceStarted = false;
|
|
78
|
+
#welcomeCodeIntelligenceStop;
|
|
79
|
+
#shutdownStarted = false;
|
|
80
|
+
constructor(app, options = {}) {
|
|
81
|
+
this.app = app;
|
|
82
|
+
this.options = options;
|
|
83
|
+
}
|
|
84
|
+
async run() {
|
|
85
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
86
|
+
this.renderNonInteractiveNotice();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
stdout.write(ansi.hideCursor);
|
|
90
|
+
try {
|
|
91
|
+
this.renderHome();
|
|
92
|
+
this.startWelcomeCodeIntelligenceIndexing();
|
|
93
|
+
if (this.options.initialView) {
|
|
94
|
+
await this.openView(this.options.initialView, "");
|
|
95
|
+
}
|
|
96
|
+
if (this.options.initialPrompt) {
|
|
97
|
+
this.enqueuePrompt(this.options.initialPrompt);
|
|
98
|
+
}
|
|
99
|
+
await this.loop();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await this.shutdownBackgroundWork("TUI closed");
|
|
103
|
+
this.#rl?.close();
|
|
104
|
+
if (stdin.isTTY) {
|
|
105
|
+
stdin.setRawMode(false);
|
|
106
|
+
}
|
|
107
|
+
stdin.pause();
|
|
108
|
+
stdout.write(ansi.showCursor);
|
|
109
|
+
stdout.write("\n");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
renderNonInteractiveNotice() {
|
|
113
|
+
console.error("inferoa TUI requires an interactive terminal. Use `inferoa --print \"prompt\"` for non-interactive runs.");
|
|
114
|
+
}
|
|
115
|
+
async loop() {
|
|
116
|
+
while (this.#running) {
|
|
117
|
+
const text = (await this.readComposer()).trim();
|
|
118
|
+
if (!text) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (text === "/" || text.startsWith("/ ")) {
|
|
122
|
+
try {
|
|
123
|
+
const command = await this.chooseSlashCommand(text.slice(1).trim());
|
|
124
|
+
await this.openView(command, "");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.handleViewError(error);
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (text === "$" || text.startsWith("$ ")) {
|
|
132
|
+
try {
|
|
133
|
+
await this.renderSkillLauncher(text.slice(1).trim());
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.handleViewError(error);
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const parsed = parseSlashCommand(text);
|
|
141
|
+
if (parsed.error) {
|
|
142
|
+
this.renderUnknownSlashCommand(text);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (parsed.command) {
|
|
146
|
+
try {
|
|
147
|
+
await this.openView(parsed.command.name, parsed.args);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
this.handleViewError(error);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
this.enqueuePrompt(text);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
enqueuePrompt(prompt, options = {}) {
|
|
158
|
+
const busy = Boolean(this.#activeAbort || this.#promptWorker || this.#promptWorkerScheduled || this.promptRequiresCodeIntelligenceGate());
|
|
159
|
+
this.#composerFooter = undefined;
|
|
160
|
+
const queued = enqueuePromptForSubmission(this.#promptQueue, prompt, { busy, renderPrompt: options.renderPrompt });
|
|
161
|
+
this.#promptQueue = queued.state;
|
|
162
|
+
if (queued.renderSubmittedPromptNow) {
|
|
163
|
+
this.renderSubmittedPrompt(prompt);
|
|
164
|
+
}
|
|
165
|
+
if (busy) {
|
|
166
|
+
this.updateQueueFooter();
|
|
167
|
+
}
|
|
168
|
+
this.schedulePromptWorker();
|
|
169
|
+
}
|
|
170
|
+
schedulePromptWorker() {
|
|
171
|
+
if (this.#promptWorker || this.#promptWorkerScheduled) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.#promptWorkerScheduled = true;
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
this.#promptWorkerScheduled = false;
|
|
177
|
+
if (this.#promptWorker || !this.#promptQueue.length || !this.#running) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.#promptWorker = this.drainPromptQueue()
|
|
181
|
+
.catch((error) => {
|
|
182
|
+
this.writeTranscript(`\n${fg256(203, error instanceof Error ? error.message : String(error))}\n\n`);
|
|
183
|
+
})
|
|
184
|
+
.finally(() => {
|
|
185
|
+
this.#promptWorker = undefined;
|
|
186
|
+
if (this.#promptQueue.length && this.#running) {
|
|
187
|
+
this.schedulePromptWorker();
|
|
188
|
+
}
|
|
189
|
+
else if (!this.#activeAbort) {
|
|
190
|
+
this.clearQueueFooter();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}, 0);
|
|
194
|
+
}
|
|
195
|
+
async drainPromptQueue() {
|
|
196
|
+
while (this.#promptQueue.length && this.#running) {
|
|
197
|
+
const next = shiftPromptForSubmission(this.#promptQueue);
|
|
198
|
+
this.#promptQueue = next.state;
|
|
199
|
+
const item = next.item;
|
|
200
|
+
if (!item) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
this.updateQueueFooter();
|
|
204
|
+
await this.submitPrompt(item.prompt, { renderPrompt: item.renderPromptAtSubmission });
|
|
205
|
+
}
|
|
206
|
+
this.clearQueueFooter();
|
|
207
|
+
}
|
|
208
|
+
updateQueueFooter() {
|
|
209
|
+
const lines = promptQueuePreviewLines(this.#promptQueue);
|
|
210
|
+
if (!lines.length) {
|
|
211
|
+
this.clearQueueFooter();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.#composerQueue = lines;
|
|
215
|
+
this.#activeComposerRedraw?.();
|
|
216
|
+
}
|
|
217
|
+
clearQueueFooter() {
|
|
218
|
+
if (promptQueuePreviewLines(this.#promptQueue).length || !this.#composerQueue) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.#composerQueue = undefined;
|
|
222
|
+
this.#activeComposerRedraw?.();
|
|
223
|
+
}
|
|
224
|
+
interruptActiveLoop() {
|
|
225
|
+
const aborted = this.abortActiveLoop("User interrupted current loop");
|
|
226
|
+
if (!aborted) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
this.#composerActivity = `${fg256(220, "●")} ${fg256(250, "Interrupting current loop")} ${fg256(244, "queued prompts will run next")}`;
|
|
230
|
+
this.updateQueueFooter();
|
|
231
|
+
this.#activeComposerRedraw?.();
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
abortActiveLoop(reason) {
|
|
235
|
+
const controller = this.#activeAbort;
|
|
236
|
+
if (!controller || controller.signal.aborted) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
controller.abort(reason);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
async requestExit() {
|
|
243
|
+
if (this.#shutdownStarted) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.#shutdownStarted = true;
|
|
247
|
+
this.#running = false;
|
|
248
|
+
await this.shutdownBackgroundWork("User exited TUI");
|
|
249
|
+
stdout.write(fg256(243, "Resume this workspace with inferoa\n"));
|
|
250
|
+
}
|
|
251
|
+
async shutdownBackgroundWork(reason) {
|
|
252
|
+
this.#running = false;
|
|
253
|
+
this.#welcomeCodeIntelligenceStop?.();
|
|
254
|
+
this.#promptQueue = createPromptQueueState();
|
|
255
|
+
this.clearQueueFooter();
|
|
256
|
+
const aborted = this.abortActiveLoop(reason);
|
|
257
|
+
const worker = this.#promptWorker;
|
|
258
|
+
if (!worker) {
|
|
259
|
+
if (aborted) {
|
|
260
|
+
this.#composerActivity = undefined;
|
|
261
|
+
this.#activeComposerRedraw?.();
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (aborted) {
|
|
266
|
+
this.#composerActivity = `${fg256(220, "●")} ${fg256(250, "Stopping current loop")}`;
|
|
267
|
+
this.#activeComposerRedraw?.();
|
|
268
|
+
}
|
|
269
|
+
await worker.catch((error) => {
|
|
270
|
+
this.writeTranscript(`\n${fg256(203, error instanceof Error ? error.message : String(error))}\n\n`);
|
|
271
|
+
});
|
|
272
|
+
this.#composerActivity = undefined;
|
|
273
|
+
this.#activeComposerRedraw?.();
|
|
274
|
+
}
|
|
275
|
+
async question(prompt) {
|
|
276
|
+
this.#rl ??= createInterface({ input: stdin, output: stdout });
|
|
277
|
+
return await this.#rl.question(prompt);
|
|
278
|
+
}
|
|
279
|
+
async readComposer(options = {}) {
|
|
280
|
+
const skills = await new SkillRegistry(this.app.workspace, this.app.config).discover().catch(() => []);
|
|
281
|
+
let buffer = options.initialBuffer ?? "";
|
|
282
|
+
let cursor = buffer.length;
|
|
283
|
+
let compactRanges = [];
|
|
284
|
+
let selected = 0;
|
|
285
|
+
let selectionTouched = false;
|
|
286
|
+
let renderedLines = 0;
|
|
287
|
+
let renderedCursorLine = 0;
|
|
288
|
+
let renderedCursorColumn = 0;
|
|
289
|
+
let renderedWidth = 0;
|
|
290
|
+
let renderedActivityLine;
|
|
291
|
+
let renderedCodeIntelligenceLine;
|
|
292
|
+
let renderedCodeIntelligenceColumn;
|
|
293
|
+
let renderedCodeIntelligenceWidth;
|
|
294
|
+
let forceFullRedraw = false;
|
|
295
|
+
let eraseAfterResize = false;
|
|
296
|
+
const pasteState = {};
|
|
297
|
+
this.#rl?.pause();
|
|
298
|
+
stdout.write(`${BRACKETED_PASTE_ENABLE}${ansi.showCursor}`);
|
|
299
|
+
return await new Promise((resolve) => {
|
|
300
|
+
const resetRenderedState = () => {
|
|
301
|
+
renderedLines = 0;
|
|
302
|
+
renderedCursorLine = 0;
|
|
303
|
+
renderedCursorColumn = 0;
|
|
304
|
+
renderedWidth = 0;
|
|
305
|
+
renderedActivityLine = undefined;
|
|
306
|
+
renderedCodeIntelligenceLine = undefined;
|
|
307
|
+
renderedCodeIntelligenceColumn = undefined;
|
|
308
|
+
renderedCodeIntelligenceWidth = undefined;
|
|
309
|
+
};
|
|
310
|
+
const erase = (options = {}) => {
|
|
311
|
+
if (!renderedLines) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
stdout.write(ansi.hideCursor);
|
|
315
|
+
const rowsUp = options.resized
|
|
316
|
+
? composerEraseRowsForResize({
|
|
317
|
+
renderedCursorLine,
|
|
318
|
+
renderedCursorColumn,
|
|
319
|
+
renderedWidth,
|
|
320
|
+
terminalWidth: safeTerminalWidth(),
|
|
321
|
+
})
|
|
322
|
+
: renderedCursorLine;
|
|
323
|
+
if (rowsUp > 0) {
|
|
324
|
+
stdout.write(`\x1b[${rowsUp}A`);
|
|
325
|
+
}
|
|
326
|
+
stdout.write(`\r${CLEAR_TO_END}`);
|
|
327
|
+
resetRenderedState();
|
|
328
|
+
};
|
|
329
|
+
const redraw = () => {
|
|
330
|
+
render();
|
|
331
|
+
};
|
|
332
|
+
const redrawActivity = () => {
|
|
333
|
+
if (renderedActivityLine === undefined || !this.#composerActivity || !renderedLines) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const line = renderComposerActivityLine(this.#composerActivity, safeTerminalWidth());
|
|
337
|
+
stdout.write(ansi.hideCursor);
|
|
338
|
+
moveCursorVertical(renderedActivityLine - renderedCursorLine);
|
|
339
|
+
stdout.write(`\r${CLEAR_LINE}${line}`);
|
|
340
|
+
moveCursorVertical(renderedCursorLine - renderedActivityLine);
|
|
341
|
+
stdout.write("\r");
|
|
342
|
+
if (renderedCursorColumn > 0) {
|
|
343
|
+
stdout.write(`\x1b[${renderedCursorColumn}C`);
|
|
344
|
+
}
|
|
345
|
+
stdout.write(ansi.showCursor);
|
|
346
|
+
return true;
|
|
347
|
+
};
|
|
348
|
+
const redrawWelcomeCodeIntelligence = () => {
|
|
349
|
+
const label = this.welcomeCodeIntelligenceMeta();
|
|
350
|
+
if (renderedCodeIntelligenceLine === undefined ||
|
|
351
|
+
renderedCodeIntelligenceColumn === undefined ||
|
|
352
|
+
renderedCodeIntelligenceWidth === undefined ||
|
|
353
|
+
!label ||
|
|
354
|
+
!renderedLines ||
|
|
355
|
+
!this.shouldRenderWelcomeComposer()) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
const width = safeTerminalWidth();
|
|
359
|
+
const fieldWidth = Math.max(0, Math.min(renderedCodeIntelligenceWidth, width - renderedCodeIntelligenceColumn));
|
|
360
|
+
if (fieldWidth <= 0) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const text = padRight(fg256(244, truncateToWidth(label, fieldWidth)), fieldWidth);
|
|
364
|
+
stdout.write(ansi.hideCursor);
|
|
365
|
+
moveCursorVertical(renderedCodeIntelligenceLine - renderedCursorLine);
|
|
366
|
+
stdout.write("\r");
|
|
367
|
+
if (renderedCodeIntelligenceColumn > 0) {
|
|
368
|
+
stdout.write(`\x1b[${renderedCodeIntelligenceColumn}C`);
|
|
369
|
+
}
|
|
370
|
+
stdout.write(text);
|
|
371
|
+
moveCursorVertical(renderedCursorLine - renderedCodeIntelligenceLine);
|
|
372
|
+
stdout.write("\r");
|
|
373
|
+
if (renderedCursorColumn > 0) {
|
|
374
|
+
stdout.write(`\x1b[${renderedCursorColumn}C`);
|
|
375
|
+
}
|
|
376
|
+
stdout.write(ansi.showCursor);
|
|
377
|
+
return true;
|
|
378
|
+
};
|
|
379
|
+
const cleanup = () => {
|
|
380
|
+
erase();
|
|
381
|
+
stdin.off("data", onData);
|
|
382
|
+
stdout.off("resize", onResize);
|
|
383
|
+
if (stdin.isTTY) {
|
|
384
|
+
stdin.setRawMode(false);
|
|
385
|
+
}
|
|
386
|
+
if (this.#activeComposerErase === erase) {
|
|
387
|
+
this.#activeComposerErase = undefined;
|
|
388
|
+
}
|
|
389
|
+
if (this.#activeComposerRedraw === redraw) {
|
|
390
|
+
this.#activeComposerRedraw = undefined;
|
|
391
|
+
}
|
|
392
|
+
if (this.#activeComposerActivityRedraw === redrawActivity) {
|
|
393
|
+
this.#activeComposerActivityRedraw = undefined;
|
|
394
|
+
}
|
|
395
|
+
if (this.#activeWelcomeCodeIntelligenceRedraw === redrawWelcomeCodeIntelligence) {
|
|
396
|
+
this.#activeWelcomeCodeIntelligenceRedraw = undefined;
|
|
397
|
+
}
|
|
398
|
+
stdout.write(`${BRACKETED_PASTE_DISABLE}${ansi.hideCursor}`);
|
|
399
|
+
this.resumeReadline();
|
|
400
|
+
};
|
|
401
|
+
const finish = (text) => {
|
|
402
|
+
cleanup();
|
|
403
|
+
stdout.write(ansi.reset);
|
|
404
|
+
resolve(text);
|
|
405
|
+
};
|
|
406
|
+
const render = () => {
|
|
407
|
+
const items = composerItems();
|
|
408
|
+
if (selected >= items.length) {
|
|
409
|
+
selected = 0;
|
|
410
|
+
}
|
|
411
|
+
if (forceFullRedraw && this.shouldRenderWelcomeComposer()) {
|
|
412
|
+
stdout.write(ansi.clear);
|
|
413
|
+
resetRenderedState();
|
|
414
|
+
forceFullRedraw = false;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
erase({ resized: eraseAfterResize });
|
|
418
|
+
forceFullRedraw = false;
|
|
419
|
+
eraseAfterResize = false;
|
|
420
|
+
}
|
|
421
|
+
const width = safeTerminalWidth();
|
|
422
|
+
const height = safeTerminalHeight();
|
|
423
|
+
const block = this.shouldRenderWelcomeComposer()
|
|
424
|
+
? renderWelcomeComposerSurface({
|
|
425
|
+
buffer,
|
|
426
|
+
cursor,
|
|
427
|
+
compactRanges,
|
|
428
|
+
items,
|
|
429
|
+
selected,
|
|
430
|
+
width,
|
|
431
|
+
height,
|
|
432
|
+
activity: this.#composerActivity,
|
|
433
|
+
queue: this.#composerQueue,
|
|
434
|
+
footer: this.#composerFooter,
|
|
435
|
+
workspaceRoot: this.app.workspace.root,
|
|
436
|
+
mode: this.app.config.model_setup.mode,
|
|
437
|
+
model: this.app.config.model_setup.model ?? "unconfigured",
|
|
438
|
+
contextWindow: this.configuredContextWindow(),
|
|
439
|
+
codeIntelligence: this.welcomeCodeIntelligenceMeta(),
|
|
440
|
+
placeholder: options.placeholder,
|
|
441
|
+
})
|
|
442
|
+
: renderComposerSurface({
|
|
443
|
+
buffer,
|
|
444
|
+
cursor,
|
|
445
|
+
compactRanges,
|
|
446
|
+
items,
|
|
447
|
+
selected,
|
|
448
|
+
width,
|
|
449
|
+
panel: this.#composerPanel,
|
|
450
|
+
activity: this.#composerActivity,
|
|
451
|
+
queue: this.#composerQueue,
|
|
452
|
+
footer: this.#composerFooter,
|
|
453
|
+
metadataLeft: this.composerMetadataLeft(),
|
|
454
|
+
metadataRight: this.composerMetadataRight(),
|
|
455
|
+
placeholder: options.placeholder,
|
|
456
|
+
});
|
|
457
|
+
stdout.write(block.lines.join("\n"));
|
|
458
|
+
renderedLines = block.lines.length;
|
|
459
|
+
renderedCursorLine = block.cursorLine;
|
|
460
|
+
renderedCursorColumn = block.cursorColumn;
|
|
461
|
+
renderedWidth = width;
|
|
462
|
+
renderedActivityLine = block.activityLine;
|
|
463
|
+
renderedCodeIntelligenceLine = block.codeIntelligenceLine;
|
|
464
|
+
renderedCodeIntelligenceColumn = block.codeIntelligenceColumn;
|
|
465
|
+
renderedCodeIntelligenceWidth = block.codeIntelligenceWidth;
|
|
466
|
+
const up = Math.max(0, renderedLines - 1 - block.cursorLine);
|
|
467
|
+
if (up > 0) {
|
|
468
|
+
stdout.write(`\x1b[${up}A`);
|
|
469
|
+
}
|
|
470
|
+
stdout.write("\r");
|
|
471
|
+
if (block.cursorColumn > 0) {
|
|
472
|
+
stdout.write(`\x1b[${block.cursorColumn}C`);
|
|
473
|
+
}
|
|
474
|
+
stdout.write(ansi.showCursor);
|
|
475
|
+
};
|
|
476
|
+
this.#activeComposerErase = erase;
|
|
477
|
+
this.#activeComposerRedraw = redraw;
|
|
478
|
+
this.#activeComposerActivityRedraw = redrawActivity;
|
|
479
|
+
this.#activeWelcomeCodeIntelligenceRedraw = redrawWelcomeCodeIntelligence;
|
|
480
|
+
const composerItems = () => options.suggestions === false ? [] : this.composerSuggestions(buffer, skills);
|
|
481
|
+
const completeSelection = () => {
|
|
482
|
+
const items = composerItems();
|
|
483
|
+
const item = items[selected];
|
|
484
|
+
if (!item) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
buffer = item.value;
|
|
488
|
+
cursor = buffer.length;
|
|
489
|
+
compactRanges = [];
|
|
490
|
+
selected = 0;
|
|
491
|
+
selectionTouched = false;
|
|
492
|
+
render();
|
|
493
|
+
return true;
|
|
494
|
+
};
|
|
495
|
+
const insertText = (text) => {
|
|
496
|
+
const safeCursor = cursor;
|
|
497
|
+
const next = insertComposerText(buffer, cursor, text);
|
|
498
|
+
buffer = next.buffer;
|
|
499
|
+
cursor = next.cursor;
|
|
500
|
+
compactRanges = adjustComposerCompactRanges(compactRanges, safeCursor, safeCursor, text.length);
|
|
501
|
+
selected = 0;
|
|
502
|
+
selectionTouched = false;
|
|
503
|
+
render();
|
|
504
|
+
};
|
|
505
|
+
const insertPaste = (text) => {
|
|
506
|
+
const safeCursor = cursor;
|
|
507
|
+
const next = insertComposerPaste(buffer, cursor, normalizeComposerPastedInput(text));
|
|
508
|
+
buffer = next.buffer;
|
|
509
|
+
cursor = next.cursor;
|
|
510
|
+
compactRanges = adjustComposerCompactRanges(compactRanges, safeCursor, safeCursor, next.cursor - safeCursor);
|
|
511
|
+
if (next.compactRange) {
|
|
512
|
+
compactRanges.push(next.compactRange);
|
|
513
|
+
}
|
|
514
|
+
selected = 0;
|
|
515
|
+
selectionTouched = false;
|
|
516
|
+
render();
|
|
517
|
+
};
|
|
518
|
+
const deleteRange = (start, end) => {
|
|
519
|
+
buffer = `${buffer.slice(0, start)}${buffer.slice(end)}`;
|
|
520
|
+
cursor = start;
|
|
521
|
+
compactRanges = adjustComposerCompactRanges(compactRanges, start, end, 0);
|
|
522
|
+
selected = 0;
|
|
523
|
+
selectionTouched = false;
|
|
524
|
+
render();
|
|
525
|
+
};
|
|
526
|
+
const submit = () => {
|
|
527
|
+
const items = composerItems();
|
|
528
|
+
const item = items[selected];
|
|
529
|
+
const trimmed = buffer.trim();
|
|
530
|
+
const prompt = compactRanges.length ? buffer : trimmed;
|
|
531
|
+
if (!prompt.trim()) {
|
|
532
|
+
render();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (trimmed === "/" && item) {
|
|
536
|
+
finish(item.value);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (trimmed === "$" && item && selectionTouched) {
|
|
540
|
+
finish(item.value);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
finish(prompt);
|
|
544
|
+
};
|
|
545
|
+
const onData = (chunk) => {
|
|
546
|
+
if (this.#inputModalActive) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const rawInput = chunk.toString("utf8");
|
|
550
|
+
const pastedFallback = composerPlainPasteFallback(rawInput);
|
|
551
|
+
if (pastedFallback !== undefined) {
|
|
552
|
+
insertPaste(pastedFallback);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
let done = false;
|
|
556
|
+
for (const key of terminalInputTokens(rawInput, pasteState)) {
|
|
557
|
+
const pasted = pasteTokenContent(key);
|
|
558
|
+
if (pasted !== undefined) {
|
|
559
|
+
insertPaste(pasted);
|
|
560
|
+
}
|
|
561
|
+
else if (key === "\u0003") {
|
|
562
|
+
finish("/exit");
|
|
563
|
+
done = true;
|
|
564
|
+
}
|
|
565
|
+
else if (key === "\u001b") {
|
|
566
|
+
if (buffer) {
|
|
567
|
+
buffer = "";
|
|
568
|
+
cursor = 0;
|
|
569
|
+
compactRanges = [];
|
|
570
|
+
selected = 0;
|
|
571
|
+
selectionTouched = false;
|
|
572
|
+
render();
|
|
573
|
+
}
|
|
574
|
+
else if (this.interruptActiveLoop()) {
|
|
575
|
+
render();
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
finish("");
|
|
579
|
+
done = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (key === "\u001b[A") {
|
|
583
|
+
const count = composerItems().length;
|
|
584
|
+
if (count) {
|
|
585
|
+
selected = (selected - 1 + count) % count;
|
|
586
|
+
selectionTouched = true;
|
|
587
|
+
render();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else if (key === "\u001b[B") {
|
|
591
|
+
const count = composerItems().length;
|
|
592
|
+
if (count) {
|
|
593
|
+
selected = (selected + 1) % count;
|
|
594
|
+
selectionTouched = true;
|
|
595
|
+
render();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else if (key === "\u001b[C") {
|
|
599
|
+
cursor = moveComposerCursorRight(buffer, cursor);
|
|
600
|
+
render();
|
|
601
|
+
}
|
|
602
|
+
else if (key === "\u001b[D") {
|
|
603
|
+
cursor = moveComposerCursorLeft(buffer, cursor);
|
|
604
|
+
render();
|
|
605
|
+
}
|
|
606
|
+
else if (key === "\u001b[H" || key === "\u001b[1~") {
|
|
607
|
+
cursor = moveComposerCursorHome(buffer, cursor);
|
|
608
|
+
render();
|
|
609
|
+
}
|
|
610
|
+
else if (key === "\u001b[F" || key === "\u001b[4~") {
|
|
611
|
+
cursor = moveComposerCursorEnd(buffer, cursor);
|
|
612
|
+
render();
|
|
613
|
+
}
|
|
614
|
+
else if (key === "\t") {
|
|
615
|
+
const subcommandRoot = slashCommandWithSubcommands(buffer);
|
|
616
|
+
if (subcommandRoot && !buffer.trim().includes(" ")) {
|
|
617
|
+
buffer = `/${subcommandRoot} `;
|
|
618
|
+
cursor = buffer.length;
|
|
619
|
+
compactRanges = [];
|
|
620
|
+
selected = 0;
|
|
621
|
+
selectionTouched = false;
|
|
622
|
+
render();
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
completeSelection();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (key === "shift-enter") {
|
|
629
|
+
insertText("\n");
|
|
630
|
+
}
|
|
631
|
+
else if (key === "\r" || key === "\n") {
|
|
632
|
+
submit();
|
|
633
|
+
done = true;
|
|
634
|
+
}
|
|
635
|
+
else if (key === "\u0014") {
|
|
636
|
+
finish("/tools expand");
|
|
637
|
+
done = true;
|
|
638
|
+
}
|
|
639
|
+
else if (key === "\u007f") {
|
|
640
|
+
const compactRange = compactRangeBeforeCursor(compactRanges, cursor);
|
|
641
|
+
if (compactRange) {
|
|
642
|
+
deleteRange(compactRange.start, compactRange.end);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
const oldCursor = cursor;
|
|
646
|
+
const next = backspaceComposer(buffer, cursor);
|
|
647
|
+
buffer = next.buffer;
|
|
648
|
+
cursor = next.cursor;
|
|
649
|
+
compactRanges = adjustComposerCompactRanges(compactRanges, cursor, oldCursor, 0);
|
|
650
|
+
selected = 0;
|
|
651
|
+
selectionTouched = false;
|
|
652
|
+
render();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
else if (isPrintableInput(key)) {
|
|
656
|
+
insertText(printableText(key));
|
|
657
|
+
}
|
|
658
|
+
if (done) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const onResize = () => {
|
|
664
|
+
forceFullRedraw = this.shouldRenderWelcomeComposer();
|
|
665
|
+
eraseAfterResize = !forceFullRedraw;
|
|
666
|
+
render();
|
|
667
|
+
};
|
|
668
|
+
stdin.setRawMode(true);
|
|
669
|
+
stdin.resume();
|
|
670
|
+
stdin.on("data", onData);
|
|
671
|
+
stdout.on("resize", onResize);
|
|
672
|
+
render();
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
shouldRenderWelcomeComposer() {
|
|
676
|
+
return !this.#hasTranscript && !this.#sessionId && !this.#activeAbort && !this.#promptWorker && !this.#promptWorkerScheduled;
|
|
677
|
+
}
|
|
678
|
+
composerSuggestions(buffer, skills) {
|
|
679
|
+
if (buffer.startsWith("/")) {
|
|
680
|
+
const lower = buffer.toLowerCase();
|
|
681
|
+
const subcommandMatch = lower.match(/^\/([a-z]+)\s+(.*)$/);
|
|
682
|
+
if (subcommandMatch) {
|
|
683
|
+
const commandName = subcommandMatch[1];
|
|
684
|
+
const query = subcommandMatch[2]?.trim().toLowerCase() ?? "";
|
|
685
|
+
return slashSubcommands(commandName)
|
|
686
|
+
.filter((item) => !query || `${item.value} ${item.description}`.toLowerCase().includes(query))
|
|
687
|
+
.sort((a, b) => commandScore(a.name, a.description, query) - commandScore(b.name, b.description, query))
|
|
688
|
+
.slice(0, 8)
|
|
689
|
+
.map((item) => ({
|
|
690
|
+
value: item.value,
|
|
691
|
+
label: item.value,
|
|
692
|
+
description: item.description,
|
|
693
|
+
kind: "command",
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
const query = buffer.slice(1).trim().toLowerCase();
|
|
697
|
+
return SLASH_COMMANDS.filter((command) => !query || `${command.name} ${command.description}`.toLowerCase().includes(query))
|
|
698
|
+
.sort((a, b) => commandScore(a.name, a.description, query) - commandScore(b.name, b.description, query))
|
|
699
|
+
.slice(0, 8)
|
|
700
|
+
.map((command) => ({
|
|
701
|
+
value: `/${command.name}`,
|
|
702
|
+
label: `/${command.name}`,
|
|
703
|
+
description: command.description,
|
|
704
|
+
kind: "command",
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
if (buffer.startsWith("$")) {
|
|
708
|
+
const query = buffer.slice(1).trim().toLowerCase();
|
|
709
|
+
const enabled = new Set(this.app.config.skills.enabled);
|
|
710
|
+
return skills
|
|
711
|
+
.filter((skill) => !query || `${skill.id} ${skill.name} ${skill.description}`.toLowerCase().includes(query))
|
|
712
|
+
.sort((a, b) => {
|
|
713
|
+
const aEnabled = enabled.has(a.id) || enabled.has(a.name);
|
|
714
|
+
const bEnabled = enabled.has(b.id) || enabled.has(b.name);
|
|
715
|
+
if (aEnabled !== bEnabled) {
|
|
716
|
+
return aEnabled ? -1 : 1;
|
|
717
|
+
}
|
|
718
|
+
return a.name.localeCompare(b.name);
|
|
719
|
+
})
|
|
720
|
+
.slice(0, 8)
|
|
721
|
+
.map((skill) => ({
|
|
722
|
+
value: `$ ${skill.id}`,
|
|
723
|
+
label: skill.name,
|
|
724
|
+
description: `${enabled.has(skill.id) || enabled.has(skill.name) ? "enabled" : "disabled"} · ${skill.description}`,
|
|
725
|
+
kind: "skill",
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
renderHome() {
|
|
731
|
+
stdout.write(ansi.clear);
|
|
732
|
+
if (!this.shouldRenderWelcomeComposer()) {
|
|
733
|
+
this.writeHomeFrame();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
startWelcomeCodeIntelligenceIndexing() {
|
|
737
|
+
const hub = this.app.runtime.codeIntelligence;
|
|
738
|
+
if (this.#welcomeCodeIntelligenceStarted || !hub.shouldStartOnWelcome()) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.#welcomeCodeIntelligenceStarted = true;
|
|
742
|
+
const unsubscribe = hub.onStatus(() => {
|
|
743
|
+
if (!this.#activeWelcomeCodeIntelligenceRedraw?.()) {
|
|
744
|
+
this.#activeComposerRedraw?.();
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
this.#welcomeCodeIntelligenceStop = () => {
|
|
748
|
+
unsubscribe();
|
|
749
|
+
this.#welcomeCodeIntelligenceStop = undefined;
|
|
750
|
+
};
|
|
751
|
+
hub.startIndexing("welcome").finally(() => {
|
|
752
|
+
this.#welcomeCodeIntelligenceStop?.();
|
|
753
|
+
if (!this.#activeWelcomeCodeIntelligenceRedraw?.()) {
|
|
754
|
+
this.#activeComposerRedraw?.();
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
promptRequiresCodeIntelligenceGate() {
|
|
759
|
+
const hub = this.app.runtime.codeIntelligence;
|
|
760
|
+
if (!hub.requireReadyBeforeChat()) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
const state = hub.status().codegraph.state;
|
|
764
|
+
return state !== "ready" && state !== "off";
|
|
765
|
+
}
|
|
766
|
+
async waitForCodeIntelligenceBeforeChat() {
|
|
767
|
+
const hub = this.app.runtime.codeIntelligence;
|
|
768
|
+
if (!hub.requireReadyBeforeChat()) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
let status = hub.status().codegraph;
|
|
772
|
+
if (status.state === "ready" || status.state === "off") {
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
const activity = this.startActivityIndicator(codeIntelligenceActivityLabel(status));
|
|
776
|
+
const unsubscribe = hub.onStatus((next) => activity.status(codeIntelligenceActivityLabel(next)));
|
|
777
|
+
try {
|
|
778
|
+
status = await hub.waitUntilReadyBeforeChat();
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
unsubscribe();
|
|
782
|
+
activity.stop();
|
|
783
|
+
}
|
|
784
|
+
if (status.state === "ready" || status.state === "off") {
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
if (status.state !== "degraded") {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
const choice = await this.selectOption("Context Optimization", [
|
|
791
|
+
{ value: "retry", label: "Retry indexing", description: "Rebuild indexed context before sending this prompt." },
|
|
792
|
+
{ value: "continue", label: "Continue", description: "Send this prompt without indexed context for this turn." },
|
|
793
|
+
], 0, [
|
|
794
|
+
fg256(203, truncateToWidth(status.error ?? "Context optimization is unavailable.", Math.max(24, terminalWidth() - 8))),
|
|
795
|
+
fg256(244, "No chat request has been sent yet."),
|
|
796
|
+
]).catch(() => "continue");
|
|
797
|
+
if (choice === "retry") {
|
|
798
|
+
const retry = await hub.startIndexing("chat_retry", { force: true });
|
|
799
|
+
if (retry.state === "ready") {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return await this.waitForCodeIntelligenceBeforeChat();
|
|
803
|
+
}
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
welcomeCodeIntelligenceMeta() {
|
|
807
|
+
const status = this.app.runtime.codeIntelligence.status().codegraph;
|
|
808
|
+
if (status.provider === "off" || status.provider === "builtin") {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
if (status.state === "ready") {
|
|
812
|
+
return status.files ? `indexed ${status.files} files` : "indexed";
|
|
813
|
+
}
|
|
814
|
+
if (status.state === "degraded") {
|
|
815
|
+
return "index degraded";
|
|
816
|
+
}
|
|
817
|
+
if (status.state === "indexing" || status.state === "syncing") {
|
|
818
|
+
return `index ${codeIntelligenceProgress(status)}`;
|
|
819
|
+
}
|
|
820
|
+
return "index pending";
|
|
821
|
+
}
|
|
822
|
+
writeHomeFrame() {
|
|
823
|
+
stdout.write(renderHomeFrame({
|
|
824
|
+
workspaceRoot: this.app.workspace.root,
|
|
825
|
+
mode: this.app.config.model_setup.mode,
|
|
826
|
+
model: this.app.config.model_setup.model ?? "unconfigured",
|
|
827
|
+
width: safeTerminalWidth(),
|
|
828
|
+
}).join("\n"));
|
|
829
|
+
stdout.write("\n\n");
|
|
830
|
+
}
|
|
831
|
+
configuredContextWindow() {
|
|
832
|
+
return this.app.config.model_setup.context_window ?? this.app.config.context.context_window;
|
|
833
|
+
}
|
|
834
|
+
async askModeObjective(label, defaultValue) {
|
|
835
|
+
const previousPanel = this.#composerPanel;
|
|
836
|
+
this.#composerPanel = {
|
|
837
|
+
lines: [
|
|
838
|
+
` ${fg256(39, label)}`,
|
|
839
|
+
` ${fg256(244, "enter submit · esc cancel")}`,
|
|
840
|
+
],
|
|
841
|
+
};
|
|
842
|
+
try {
|
|
843
|
+
return await this.readComposer({
|
|
844
|
+
placeholder: label,
|
|
845
|
+
initialBuffer: defaultValue,
|
|
846
|
+
suggestions: false,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
this.#composerPanel = previousPanel;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
composerMetadataLeft() {
|
|
854
|
+
const model = compactModelLabel(this.app.config.model_setup.model ?? "unconfigured");
|
|
855
|
+
return [
|
|
856
|
+
fg256(75, compactWorkspacePath(this.app.workspace.root)),
|
|
857
|
+
fg256(238, "·"),
|
|
858
|
+
fg256(252, model),
|
|
859
|
+
].join(" ");
|
|
860
|
+
}
|
|
861
|
+
composerMetadataRight() {
|
|
862
|
+
const session = this.optionalSession();
|
|
863
|
+
if (!session) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
const plan = readPlanState(this.app.store, session.session_id);
|
|
867
|
+
const autoresearch = readAutoresearchState(this.app.store, session.session_id);
|
|
868
|
+
const goal = readGoalState(this.app.store, session.session_id);
|
|
869
|
+
return renderModeMetadataRight({ plan, autoresearch, goal });
|
|
870
|
+
}
|
|
871
|
+
inputPrompt() {
|
|
872
|
+
return `\n${bgLine(236, `› ${fg256(244, "Ask Inferoa")}`, safeTerminalWidth())}`;
|
|
873
|
+
}
|
|
874
|
+
async openView(command, args) {
|
|
875
|
+
if (command !== "clear" && command !== "exit") {
|
|
876
|
+
this.enterChatSurfaceFromWelcome();
|
|
877
|
+
}
|
|
878
|
+
const previousInline = this.#inlineMode;
|
|
879
|
+
this.#inlineMode = true;
|
|
880
|
+
this.#inlineRenderedLines = 0;
|
|
881
|
+
try {
|
|
882
|
+
switch (command) {
|
|
883
|
+
case "setup":
|
|
884
|
+
await this.renderSetupView();
|
|
885
|
+
return;
|
|
886
|
+
case "model":
|
|
887
|
+
await this.renderModelView(args);
|
|
888
|
+
return;
|
|
889
|
+
case "system":
|
|
890
|
+
await this.renderEndpointView();
|
|
891
|
+
return;
|
|
892
|
+
case "skills":
|
|
893
|
+
await this.renderSkillsView(args);
|
|
894
|
+
return;
|
|
895
|
+
case "goal":
|
|
896
|
+
await this.renderGoalView(args);
|
|
897
|
+
return;
|
|
898
|
+
case "plan":
|
|
899
|
+
await this.renderPlanView(args);
|
|
900
|
+
return;
|
|
901
|
+
case "autoresearch":
|
|
902
|
+
await this.renderAutoresearchView(args);
|
|
903
|
+
return;
|
|
904
|
+
case "cache":
|
|
905
|
+
this.renderCacheView();
|
|
906
|
+
return;
|
|
907
|
+
case "context":
|
|
908
|
+
await this.renderContextView(args);
|
|
909
|
+
return;
|
|
910
|
+
case "tools":
|
|
911
|
+
this.renderToolsView(args);
|
|
912
|
+
return;
|
|
913
|
+
case "sessions":
|
|
914
|
+
await this.renderSessionsView(args);
|
|
915
|
+
return;
|
|
916
|
+
case "activity":
|
|
917
|
+
this.renderFormattedEventView("Activity", isActivityEvent, renderSessionActivityLines);
|
|
918
|
+
return;
|
|
919
|
+
case "jobs":
|
|
920
|
+
await this.renderJobsView(args);
|
|
921
|
+
return;
|
|
922
|
+
case "todo":
|
|
923
|
+
this.renderFormattedEventView("Todo", (event) => event.type === "todo.updated", renderTodoEventLines);
|
|
924
|
+
return;
|
|
925
|
+
case "acceptance":
|
|
926
|
+
await this.renderAcceptanceView(args);
|
|
927
|
+
return;
|
|
928
|
+
case "help":
|
|
929
|
+
this.renderHelp();
|
|
930
|
+
return;
|
|
931
|
+
case "clear":
|
|
932
|
+
await this.startFreshSessionFromClear();
|
|
933
|
+
return;
|
|
934
|
+
case "resume":
|
|
935
|
+
await this.renderResumeSessionView(args);
|
|
936
|
+
return;
|
|
937
|
+
case "exit":
|
|
938
|
+
await this.requestExit();
|
|
939
|
+
return;
|
|
940
|
+
default:
|
|
941
|
+
this.renderNotice(`Unhandled command: /${command} ${args}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
this.#inlineMode = previousInline;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
enterChatSurfaceFromWelcome() {
|
|
949
|
+
if (!this.shouldRenderWelcomeComposer()) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
stdout.write(ansi.clear);
|
|
953
|
+
this.writeHomeFrame();
|
|
954
|
+
this.#hasTranscript = true;
|
|
955
|
+
}
|
|
956
|
+
handleViewError(error) {
|
|
957
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
958
|
+
if (message.includes("cancelled")) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
this.renderNotice(message);
|
|
962
|
+
}
|
|
963
|
+
async renderSetupView() {
|
|
964
|
+
const nextConfig = structuredClone(this.app.config);
|
|
965
|
+
this.renderCenteredPanel("Setup", [
|
|
966
|
+
setupProgress(1, SETUP_TOTAL_STEPS, "provider"),
|
|
967
|
+
"",
|
|
968
|
+
`${fg256(252, "Model endpoint")}`,
|
|
969
|
+
fg256(244, "Configure chat, context window, web search, and optional Omni endpoints."),
|
|
970
|
+
"",
|
|
971
|
+
`${fg256(244, "current")} ${this.describeModelSetup(this.app.config.model_setup)}`,
|
|
972
|
+
`${fg256(244, "config")} ${userConfigPath()}`,
|
|
973
|
+
], true);
|
|
974
|
+
const provider = await this.chooseProvider();
|
|
975
|
+
this.applyProviderChoice(nextConfig.model_setup, provider);
|
|
976
|
+
this.renderCenteredPanel("Setup", [setupProgress(2, SETUP_TOTAL_STEPS, "endpoint"), "", fg256(244, "OpenAI-compatible endpoint URL.")], true);
|
|
977
|
+
nextConfig.model_setup.base_url = await this.askRequired("Chat endpoint base URL", nextConfig.model_setup.base_url ?? defaultBaseUrl(provider));
|
|
978
|
+
this.applyApiKeySelection(nextConfig.model_setup, await this.askApiKeySelection(secretRef(`chat-${provider}-${nextConfig.model_setup.base_url ?? "endpoint"}`, "api-key"), `${providerLabel(provider)} API key`, nextConfig.model_setup.api_key_ref, provider === "external"));
|
|
979
|
+
delete nextConfig.model_setup.api_key;
|
|
980
|
+
this.renderCenteredPanel("Setup", [setupProgress(3, SETUP_TOTAL_STEPS, "model"), "", fg256(244, "Listing endpoint models.")], true);
|
|
981
|
+
const chatProbe = await this.probeChatModels(nextConfig);
|
|
982
|
+
nextConfig.model_setup.model = await this.pickModel("Chat model", chatProbe, nextConfig.model_setup.model);
|
|
983
|
+
this.renderCenteredPanel("Setup", [setupProgress(4, SETUP_TOTAL_STEPS, "context"), "", fg256(244, "Model context window in tokens.")], true);
|
|
984
|
+
const currentContextWindow = nextConfig.model_setup.context_window ?? nextConfig.context.context_window;
|
|
985
|
+
const contextWindowInput = await this.ask("Context window tokens", String(currentContextWindow));
|
|
986
|
+
nextConfig.model_setup.context_window = normalizeContextWindowInput(contextWindowInput, currentContextWindow);
|
|
987
|
+
nextConfig.context.context_window = nextConfig.model_setup.context_window;
|
|
988
|
+
this.renderCenteredPanel("Setup", [setupProgress(5, SETUP_TOTAL_STEPS, "web"), "", fg256(244, "Keyword web search provider.")], true);
|
|
989
|
+
await this.configureWebSearch(nextConfig);
|
|
990
|
+
this.renderCenteredPanel("Setup", [setupProgress(6, SETUP_TOTAL_STEPS, "omni"), "", fg256(244, "Optional multimodal endpoints.")], true);
|
|
991
|
+
if (await this.confirm("Configure Omni multimodal endpoints now?", this.app.config.omni.enabled)) {
|
|
992
|
+
nextConfig.omni.enabled = true;
|
|
993
|
+
for (const capability of TUI_OMNI_SETUP_CAPABILITIES) {
|
|
994
|
+
nextConfig.omni.endpoints[capability.name] = await this.configureOmniEndpoint(capability.name, capability.label, nextConfig.omni.endpoints[capability.name], capability.requiredForAcceptance);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
nextConfig.omni.enabled = false;
|
|
999
|
+
nextConfig.omni.endpoints = {};
|
|
1000
|
+
}
|
|
1001
|
+
if (!(await this.reviewSetupBeforeSave(nextConfig))) {
|
|
1002
|
+
this.renderNotice("Setup cancelled. No config was saved.");
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const target = await saveUserConfig(nextConfig);
|
|
1006
|
+
Object.assign(this.app.config, nextConfig);
|
|
1007
|
+
if (!this.app.configFiles.includes(target)) {
|
|
1008
|
+
this.app.configFiles.push(target);
|
|
1009
|
+
}
|
|
1010
|
+
this.#hasTranscript = false;
|
|
1011
|
+
this.#composerFooter = undefined;
|
|
1012
|
+
this.#composerQueue = undefined;
|
|
1013
|
+
this.#composerPanel = undefined;
|
|
1014
|
+
this.renderHome();
|
|
1015
|
+
}
|
|
1016
|
+
async reviewSetupBeforeSave(config) {
|
|
1017
|
+
const action = await this.reviewAction("Review Setup", () => setupReviewLinesForDisplay(config, setupDialogContentWidth()));
|
|
1018
|
+
return action === "save";
|
|
1019
|
+
}
|
|
1020
|
+
async chooseProvider() {
|
|
1021
|
+
const options = [
|
|
1022
|
+
{ value: "direct", label: "Direct", description: "Use your vLLM endpoint for the fastest, most predictable path" },
|
|
1023
|
+
{ value: "auto", label: "Auto", description: "Let vLLM Semantic Router choose the best route for each request" },
|
|
1024
|
+
{ value: "external", label: "External", description: "Connect a hosted OpenAI-compatible provider when you need one" },
|
|
1025
|
+
];
|
|
1026
|
+
const current = this.app.config.model_setup.mode === "auto" ? "auto" : this.app.config.model_setup.provider === "external" ? "external" : "direct";
|
|
1027
|
+
const defaultIndex = Math.max(0, options.findIndex((option) => option.value === current));
|
|
1028
|
+
return await this.selectOption("Provider", options, defaultIndex);
|
|
1029
|
+
}
|
|
1030
|
+
async chooseSlashCommand(query = "") {
|
|
1031
|
+
const normalized = query.toLowerCase();
|
|
1032
|
+
const commands = SLASH_COMMANDS.filter((command) => !normalized || `${command.name} ${command.description}`.toLowerCase().includes(normalized));
|
|
1033
|
+
if (!commands.length) {
|
|
1034
|
+
this.renderNotice(`No command matched ${query}.`);
|
|
1035
|
+
return "help";
|
|
1036
|
+
}
|
|
1037
|
+
return await this.selectOption("Commands", commands.map((command) => ({
|
|
1038
|
+
value: command.name,
|
|
1039
|
+
label: `/${command.name}`,
|
|
1040
|
+
description: command.description,
|
|
1041
|
+
})));
|
|
1042
|
+
}
|
|
1043
|
+
applyProviderChoice(setup, provider) {
|
|
1044
|
+
if (provider === "auto") {
|
|
1045
|
+
setup.mode = "auto";
|
|
1046
|
+
setup.provider = "vllm";
|
|
1047
|
+
setup.router = "vllm-sr";
|
|
1048
|
+
setup.profile = "openai_compatible";
|
|
1049
|
+
setup.base_url = setup.base_url ?? defaultBaseUrl(provider);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
setup.mode = "direct";
|
|
1053
|
+
delete setup.router;
|
|
1054
|
+
setup.profile = "openai_compatible";
|
|
1055
|
+
setup.provider = provider === "external" ? "external" : "vllm";
|
|
1056
|
+
setup.base_url = setup.base_url ?? defaultBaseUrl(provider);
|
|
1057
|
+
}
|
|
1058
|
+
async configureWebSearch(config) {
|
|
1059
|
+
const provider = await this.chooseWebSearchProvider(config.web_search.provider);
|
|
1060
|
+
config.web_search.provider = provider;
|
|
1061
|
+
delete config.web_search.api_key;
|
|
1062
|
+
if (provider === "auto" || provider === "off") {
|
|
1063
|
+
delete config.web_search.base_url;
|
|
1064
|
+
delete config.web_search.api_key_ref;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (provider === "brave") {
|
|
1068
|
+
delete config.web_search.base_url;
|
|
1069
|
+
this.applyApiKeySelection(config.web_search, await this.askApiKeySelection(secretRef("web-brave-api-key", "api-key"), "Brave Search API key", config.web_search.api_key_ref, true));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (provider === "jina") {
|
|
1073
|
+
delete config.web_search.base_url;
|
|
1074
|
+
this.applyApiKeySelection(config.web_search, await this.askApiKeySelection(secretRef("web-jina-api-key", "api-key"), "Jina Search API key", config.web_search.api_key_ref));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (provider === "searxng" || provider === "custom") {
|
|
1078
|
+
config.web_search.base_url = await this.askRequired(`${webSearchProviderLabel(provider)} base URL`, config.web_search.base_url ?? "http://localhost:8080");
|
|
1079
|
+
this.applyApiKeySelection(config.web_search, await this.askApiKeySelection(secretRef(`web-${provider}`, "api-key"), `${webSearchProviderLabel(provider)} API key`, config.web_search.api_key_ref));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
delete config.web_search.base_url;
|
|
1083
|
+
delete config.web_search.api_key_ref;
|
|
1084
|
+
}
|
|
1085
|
+
async chooseWebSearchProvider(current) {
|
|
1086
|
+
const options = webSearchProviderSetupOptions();
|
|
1087
|
+
const defaultIndex = Math.max(0, options.findIndex((option) => option.value === current));
|
|
1088
|
+
return await this.selectOption("Web Search", options, defaultIndex, [
|
|
1089
|
+
fg256(244, "Direct URLs are fetched by web_fetch even when keyword search uses fallback."),
|
|
1090
|
+
]);
|
|
1091
|
+
}
|
|
1092
|
+
async configureOmniEndpoint(name, label, current, requiredForAcceptance = false) {
|
|
1093
|
+
const suffix = requiredForAcceptance ? "required for final acceptance" : "optional";
|
|
1094
|
+
const shouldConfigure = await this.confirm(`${label}: configure endpoint? (${suffix})`, Boolean(current?.base_url && current.model));
|
|
1095
|
+
if (!shouldConfigure) {
|
|
1096
|
+
return current;
|
|
1097
|
+
}
|
|
1098
|
+
const endpoint = { ...(current ?? {}) };
|
|
1099
|
+
endpoint.base_url = await this.askRequired(`${label} base URL`, endpoint.base_url ?? this.app.config.model_setup.base_url ?? "http://localhost:8000/v1");
|
|
1100
|
+
this.applyApiKeySelection(endpoint, await this.askApiKeySelection(secretRef(`omni-${name}-${endpoint.base_url}`, "api-key"), `${label} API key`, endpoint.api_key_ref));
|
|
1101
|
+
delete endpoint.api_key;
|
|
1102
|
+
const probe = await this.probeOpenAiModels(endpoint);
|
|
1103
|
+
endpoint.model = await this.pickModel(`${label} model`, probe, endpoint.model);
|
|
1104
|
+
return endpoint;
|
|
1105
|
+
}
|
|
1106
|
+
async configureSkillSelection(config, query = "") {
|
|
1107
|
+
const discovered = await new SkillRegistry(this.app.workspace, config).discover();
|
|
1108
|
+
const normalizedQuery = query.toLowerCase();
|
|
1109
|
+
const skills = discovered
|
|
1110
|
+
.filter((skill) => !normalizedQuery || `${skill.id} ${skill.name} ${skill.description}`.toLowerCase().includes(normalizedQuery))
|
|
1111
|
+
.sort((a, b) => {
|
|
1112
|
+
const enabled = new Set(config.skills.enabled);
|
|
1113
|
+
const aEnabled = enabled.has(a.id) || enabled.has(a.name);
|
|
1114
|
+
const bEnabled = enabled.has(b.id) || enabled.has(b.name);
|
|
1115
|
+
if (aEnabled !== bEnabled) {
|
|
1116
|
+
return aEnabled ? -1 : 1;
|
|
1117
|
+
}
|
|
1118
|
+
return a.name.localeCompare(b.name);
|
|
1119
|
+
});
|
|
1120
|
+
if (!skills.length) {
|
|
1121
|
+
this.renderCenteredPanel("Skills", [query ? `No skills matched ${query}.` : "No skills discovered."], true);
|
|
1122
|
+
return config.skills.enabled;
|
|
1123
|
+
}
|
|
1124
|
+
const enabled = new Set(config.skills.enabled);
|
|
1125
|
+
const visible = skills.slice(0, 160);
|
|
1126
|
+
return await this.multiSelect("Skills", visible.map((skill) => ({
|
|
1127
|
+
value: skill.id,
|
|
1128
|
+
label: skill.name,
|
|
1129
|
+
description: `${skill.trust} · ${skill.description}`,
|
|
1130
|
+
})), visible.map((skill) => enabled.has(skill.id) || enabled.has(skill.name)), [
|
|
1131
|
+
fg256(244, "Only the index is injected into the prompt. skill_read loads details on demand."),
|
|
1132
|
+
...(skills.length > visible.length ? [fg256(244, `${skills.length - visible.length} more hidden; open /skills with a filter.`)] : []),
|
|
1133
|
+
]);
|
|
1134
|
+
}
|
|
1135
|
+
async probeChatModels(config) {
|
|
1136
|
+
this.renderCenteredPanel("Model Discovery", [
|
|
1137
|
+
`${fg256(39, "GET")} ${(config.model_setup.base_url ?? "").replace(/\/$/, "")}/models`,
|
|
1138
|
+
fg256(243, "Listing models before selection."),
|
|
1139
|
+
], true);
|
|
1140
|
+
const snapshot = await new EndpointSignals(config).snapshot();
|
|
1141
|
+
return {
|
|
1142
|
+
models: modelsFromSnapshot(snapshot),
|
|
1143
|
+
errors: snapshot.errors ?? [],
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
async probeOpenAiModels(endpoint) {
|
|
1147
|
+
if (!endpoint.base_url) {
|
|
1148
|
+
return { models: [], errors: ["base_url is required"] };
|
|
1149
|
+
}
|
|
1150
|
+
const base = endpoint.base_url.replace(/\/$/, "");
|
|
1151
|
+
this.renderCenteredPanel("Model Discovery", [`${fg256(39, "GET")} ${base}/models`, fg256(243, "Listing multimodal endpoint models.")], true);
|
|
1152
|
+
try {
|
|
1153
|
+
const response = await fetch(`${base}/models`, {
|
|
1154
|
+
method: "GET",
|
|
1155
|
+
headers: authHeaders(endpoint),
|
|
1156
|
+
});
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
return { models: [], errors: [`/models returned ${response.status}`] };
|
|
1159
|
+
}
|
|
1160
|
+
const json = (await response.json());
|
|
1161
|
+
const data = Array.isArray(json.data) ? dataAsJsonObjects(json.data) : [];
|
|
1162
|
+
return { models: data.map((model) => stringField(model.id) ?? stringField(model.name)).filter((model) => Boolean(model)), errors: [] };
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
return { models: [], errors: [`/models unavailable: ${error instanceof Error ? error.message : String(error)}`] };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async pickModel(title, probe, current) {
|
|
1169
|
+
if (probe.models.length) {
|
|
1170
|
+
const defaultModel = current && probe.models.includes(current) ? current : probe.models[0] ?? current;
|
|
1171
|
+
const defaultIndex = defaultModel && probe.models.includes(defaultModel) ? probe.models.indexOf(defaultModel) : 0;
|
|
1172
|
+
return await this.selectOption(title, probe.models.map((model) => ({ value: model, label: model })), defaultIndex, probe.errors.length ? [fg256(203, "Probe errors"), ...probe.errors.map((error) => ` ${error}`)] : []);
|
|
1173
|
+
}
|
|
1174
|
+
this.renderCenteredPanel(title, ["No models returned. Type a model id manually.", ...probe.errors.map((error) => fg256(203, error))]);
|
|
1175
|
+
while (true) {
|
|
1176
|
+
const answer = await this.ask("Model id", current);
|
|
1177
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
1178
|
+
const selected = probe.models[index] ?? (answer.trim() || current);
|
|
1179
|
+
if (selected) {
|
|
1180
|
+
return selected;
|
|
1181
|
+
}
|
|
1182
|
+
this.renderNotice("A model id is required.");
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async ask(label, defaultValue, options = {}) {
|
|
1186
|
+
if (!stdin.isTTY) {
|
|
1187
|
+
return defaultValue ?? "";
|
|
1188
|
+
}
|
|
1189
|
+
let value = "";
|
|
1190
|
+
this.#rl?.pause();
|
|
1191
|
+
stdout.write(ansi.hideCursor);
|
|
1192
|
+
return await new Promise((resolve, reject) => {
|
|
1193
|
+
const render = () => {
|
|
1194
|
+
const panelInputWidth = Math.min(76, Math.max(48, terminalWidth() - 14));
|
|
1195
|
+
const display = options.secret ? "•".repeat(value.length) : value;
|
|
1196
|
+
const shown = truncateToWidth(display, panelInputWidth - 5);
|
|
1197
|
+
const cursor = fg256(75, "▌");
|
|
1198
|
+
const defaultHint = defaultValue && !options.secret ? `enter accept · default ${defaultValue}` : "enter accept";
|
|
1199
|
+
this.renderCenteredPanel(label, [
|
|
1200
|
+
`${fg256(75, "›")} ${shown}${cursor}${shown ? "" : ` ${fg256(238, "type to override")}`}`,
|
|
1201
|
+
"",
|
|
1202
|
+
setupHint(`${defaultHint} · esc cancel`),
|
|
1203
|
+
], true);
|
|
1204
|
+
};
|
|
1205
|
+
const cleanup = () => {
|
|
1206
|
+
stdin.off("data", onData);
|
|
1207
|
+
stdout.off("resize", onResize);
|
|
1208
|
+
if (stdin.isTTY) {
|
|
1209
|
+
stdin.setRawMode(false);
|
|
1210
|
+
}
|
|
1211
|
+
stdout.write(ansi.hideCursor);
|
|
1212
|
+
this.resumeReadline();
|
|
1213
|
+
};
|
|
1214
|
+
const finish = () => {
|
|
1215
|
+
cleanup();
|
|
1216
|
+
resolve(value.trim() || defaultValue || "");
|
|
1217
|
+
};
|
|
1218
|
+
const cancel = () => {
|
|
1219
|
+
cleanup();
|
|
1220
|
+
reject(new Error("Input cancelled"));
|
|
1221
|
+
};
|
|
1222
|
+
const onData = (chunk) => {
|
|
1223
|
+
let done = false;
|
|
1224
|
+
for (const key of terminalInputTokens(chunk.toString("utf8"))) {
|
|
1225
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
1226
|
+
cancel();
|
|
1227
|
+
done = true;
|
|
1228
|
+
}
|
|
1229
|
+
else if (key === "\r" || key === "\n") {
|
|
1230
|
+
finish();
|
|
1231
|
+
done = true;
|
|
1232
|
+
}
|
|
1233
|
+
else if (key === "\u007f") {
|
|
1234
|
+
value = value.slice(0, -1);
|
|
1235
|
+
render();
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
const printable = printableText(key);
|
|
1239
|
+
if (printable) {
|
|
1240
|
+
value += printable;
|
|
1241
|
+
render();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (done) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
const onResize = () => {
|
|
1250
|
+
render();
|
|
1251
|
+
};
|
|
1252
|
+
stdin.setRawMode(true);
|
|
1253
|
+
stdin.resume();
|
|
1254
|
+
stdin.on("data", onData);
|
|
1255
|
+
stdout.on("resize", onResize);
|
|
1256
|
+
render();
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
async askClarification(request) {
|
|
1260
|
+
if (!stdin.isTTY) {
|
|
1261
|
+
throw new Error("Clarification requires an interactive terminal.");
|
|
1262
|
+
}
|
|
1263
|
+
let state = createClarifyInputState(request);
|
|
1264
|
+
this.#inputModalActive = true;
|
|
1265
|
+
const composerWasActive = Boolean(this.#activeComposerRedraw);
|
|
1266
|
+
this.#rl?.pause();
|
|
1267
|
+
if (stdin.isTTY) {
|
|
1268
|
+
stdin.setRawMode(true);
|
|
1269
|
+
}
|
|
1270
|
+
stdout.write(ansi.showCursor);
|
|
1271
|
+
return await new Promise((resolve, reject) => {
|
|
1272
|
+
const render = () => {
|
|
1273
|
+
this.#composerPanel = renderClarifyComposerPanel(request, state, safeTerminalWidth());
|
|
1274
|
+
this.#activeComposerRedraw?.();
|
|
1275
|
+
};
|
|
1276
|
+
const cleanup = () => {
|
|
1277
|
+
this.#inputModalActive = false;
|
|
1278
|
+
this.#composerPanel = undefined;
|
|
1279
|
+
stdin.off("data", onData);
|
|
1280
|
+
stdout.off("resize", onResize);
|
|
1281
|
+
if (stdin.isTTY) {
|
|
1282
|
+
stdin.setRawMode(composerWasActive);
|
|
1283
|
+
}
|
|
1284
|
+
stdout.write(ansi.hideCursor);
|
|
1285
|
+
this.resumeReadline();
|
|
1286
|
+
this.#activeComposerRedraw?.();
|
|
1287
|
+
};
|
|
1288
|
+
const finish = (response) => {
|
|
1289
|
+
cleanup();
|
|
1290
|
+
resolve(response);
|
|
1291
|
+
};
|
|
1292
|
+
const cancel = () => {
|
|
1293
|
+
cleanup();
|
|
1294
|
+
reject(new Error("Clarification cancelled"));
|
|
1295
|
+
};
|
|
1296
|
+
const onData = (chunk) => {
|
|
1297
|
+
for (const key of terminalInputTokens(chunk.toString("utf8"))) {
|
|
1298
|
+
const result = applyClarifyInputToken(state, request, key);
|
|
1299
|
+
state = result.state;
|
|
1300
|
+
if (result.cancelled) {
|
|
1301
|
+
cancel();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (result.response) {
|
|
1305
|
+
finish(result.response);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
render();
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
const onResize = () => {
|
|
1312
|
+
render();
|
|
1313
|
+
};
|
|
1314
|
+
stdin.setRawMode(true);
|
|
1315
|
+
stdin.resume();
|
|
1316
|
+
stdin.on("data", onData);
|
|
1317
|
+
stdout.on("resize", onResize);
|
|
1318
|
+
render();
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async askRequired(label, defaultValue) {
|
|
1322
|
+
while (true) {
|
|
1323
|
+
const value = await this.ask(label, defaultValue);
|
|
1324
|
+
if (value) {
|
|
1325
|
+
return value;
|
|
1326
|
+
}
|
|
1327
|
+
this.renderNotice(`${label} is required.`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async askApiKeySelection(defaultRef, label, currentRef, required = false) {
|
|
1331
|
+
const hasCurrentSecret = Boolean(currentRef && readSecret(currentRef));
|
|
1332
|
+
const effectiveRequired = required || Boolean(currentRef && !hasCurrentSecret);
|
|
1333
|
+
const hint = hasCurrentSecret ? "blank keeps current vault key" : effectiveRequired ? "paste key" : "blank for no auth";
|
|
1334
|
+
const value = (await this.ask(`${label} (${hint})`, undefined, { secret: true })).trim();
|
|
1335
|
+
if (!value) {
|
|
1336
|
+
if (hasCurrentSecret && currentRef) {
|
|
1337
|
+
return { api_key_ref: currentRef };
|
|
1338
|
+
}
|
|
1339
|
+
if (effectiveRequired) {
|
|
1340
|
+
this.renderNotice(`${label} is required. Paste the key so Inferoa can store it in the local vault.`);
|
|
1341
|
+
return await this.askApiKeySelection(defaultRef, label, undefined, true);
|
|
1342
|
+
}
|
|
1343
|
+
return {};
|
|
1344
|
+
}
|
|
1345
|
+
if (isEnvVarName(value) && !looksLikeApiKey(value)) {
|
|
1346
|
+
this.renderNotice("Setup stores API keys in the local vault. Paste the actual key instead of an environment variable name.");
|
|
1347
|
+
return await this.askApiKeySelection(defaultRef, label, currentRef, required);
|
|
1348
|
+
}
|
|
1349
|
+
const vaultPath = await writeSecret(defaultRef, value);
|
|
1350
|
+
this.renderCenteredPanel("Local Vault", [
|
|
1351
|
+
`${fg256(48, "✓")} stored as ${defaultRef}`,
|
|
1352
|
+
fg256(243, vaultPath),
|
|
1353
|
+
fg256(243, "Config stores only api_key_ref."),
|
|
1354
|
+
], true);
|
|
1355
|
+
return { api_key_ref: defaultRef };
|
|
1356
|
+
}
|
|
1357
|
+
applyApiKeySelection(endpoint, selection) {
|
|
1358
|
+
endpoint.api_key_ref = selection.api_key_ref;
|
|
1359
|
+
delete endpoint.api_key_env;
|
|
1360
|
+
}
|
|
1361
|
+
async confirm(label, defaultValue) {
|
|
1362
|
+
return ((await this.selectOption(label, [
|
|
1363
|
+
{ value: "yes", label: "Yes" },
|
|
1364
|
+
{ value: "no", label: "No" },
|
|
1365
|
+
], defaultValue ? 0 : 1)) === "yes");
|
|
1366
|
+
}
|
|
1367
|
+
async selectOption(title, options, defaultIndex = 0, footer = []) {
|
|
1368
|
+
if (!options.length) {
|
|
1369
|
+
throw new Error(`${title} has no options`);
|
|
1370
|
+
}
|
|
1371
|
+
let selected = Math.max(0, Math.min(defaultIndex, options.length - 1));
|
|
1372
|
+
this.#rl?.pause();
|
|
1373
|
+
const render = () => {
|
|
1374
|
+
const lines = options.map((option, index) => {
|
|
1375
|
+
const active = index === selected;
|
|
1376
|
+
return renderSetupOptionLine(option.label, option.description, active);
|
|
1377
|
+
});
|
|
1378
|
+
this.renderCenteredPanel(title, [...lines, "", setupHint("↑/↓ move · space/enter select · esc cancel"), ...footer], true);
|
|
1379
|
+
};
|
|
1380
|
+
render();
|
|
1381
|
+
return await new Promise((resolve, reject) => {
|
|
1382
|
+
const cleanup = () => {
|
|
1383
|
+
stdin.off("data", onData);
|
|
1384
|
+
stdout.off("resize", onResize);
|
|
1385
|
+
if (stdin.isTTY) {
|
|
1386
|
+
stdin.setRawMode(false);
|
|
1387
|
+
}
|
|
1388
|
+
this.resumeReadline();
|
|
1389
|
+
};
|
|
1390
|
+
const finish = () => {
|
|
1391
|
+
const value = options[selected]?.value ?? options[0].value;
|
|
1392
|
+
cleanup();
|
|
1393
|
+
stdout.write("\n");
|
|
1394
|
+
resolve(value);
|
|
1395
|
+
};
|
|
1396
|
+
const cancel = () => {
|
|
1397
|
+
cleanup();
|
|
1398
|
+
reject(new Error("Selection cancelled"));
|
|
1399
|
+
};
|
|
1400
|
+
const onData = (chunk) => {
|
|
1401
|
+
const key = chunk.toString("utf8");
|
|
1402
|
+
if (key.includes("\u0003")) {
|
|
1403
|
+
cancel();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (key === "\u001b") {
|
|
1407
|
+
cancel();
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (key.includes("\u001b[A") || key === "k") {
|
|
1411
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
1412
|
+
render();
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (key.includes("\u001b[B") || key === "j") {
|
|
1416
|
+
selected = (selected + 1) % options.length;
|
|
1417
|
+
render();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (key.includes(" ") || key.includes("\r") || key.includes("\n")) {
|
|
1421
|
+
finish();
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
const onResize = () => {
|
|
1425
|
+
render();
|
|
1426
|
+
};
|
|
1427
|
+
stdin.setRawMode(true);
|
|
1428
|
+
stdin.resume();
|
|
1429
|
+
stdin.on("data", onData);
|
|
1430
|
+
stdout.on("resize", onResize);
|
|
1431
|
+
render();
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
async reviewAction(title, body) {
|
|
1435
|
+
const options = [
|
|
1436
|
+
{ value: "save", label: "Save setup", description: "Write the user config now." },
|
|
1437
|
+
{ value: "cancel", label: "Cancel", description: "Return to chat without changing config." },
|
|
1438
|
+
];
|
|
1439
|
+
let selected = 0;
|
|
1440
|
+
this.#rl?.pause();
|
|
1441
|
+
const render = () => {
|
|
1442
|
+
const bodyLines = typeof body === "function" ? body() : body;
|
|
1443
|
+
const choices = options.map((option, index) => {
|
|
1444
|
+
const active = index === selected;
|
|
1445
|
+
return renderSetupOptionLine(option.label, option.description, active);
|
|
1446
|
+
});
|
|
1447
|
+
this.renderCenteredPanel(title, [...bodyLines, "", ...choices, "", setupHint("↑/↓ move · space/enter select · esc cancel")], true);
|
|
1448
|
+
};
|
|
1449
|
+
render();
|
|
1450
|
+
return await new Promise((resolve) => {
|
|
1451
|
+
const cleanup = () => {
|
|
1452
|
+
stdin.off("data", onData);
|
|
1453
|
+
stdout.off("resize", onResize);
|
|
1454
|
+
if (stdin.isTTY) {
|
|
1455
|
+
stdin.setRawMode(false);
|
|
1456
|
+
}
|
|
1457
|
+
this.resumeReadline();
|
|
1458
|
+
};
|
|
1459
|
+
const finish = (value) => {
|
|
1460
|
+
cleanup();
|
|
1461
|
+
stdout.write("\n");
|
|
1462
|
+
resolve(value);
|
|
1463
|
+
};
|
|
1464
|
+
const onData = (chunk) => {
|
|
1465
|
+
const key = chunk.toString("utf8");
|
|
1466
|
+
if (key.includes("\u0003") || key === "\u001b") {
|
|
1467
|
+
finish("cancel");
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (key.includes("\u001b[A") || key === "k") {
|
|
1471
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
1472
|
+
render();
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (key.includes("\u001b[B") || key === "j") {
|
|
1476
|
+
selected = (selected + 1) % options.length;
|
|
1477
|
+
render();
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (key.includes(" ") || key.includes("\r") || key.includes("\n")) {
|
|
1481
|
+
finish(options[selected]?.value ?? "save");
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
const onResize = () => {
|
|
1485
|
+
render();
|
|
1486
|
+
};
|
|
1487
|
+
stdin.setRawMode(true);
|
|
1488
|
+
stdin.resume();
|
|
1489
|
+
stdin.on("data", onData);
|
|
1490
|
+
stdout.on("resize", onResize);
|
|
1491
|
+
render();
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
async multiSelect(title, options, defaults = [], footer = []) {
|
|
1495
|
+
if (!options.length) {
|
|
1496
|
+
return [];
|
|
1497
|
+
}
|
|
1498
|
+
let selected = 0;
|
|
1499
|
+
const checked = new Set();
|
|
1500
|
+
options.forEach((option, index) => {
|
|
1501
|
+
if (defaults[index]) {
|
|
1502
|
+
checked.add(option.value);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
this.#rl?.pause();
|
|
1506
|
+
const render = () => {
|
|
1507
|
+
const start = Math.max(0, Math.min(selected - 6, Math.max(0, options.length - 12)));
|
|
1508
|
+
const visible = options.slice(start, start + 12);
|
|
1509
|
+
const lines = visible.map((option, offset) => {
|
|
1510
|
+
const index = start + offset;
|
|
1511
|
+
const active = index === selected;
|
|
1512
|
+
const mark = checked.has(option.value) ? fg256(75, "on ") : fg256(244, "off");
|
|
1513
|
+
const label = `${mark} ${option.label}`;
|
|
1514
|
+
return renderSetupOptionLine(label, option.description, active);
|
|
1515
|
+
});
|
|
1516
|
+
this.renderCenteredPanel(title, [...lines, "", setupHint("↑/↓ move · space toggle · enter save · esc cancel"), ...footer], true);
|
|
1517
|
+
};
|
|
1518
|
+
render();
|
|
1519
|
+
return await new Promise((resolve, reject) => {
|
|
1520
|
+
const cleanup = () => {
|
|
1521
|
+
stdin.off("data", onData);
|
|
1522
|
+
stdout.off("resize", onResize);
|
|
1523
|
+
if (stdin.isTTY) {
|
|
1524
|
+
stdin.setRawMode(false);
|
|
1525
|
+
}
|
|
1526
|
+
this.resumeReadline();
|
|
1527
|
+
};
|
|
1528
|
+
const finish = () => {
|
|
1529
|
+
cleanup();
|
|
1530
|
+
stdout.write("\n");
|
|
1531
|
+
resolve([...checked]);
|
|
1532
|
+
};
|
|
1533
|
+
const cancel = () => {
|
|
1534
|
+
cleanup();
|
|
1535
|
+
reject(new Error("Selection cancelled"));
|
|
1536
|
+
};
|
|
1537
|
+
const onData = (chunk) => {
|
|
1538
|
+
const key = chunk.toString("utf8");
|
|
1539
|
+
if (key.includes("\u0003") || key === "\u001b") {
|
|
1540
|
+
cancel();
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (key.includes("\u001b[A") || key === "k") {
|
|
1544
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
1545
|
+
render();
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
if (key.includes("\u001b[B") || key === "j") {
|
|
1549
|
+
selected = (selected + 1) % options.length;
|
|
1550
|
+
render();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (key.includes(" ")) {
|
|
1554
|
+
const value = options[selected]?.value;
|
|
1555
|
+
if (value) {
|
|
1556
|
+
if (checked.has(value)) {
|
|
1557
|
+
checked.delete(value);
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
checked.add(value);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
render();
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (key.includes("\r") || key.includes("\n")) {
|
|
1567
|
+
finish();
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
const onResize = () => {
|
|
1571
|
+
render();
|
|
1572
|
+
};
|
|
1573
|
+
stdin.setRawMode(true);
|
|
1574
|
+
stdin.resume();
|
|
1575
|
+
stdin.on("data", onData);
|
|
1576
|
+
stdout.on("resize", onResize);
|
|
1577
|
+
render();
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
describeModelSetup(setup) {
|
|
1581
|
+
return describeModelSetupForDisplay(setup);
|
|
1582
|
+
}
|
|
1583
|
+
describeOmniConfig(config) {
|
|
1584
|
+
const configured = Object.entries(config.omni.endpoints).filter(([, endpoint]) => endpoint?.base_url && endpoint.model);
|
|
1585
|
+
if (!config.omni.enabled || !configured.length) {
|
|
1586
|
+
return "disabled";
|
|
1587
|
+
}
|
|
1588
|
+
return configured.map(([name, endpoint]) => `${name}:${endpoint?.model}`).join(" · ");
|
|
1589
|
+
}
|
|
1590
|
+
describeWebSearchConfig(config) {
|
|
1591
|
+
const web = config.web_search;
|
|
1592
|
+
const label = webSearchProviderLabel(web.provider);
|
|
1593
|
+
if (web.provider === "auto") {
|
|
1594
|
+
return `${label} · fallback ready`;
|
|
1595
|
+
}
|
|
1596
|
+
if (web.provider === "off") {
|
|
1597
|
+
return `${label} · zero-key fallback`;
|
|
1598
|
+
}
|
|
1599
|
+
const auth = web.api_key_ref ? "vault auth" : web.provider === "jina" ? "public/no auth" : "no auth";
|
|
1600
|
+
const base = web.base_url ? ` · ${web.base_url}` : "";
|
|
1601
|
+
return `${label}${base} · ${auth}`;
|
|
1602
|
+
}
|
|
1603
|
+
async renderModelView(args) {
|
|
1604
|
+
if (!this.app.config.model_setup.base_url) {
|
|
1605
|
+
this.renderNotice("No model endpoint configured. Use /setup first.");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const probe = await this.probeChatModels(this.app.config);
|
|
1609
|
+
if (probe.errors.some((error) => error.includes("API key is missing from the local vault"))) {
|
|
1610
|
+
this.renderPanel("Model", [
|
|
1611
|
+
fg256(203, "The external provider key is missing from the local vault."),
|
|
1612
|
+
"Run /setup and paste the API key once. The user config stores only api_key_ref.",
|
|
1613
|
+
]);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const requested = args.trim();
|
|
1617
|
+
const selected = requested ? this.resolveModelSelection(requested, probe.models) : await this.pickModel("Model Selector", probe, this.app.config.model_setup.model);
|
|
1618
|
+
if (!selected) {
|
|
1619
|
+
this.renderNotice("No model selected.");
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
this.app.config.model_setup.model = selected;
|
|
1623
|
+
const target = await saveUserConfig(this.app.config);
|
|
1624
|
+
if (!this.app.configFiles.includes(target)) {
|
|
1625
|
+
this.app.configFiles.push(target);
|
|
1626
|
+
}
|
|
1627
|
+
this.renderPanel("Model Saved", [`${fg256(48, "✓")} ${selected}`, `${fg256(39, "Config")} ${target}`]);
|
|
1628
|
+
}
|
|
1629
|
+
async renderEndpointView() {
|
|
1630
|
+
const snapshot = await new EndpointSignals(this.app.config).snapshot();
|
|
1631
|
+
this.renderPanel("System", endpointStatusLinesForDisplay(snapshot, this.app.config, this.describeWebSearchConfig(this.app.config)));
|
|
1632
|
+
}
|
|
1633
|
+
renderCacheView() {
|
|
1634
|
+
const session = this.optionalSession();
|
|
1635
|
+
if (!session) {
|
|
1636
|
+
this.renderPanel(PREFIX_CACHE_REPORT_TITLE, ["No active session yet. Run a prompt first."]);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const evidence = this.app.store.listEndpointEvidence(session.session_id);
|
|
1640
|
+
const events = this.app.store.listEvents(session.session_id);
|
|
1641
|
+
const lines = evidence.slice(-12).map((item, index) => {
|
|
1642
|
+
const usage = item.usage;
|
|
1643
|
+
return ` ${index + 1}. ${renderCacheReportTurn({
|
|
1644
|
+
usage,
|
|
1645
|
+
cacheKind: cacheTurnKind(events, stringField(item.run_id)),
|
|
1646
|
+
})}`;
|
|
1647
|
+
});
|
|
1648
|
+
this.renderPanel(PREFIX_CACHE_REPORT_TITLE, evidence.length
|
|
1649
|
+
? [...cacheEvidenceOverview(evidence, events), "", fg256(39, "Recent turns"), ...lines]
|
|
1650
|
+
: ["No prefix cache records yet."]);
|
|
1651
|
+
}
|
|
1652
|
+
async renderContextView(args = "") {
|
|
1653
|
+
const action = args.trim().toLowerCase();
|
|
1654
|
+
if (action === "reindex" || action === "rebuild") {
|
|
1655
|
+
this.app.runtime.codeIntelligence.startIndexing("manual_reindex", { force: true });
|
|
1656
|
+
this.renderPanel("Context", ["Context index rebuild requested.", fg256(244, "Use /context to inspect progress.")]);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const session = this.optionalSession();
|
|
1660
|
+
const contextWindow = this.app.config.model_setup.context_window ?? this.app.config.context.context_window;
|
|
1661
|
+
const allEvents = session ? this.app.store.listEvents(session.session_id) : [];
|
|
1662
|
+
const events = allEvents.filter((event) => isContextCompressionEvent(event));
|
|
1663
|
+
const latestCompacted = allEvents.filter((event) => event.type === "context.compacted").at(-1);
|
|
1664
|
+
const latestEvidence = allEvents.filter((event) => event.type === "evidence.context_compression").at(-1);
|
|
1665
|
+
const summary = stringField(latestCompacted?.data.summary);
|
|
1666
|
+
const summaryLines = summary ? summary.split(/\r?\n/).filter(Boolean).slice(0, 8) : [];
|
|
1667
|
+
const latest = latestCompacted
|
|
1668
|
+
? [
|
|
1669
|
+
`${fg256(39, "reason")} ${stringField(latestCompacted.data.reason) ?? "unknown"}`,
|
|
1670
|
+
`${fg256(39, "archive")} ${stringField(latestCompacted.data.archive_resource_uri) ?? "none"}`,
|
|
1671
|
+
`${fg256(39, "protected")} ${numberField(latestCompacted.data.protected_tail_events) ?? "unknown"} user prompts preserved`,
|
|
1672
|
+
`${fg256(39, "before")} ${numberField(latestCompacted.data.estimated_tokens_before) ?? "unknown"} estimated tokens`,
|
|
1673
|
+
...(latestEvidence
|
|
1674
|
+
? [
|
|
1675
|
+
`${fg256(39, "threshold")} ${numberField(latestEvidence.data.threshold_tokens) ?? "unknown"} tokens`,
|
|
1676
|
+
`${fg256(39, "record")} persisted`,
|
|
1677
|
+
]
|
|
1678
|
+
: []),
|
|
1679
|
+
...(summaryLines.length ? ["", fg256(39, "Summary"), ...summaryLines.map((line) => ` ${truncateToWidth(line, Math.max(20, terminalWidth() - 8))}`)] : []),
|
|
1680
|
+
]
|
|
1681
|
+
: [" none"];
|
|
1682
|
+
const recent = events.slice(-8).map((event) => {
|
|
1683
|
+
const reason = stringField(event.data.reason);
|
|
1684
|
+
const uri = stringField(event.data.archive_resource_uri);
|
|
1685
|
+
const suffix = [reason, uri].filter(Boolean).join(" · ");
|
|
1686
|
+
return ` ${event.created_at} ${event.type}${suffix ? ` · ${truncateToWidth(suffix, Math.max(24, terminalWidth() - 42))}` : ""}`;
|
|
1687
|
+
});
|
|
1688
|
+
const intelligence = this.app.runtime.codeIntelligence.status();
|
|
1689
|
+
const cg = intelligence.codegraph;
|
|
1690
|
+
this.renderPanel("Context", [
|
|
1691
|
+
`threshold ${(this.app.config.context.compression_threshold * 100).toFixed(0)}%`,
|
|
1692
|
+
`window ${contextWindow}`,
|
|
1693
|
+
`protected recent loops ${this.app.config.context.protected_recent_loops ?? 3}`,
|
|
1694
|
+
`forced ${this.app.config.context.force_compression ? "on" : "off"}`,
|
|
1695
|
+
"",
|
|
1696
|
+
fg256(39, "Code intelligence"),
|
|
1697
|
+
` engine ${cg.state}${cg.phase ? ` · ${cg.phase}` : ""}${cg.current !== undefined && cg.total !== undefined ? ` · ${cg.current}/${cg.total}` : ""}`,
|
|
1698
|
+
` index ${cg.files ?? "?"} files · ${cg.nodes ?? "?"} symbols · ${cg.edges ?? "?"} links · watcher ${cg.watcher ?? "unknown"}`,
|
|
1699
|
+
` lsp ${intelligence.lsp.languages.length} language profiles · ast ${intelligence.ast.languages.join(", ")}`,
|
|
1700
|
+
...(cg.languages?.length ? [` languages ${cg.languages.slice(0, 10).join(", ")}`] : []),
|
|
1701
|
+
...(cg.frameworks?.length ? [` frameworks ${cg.frameworks.slice(0, 8).join(", ")}`] : []),
|
|
1702
|
+
...(cg.error ? [` ${fg256(203, truncateToWidth(cg.error, Math.max(24, terminalWidth() - 8)))}`] : []),
|
|
1703
|
+
` ${fg256(39, "/context reindex")} rebuild context index`,
|
|
1704
|
+
"",
|
|
1705
|
+
fg256(39, "Latest compression"),
|
|
1706
|
+
...latest,
|
|
1707
|
+
"",
|
|
1708
|
+
fg256(39, "Compression events"),
|
|
1709
|
+
...(recent.length ? recent : [" none"]),
|
|
1710
|
+
]);
|
|
1711
|
+
}
|
|
1712
|
+
renderToolsView(args = "") {
|
|
1713
|
+
const action = args.trim().toLowerCase();
|
|
1714
|
+
if (action === "expand" || action === "full") {
|
|
1715
|
+
this.#toolTraceMode = "expanded";
|
|
1716
|
+
this.renderLatestToolTrace("Tool Trace", false);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (action === "compact" || action === "fold") {
|
|
1720
|
+
this.#toolTraceMode = "compact";
|
|
1721
|
+
this.renderLatestToolTrace("Tool Trace", true);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (action === "last") {
|
|
1725
|
+
this.renderLatestToolTrace("Tool Trace", this.#toolTraceMode === "compact");
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const registry = new ToolRegistry(this.app.config, this.app.workspace, this.app.store);
|
|
1729
|
+
const tools = registry.list();
|
|
1730
|
+
this.renderPanel("Tools", [
|
|
1731
|
+
`${tools.length} fixed tools`,
|
|
1732
|
+
`${fg256(39, "/tools expand")} full latest tool run · ${fg256(39, "/tools compact")} fold long successful runs`,
|
|
1733
|
+
"",
|
|
1734
|
+
...tools.map((tool) => ` ${fg256(permissionColor(tool.permission), tool.permission.padEnd(13))} ${displayToolName(tool.name)}`),
|
|
1735
|
+
"",
|
|
1736
|
+
fg256(243, "Renderers: diff, shell/process, git, todo, activity, code intelligence, and Omni cards."),
|
|
1737
|
+
]);
|
|
1738
|
+
}
|
|
1739
|
+
renderLatestToolTrace(title, collapseCompact) {
|
|
1740
|
+
const sessionId = this.#sessionId;
|
|
1741
|
+
if (!sessionId) {
|
|
1742
|
+
this.renderPanel(title, ["No active session yet."]);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
const toolEvents = this.app.store.listEvents(sessionId).filter((event) => event.type === "tool.call" || event.type === "tool.result");
|
|
1746
|
+
const lastRunId = toolEvents.slice().reverse().find((event) => event.run_id)?.run_id;
|
|
1747
|
+
if (!lastRunId) {
|
|
1748
|
+
this.renderPanel(title, ["No tool trace recorded yet."]);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const lines = renderToolCards(toolEvents.filter((event) => event.run_id === lastRunId), this.app.store, { collapseCompact });
|
|
1752
|
+
this.renderPanel(title, [
|
|
1753
|
+
`${collapseCompact ? "compact" : "expanded"} · run ${lastRunId}`,
|
|
1754
|
+
"",
|
|
1755
|
+
...(lines.length ? lines : ["No tool trace recorded yet."]),
|
|
1756
|
+
]);
|
|
1757
|
+
}
|
|
1758
|
+
async renderSkillsView(args) {
|
|
1759
|
+
const query = args.trim();
|
|
1760
|
+
if (query === "list" || query.startsWith("list ")) {
|
|
1761
|
+
await this.renderSkillLauncher(query.replace(/^list\s*/, ""));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (query === "manage") {
|
|
1765
|
+
const nextConfig = structuredClone(this.app.config);
|
|
1766
|
+
nextConfig.skills.enabled = await this.manageSkillSelection(nextConfig, "");
|
|
1767
|
+
const target = await saveUserConfig(nextConfig);
|
|
1768
|
+
Object.assign(this.app.config, nextConfig);
|
|
1769
|
+
if (!this.app.configFiles.includes(target)) {
|
|
1770
|
+
this.app.configFiles.push(target);
|
|
1771
|
+
}
|
|
1772
|
+
this.renderPanel("Skills Saved", [`${fg256(48, "✓")} ${nextConfig.skills.enabled.length} enabled`, `${fg256(39, "Config")} ${target}`]);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (!query) {
|
|
1776
|
+
const action = await this.chooseSkillAction();
|
|
1777
|
+
if (action === "list") {
|
|
1778
|
+
await this.renderSkillLauncher("");
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (action === "manage") {
|
|
1782
|
+
await this.renderSkillsView("manage");
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
await this.renderSkillLauncher(query);
|
|
1787
|
+
}
|
|
1788
|
+
async renderSkillLauncher(query) {
|
|
1789
|
+
const skills = await new SkillRegistry(this.app.workspace, this.app.config).discover();
|
|
1790
|
+
if (!skills.length) {
|
|
1791
|
+
this.renderPanel("Skills", ["No skills discovered."]);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const direct = query ? this.resolveSkillQuery(query, skills) : undefined;
|
|
1795
|
+
const skill = direct ?? (await this.chooseSkillFromCatalog(skills, query));
|
|
1796
|
+
await this.triggerSkill(skill);
|
|
1797
|
+
}
|
|
1798
|
+
async triggerSkill(skill) {
|
|
1799
|
+
const enabled = new Set(this.app.config.skills.enabled);
|
|
1800
|
+
const wasEnabled = enabled.has(skill.id) || enabled.has(skill.name);
|
|
1801
|
+
if (!wasEnabled) {
|
|
1802
|
+
enabled.add(skill.id);
|
|
1803
|
+
this.app.config.skills.enabled = [...enabled].sort();
|
|
1804
|
+
const target = await saveUserConfig(this.app.config);
|
|
1805
|
+
if (!this.app.configFiles.includes(target)) {
|
|
1806
|
+
this.app.configFiles.push(target);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
this.renderPanel("Skill", [
|
|
1810
|
+
`${fg256(48, "✓")} ${wasEnabled ? "ready" : "enabled"} · ${skill.name}`,
|
|
1811
|
+
fg256(244, "Only the compact skill index is kept in the prompt. The agent can load details with skill_read."),
|
|
1812
|
+
]);
|
|
1813
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
1814
|
+
this.renderNotice("Skill is enabled. Configure a model with /setup before triggering model work.");
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
this.enqueuePrompt(`Use the ${skill.name} skill (${skill.id}). Read its body with skill_read if useful, then apply it to the current task. If no concrete task is present, ask one concise clarifying question.`);
|
|
1818
|
+
}
|
|
1819
|
+
resolveSkillQuery(query, skills) {
|
|
1820
|
+
const normalized = query.toLowerCase();
|
|
1821
|
+
return skills.find((skill) => skill.id.toLowerCase() === normalized || skill.name.toLowerCase() === normalized);
|
|
1822
|
+
}
|
|
1823
|
+
async chooseSkillFromCatalog(skills, initialQuery = "") {
|
|
1824
|
+
const enabled = new Set(this.app.config.skills.enabled);
|
|
1825
|
+
let query = initialQuery;
|
|
1826
|
+
let selected = 0;
|
|
1827
|
+
let renderedLines = 0;
|
|
1828
|
+
this.#rl?.pause();
|
|
1829
|
+
const filteredSkills = () => {
|
|
1830
|
+
const normalized = query.toLowerCase();
|
|
1831
|
+
return skills
|
|
1832
|
+
.filter((skill) => !normalized || `${skill.id} ${skill.name} ${skill.description}`.toLowerCase().includes(normalized))
|
|
1833
|
+
.sort((a, b) => {
|
|
1834
|
+
const aEnabled = enabled.has(a.id) || enabled.has(a.name);
|
|
1835
|
+
const bEnabled = enabled.has(b.id) || enabled.has(b.name);
|
|
1836
|
+
if (aEnabled !== bEnabled) {
|
|
1837
|
+
return aEnabled ? -1 : 1;
|
|
1838
|
+
}
|
|
1839
|
+
return a.name.localeCompare(b.name);
|
|
1840
|
+
});
|
|
1841
|
+
};
|
|
1842
|
+
return await new Promise((resolve, reject) => {
|
|
1843
|
+
const erase = () => {
|
|
1844
|
+
if (renderedLines) {
|
|
1845
|
+
stdout.write(`\x1b[${Math.max(0, renderedLines - 1)}A\r\x1b[J`);
|
|
1846
|
+
renderedLines = 0;
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
const cleanup = () => {
|
|
1850
|
+
erase();
|
|
1851
|
+
stdin.off("data", onData);
|
|
1852
|
+
stdout.off("resize", onResize);
|
|
1853
|
+
if (stdin.isTTY) {
|
|
1854
|
+
stdin.setRawMode(false);
|
|
1855
|
+
}
|
|
1856
|
+
this.resumeReadline();
|
|
1857
|
+
};
|
|
1858
|
+
const finish = () => {
|
|
1859
|
+
const skill = filteredSkills()[selected];
|
|
1860
|
+
if (!skill) {
|
|
1861
|
+
render();
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
cleanup();
|
|
1865
|
+
resolve(skill);
|
|
1866
|
+
};
|
|
1867
|
+
const cancel = () => {
|
|
1868
|
+
cleanup();
|
|
1869
|
+
reject(new Error("Selection cancelled"));
|
|
1870
|
+
};
|
|
1871
|
+
const render = () => {
|
|
1872
|
+
const filtered = filteredSkills();
|
|
1873
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, filtered.length - 1)));
|
|
1874
|
+
const width = terminalWidth();
|
|
1875
|
+
const start = Math.max(0, Math.min(selected - 3, Math.max(0, filtered.length - 7)));
|
|
1876
|
+
const visible = filtered.slice(start, start + 7);
|
|
1877
|
+
const lines = [
|
|
1878
|
+
bg256(236, padRight(" Skills", width)),
|
|
1879
|
+
bg256(236, padRight(` ${skills.length} discovered · ${enabled.size} enabled · type to filter`, width)),
|
|
1880
|
+
bg256(236, padRight("", width)),
|
|
1881
|
+
bg256(236, padRight(` search ${query || fg256(244, "all skills")}`, width)),
|
|
1882
|
+
...visible.map((skill, offset) => {
|
|
1883
|
+
const index = start + offset;
|
|
1884
|
+
const active = index === selected;
|
|
1885
|
+
const on = enabled.has(skill.id) || enabled.has(skill.name);
|
|
1886
|
+
const marker = active ? fg256(75, "›") : " ";
|
|
1887
|
+
const name = active ? fg256(87, padRight(truncateToWidth(skill.name, 28), 30)) : fg256(250, padRight(truncateToWidth(skill.name, 28), 30));
|
|
1888
|
+
const status = on ? fg256(75, "enabled ") : fg256(244, "disabled");
|
|
1889
|
+
const desc = fg256(244, truncateToWidth(skill.description, Math.max(20, width - 54)));
|
|
1890
|
+
return bg256(236, padRight(`${marker} ${name} ${status} · ${desc}`, width));
|
|
1891
|
+
}),
|
|
1892
|
+
bg256(236, padRight("", width)),
|
|
1893
|
+
fg256(244, " tab insert skill · enter trigger · ↑/↓ choose · esc clear"),
|
|
1894
|
+
];
|
|
1895
|
+
erase();
|
|
1896
|
+
renderedLines = lines.length;
|
|
1897
|
+
stdout.write(lines.join("\n"));
|
|
1898
|
+
};
|
|
1899
|
+
const onData = (chunk) => {
|
|
1900
|
+
const key = chunk.toString("utf8");
|
|
1901
|
+
if (key.includes("\u0003") || key === "\u001b") {
|
|
1902
|
+
cancel();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if (key.includes("\u001b[A")) {
|
|
1906
|
+
selected = (selected - 1 + filteredSkills().length) % Math.max(1, filteredSkills().length);
|
|
1907
|
+
render();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (key.includes("\u001b[B")) {
|
|
1911
|
+
selected = (selected + 1) % Math.max(1, filteredSkills().length);
|
|
1912
|
+
render();
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
if (key.includes("\r") || key.includes("\n") || key.includes("\t")) {
|
|
1916
|
+
finish();
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (key.includes("\u007f")) {
|
|
1920
|
+
query = query.slice(0, -1);
|
|
1921
|
+
selected = 0;
|
|
1922
|
+
render();
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const printable = printableText(key);
|
|
1926
|
+
if (printable) {
|
|
1927
|
+
query += printable;
|
|
1928
|
+
selected = 0;
|
|
1929
|
+
render();
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
const onResize = () => {
|
|
1933
|
+
render();
|
|
1934
|
+
};
|
|
1935
|
+
stdin.setRawMode(true);
|
|
1936
|
+
stdin.resume();
|
|
1937
|
+
stdin.on("data", onData);
|
|
1938
|
+
stdout.on("resize", onResize);
|
|
1939
|
+
render();
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
async renderSkillDetailView(skill) {
|
|
1943
|
+
const enabled = this.app.config.skills.enabled.includes(skill.id) || this.app.config.skills.enabled.includes(skill.name);
|
|
1944
|
+
const body = skill.path ? await fs.readFile(skill.path, "utf8").catch(() => "") : "";
|
|
1945
|
+
const preview = stripFrontmatter(body)
|
|
1946
|
+
.split(/\r?\n/)
|
|
1947
|
+
.map((line) => line.trimEnd())
|
|
1948
|
+
.filter((line) => line.trim())
|
|
1949
|
+
.slice(0, 12);
|
|
1950
|
+
this.renderPanel("Skill", [
|
|
1951
|
+
`${enabled ? fg256(75, "enabled") : fg256(244, "disabled")} · ${skill.name}`,
|
|
1952
|
+
fg256(244, skill.description || "No description"),
|
|
1953
|
+
"",
|
|
1954
|
+
`${fg256(75, "Use")} ${fg256(252, `$ ${skill.id}`)} ${fg256(244, "to reopen this skill. /skills manage changes enabled skills.")}`,
|
|
1955
|
+
...(preview.length ? ["", fg256(75, "Preview"), ...preview.map((line) => ` ${truncateToWidth(line, Math.max(20, terminalWidth() - 8))}`)] : []),
|
|
1956
|
+
]);
|
|
1957
|
+
}
|
|
1958
|
+
async chooseSkillAction() {
|
|
1959
|
+
const options = [
|
|
1960
|
+
{ value: "list", label: "List skills", description: "Open the skill catalog and trigger a skill." },
|
|
1961
|
+
{ value: "manage", label: "Enable/Disable", description: "Turn skills on or off." },
|
|
1962
|
+
];
|
|
1963
|
+
let selected = 0;
|
|
1964
|
+
let renderedLines = 0;
|
|
1965
|
+
this.#rl?.pause();
|
|
1966
|
+
return await new Promise((resolve, reject) => {
|
|
1967
|
+
const erase = () => {
|
|
1968
|
+
if (renderedLines) {
|
|
1969
|
+
stdout.write(`\x1b[${Math.max(0, renderedLines - 1)}A\r\x1b[J`);
|
|
1970
|
+
renderedLines = 0;
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
const cleanup = () => {
|
|
1974
|
+
erase();
|
|
1975
|
+
stdin.off("data", onData);
|
|
1976
|
+
stdout.off("resize", onResize);
|
|
1977
|
+
if (stdin.isTTY) {
|
|
1978
|
+
stdin.setRawMode(false);
|
|
1979
|
+
}
|
|
1980
|
+
this.resumeReadline();
|
|
1981
|
+
};
|
|
1982
|
+
const render = () => {
|
|
1983
|
+
const width = terminalWidth();
|
|
1984
|
+
const lines = [
|
|
1985
|
+
bg256(236, padRight(" Skills", width)),
|
|
1986
|
+
bg256(236, padRight(" Choose an action", width)),
|
|
1987
|
+
bg256(236, padRight("", width)),
|
|
1988
|
+
...options.map((option, index) => {
|
|
1989
|
+
const active = index === selected;
|
|
1990
|
+
const marker = active ? fg256(75, "›") : " ";
|
|
1991
|
+
const label = active ? fg256(87, padRight(`${index + 1}. ${option.label}`, 24)) : fg256(250, padRight(`${index + 1}. ${option.label}`, 24));
|
|
1992
|
+
return bg256(236, padRight(`${marker} ${label} ${fg256(244, option.description ?? "")}`, width));
|
|
1993
|
+
}),
|
|
1994
|
+
bg256(236, padRight("", width)),
|
|
1995
|
+
fg256(244, " ↑/↓ move · enter select · esc cancel · $ opens the skill catalog"),
|
|
1996
|
+
];
|
|
1997
|
+
erase();
|
|
1998
|
+
renderedLines = lines.length;
|
|
1999
|
+
stdout.write(lines.join("\n"));
|
|
2000
|
+
};
|
|
2001
|
+
const finish = () => {
|
|
2002
|
+
const value = options[selected]?.value ?? "list";
|
|
2003
|
+
cleanup();
|
|
2004
|
+
resolve(value);
|
|
2005
|
+
};
|
|
2006
|
+
const cancel = () => {
|
|
2007
|
+
cleanup();
|
|
2008
|
+
reject(new Error("Selection cancelled"));
|
|
2009
|
+
};
|
|
2010
|
+
const onData = (chunk) => {
|
|
2011
|
+
const key = chunk.toString("utf8");
|
|
2012
|
+
if (key.includes("\u0003") || key === "\u001b") {
|
|
2013
|
+
cancel();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (key.includes("\u001b[A")) {
|
|
2017
|
+
selected = (selected - 1 + options.length) % options.length;
|
|
2018
|
+
render();
|
|
2019
|
+
}
|
|
2020
|
+
if (key.includes("\u001b[B")) {
|
|
2021
|
+
selected = (selected + 1) % options.length;
|
|
2022
|
+
render();
|
|
2023
|
+
}
|
|
2024
|
+
if (key.includes("\r") || key.includes("\n")) {
|
|
2025
|
+
finish();
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
const onResize = () => {
|
|
2029
|
+
render();
|
|
2030
|
+
};
|
|
2031
|
+
stdin.setRawMode(true);
|
|
2032
|
+
stdin.resume();
|
|
2033
|
+
stdin.on("data", onData);
|
|
2034
|
+
stdout.on("resize", onResize);
|
|
2035
|
+
render();
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
async renderSkillListView(query) {
|
|
2039
|
+
const skills = await new SkillRegistry(this.app.workspace, this.app.config).discover();
|
|
2040
|
+
const enabled = new Set(this.app.config.skills.enabled);
|
|
2041
|
+
const normalized = query.toLowerCase();
|
|
2042
|
+
const filtered = skills
|
|
2043
|
+
.filter((skill) => !normalized || `${skill.id} ${skill.name} ${skill.description}`.toLowerCase().includes(normalized))
|
|
2044
|
+
.slice(0, 24);
|
|
2045
|
+
this.renderPanel("Skills", [
|
|
2046
|
+
`${enabled.size} enabled · ${skills.length} discovered`,
|
|
2047
|
+
"",
|
|
2048
|
+
...filtered.map((skill) => {
|
|
2049
|
+
const status = enabled.has(skill.id) || enabled.has(skill.name) ? fg256(75, "on ") : fg256(244, "off");
|
|
2050
|
+
return ` ${status} ${padRight(truncateToWidth(skill.name, 26), 28)} ${fg256(244, truncateToWidth(skill.description, Math.max(24, terminalWidth() - 42)))}`;
|
|
2051
|
+
}),
|
|
2052
|
+
...(skills.length > filtered.length ? ["", fg256(244, `Showing ${filtered.length}; use $ and type to filter.`)] : []),
|
|
2053
|
+
]);
|
|
2054
|
+
}
|
|
2055
|
+
async renderGoalView(args) {
|
|
2056
|
+
const parsed = parseModeAction(args, new Set(["show", "set", "plan", "pause", "resume", "budget", "complete", "drop"]));
|
|
2057
|
+
const existingSession = this.optionalSession();
|
|
2058
|
+
if (!existingSession && parsed.action === "show") {
|
|
2059
|
+
this.renderPanel("Goal", ["No active session yet. Use /goal <objective> to start one."]);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
const session = existingSession ?? this.createModeSession(titleFromPromptForMode(parsed.rest || "Goal"));
|
|
2063
|
+
const current = readGoalState(this.app.store, session.session_id);
|
|
2064
|
+
if (!parsed.action && !parsed.rest) {
|
|
2065
|
+
if (current) {
|
|
2066
|
+
this.renderGoalPanel(current);
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const objective = (await this.askModeObjective("Goal objective")).trim();
|
|
2070
|
+
if (!objective) {
|
|
2071
|
+
this.renderNotice("No goal objective entered.");
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
await this.startGoal(session, objective);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if (!parsed.action || parsed.action === "set") {
|
|
2078
|
+
const objective = parsed.rest.trim() || (await this.askModeObjective("Goal objective", current?.goal.objective)).trim();
|
|
2079
|
+
if (!objective) {
|
|
2080
|
+
this.renderNotice("No goal objective entered.");
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
await this.startGoal(session, objective);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (parsed.action === "show") {
|
|
2087
|
+
this.renderGoalPanel(current);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (!current) {
|
|
2091
|
+
this.renderPanel("Goal", ["No goal set. Use /goal <objective> to start one."]);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (parsed.action === "pause") {
|
|
2095
|
+
const next = cloneGoalState(current);
|
|
2096
|
+
next.enabled = false;
|
|
2097
|
+
if (next.goal.status === "active" || next.goal.status === "budget-limited") {
|
|
2098
|
+
next.goal.status = "paused";
|
|
2099
|
+
}
|
|
2100
|
+
next.goal.updated_at = new Date().toISOString();
|
|
2101
|
+
this.renderGoalPanel(writeGoalState(this.app.store, session.session_id, next));
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
if (parsed.action === "resume") {
|
|
2105
|
+
const next = cloneGoalState(current);
|
|
2106
|
+
if (next.goal.status === "complete" || next.goal.status === "dropped") {
|
|
2107
|
+
this.renderNotice(`Cannot resume a ${next.goal.status} goal.`);
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
next.enabled = true;
|
|
2111
|
+
next.goal.status = "active";
|
|
2112
|
+
next.goal.updated_at = new Date().toISOString();
|
|
2113
|
+
const saved = writeGoalState(this.app.store, session.session_id, next);
|
|
2114
|
+
this.renderGoalPanel(saved);
|
|
2115
|
+
this.enqueueGoalContinuation(saved.goal.objective);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (parsed.action === "plan") {
|
|
2119
|
+
this.renderGoalPanel(current);
|
|
2120
|
+
this.enqueueGoalPlanningContinuation(current.goal.objective);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (parsed.action === "budget") {
|
|
2124
|
+
const raw = parsed.rest.trim() || (await this.ask("Goal budget (positive integer or off)", current.goal.token_budget === undefined ? "off" : String(current.goal.token_budget))).trim();
|
|
2125
|
+
const next = cloneGoalState(current);
|
|
2126
|
+
if (raw.toLowerCase() === "off") {
|
|
2127
|
+
delete next.goal.token_budget;
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
const value = Number.parseInt(raw, 10);
|
|
2131
|
+
validateTokenBudget(value);
|
|
2132
|
+
next.goal.token_budget = value;
|
|
2133
|
+
if (next.goal.status === "budget-limited" && next.goal.tokens_used < value) {
|
|
2134
|
+
next.goal.status = "active";
|
|
2135
|
+
next.enabled = true;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
next.goal.updated_at = new Date().toISOString();
|
|
2139
|
+
this.renderGoalPanel(writeGoalState(this.app.store, session.session_id, next));
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (parsed.action === "complete" || parsed.action === "drop") {
|
|
2143
|
+
if (parsed.action === "complete") {
|
|
2144
|
+
const incompleteMessage = incompleteGoalPlanningMessage(current.goal);
|
|
2145
|
+
if (incompleteMessage) {
|
|
2146
|
+
this.renderNotice(incompleteMessage);
|
|
2147
|
+
this.renderGoalPanel(current);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const next = cloneGoalState(current);
|
|
2152
|
+
const summary = parsed.rest.trim() || (await this.ask(parsed.action === "complete" ? "Completion summary" : "Drop reason", current.goal.summary)).trim();
|
|
2153
|
+
if (parsed.action === "complete" && !summary) {
|
|
2154
|
+
this.renderNotice("Completion summary is required.");
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
if (summary) {
|
|
2158
|
+
next.goal.summary = summary;
|
|
2159
|
+
}
|
|
2160
|
+
next.enabled = false;
|
|
2161
|
+
next.goal.status = parsed.action === "complete" ? "complete" : "dropped";
|
|
2162
|
+
next.goal.updated_at = new Date().toISOString();
|
|
2163
|
+
const runId = parsed.action === "complete" ? randomId("goal") : undefined;
|
|
2164
|
+
const saved = writeGoalState(this.app.store, session.session_id, next, runId);
|
|
2165
|
+
if (parsed.action === "complete" && runId) {
|
|
2166
|
+
recordGoalCompletionReport(this.app.store, session.session_id, runId);
|
|
2167
|
+
}
|
|
2168
|
+
this.renderGoalPanel(saved);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
async startGoal(session, objective) {
|
|
2172
|
+
const state = writeGoalState(this.app.store, session.session_id, createGoalState({ objective }));
|
|
2173
|
+
this.renderGoalPanel(state);
|
|
2174
|
+
this.enqueueGoalContinuation(objective);
|
|
2175
|
+
}
|
|
2176
|
+
enqueueGoalContinuation(objective) {
|
|
2177
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
2178
|
+
this.renderNotice("Goal is saved. Configure a model with /setup before triggering model work.");
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
this.enqueuePrompt([
|
|
2182
|
+
`Goal objective: ${objective}`,
|
|
2183
|
+
"If this goal is broad or multi-step, call the goal tool with op=decompose to create internal steps before risky edits.",
|
|
2184
|
+
"Execute the goal while keeping step status, notes, and evidence current with goal op=update_step. Complete only when the objective is genuinely handled.",
|
|
2185
|
+
].join("\n"), { renderPrompt: false });
|
|
2186
|
+
}
|
|
2187
|
+
enqueueGoalPlanningContinuation(objective) {
|
|
2188
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
2189
|
+
this.renderNotice("Goal is saved. Configure a model with /setup before triggering model planning.");
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
this.enqueuePrompt([
|
|
2193
|
+
`Goal objective: ${objective}`,
|
|
2194
|
+
"Review the current goal state. Decompose or update the internal goal plan with the goal tool, including active step, blockers, and verification steps.",
|
|
2195
|
+
].join("\n"), { renderPrompt: false });
|
|
2196
|
+
}
|
|
2197
|
+
renderGoalPanel(state) {
|
|
2198
|
+
if (!state) {
|
|
2199
|
+
this.renderPanel("Goal", ["No goal set."]);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const goal = state.goal;
|
|
2203
|
+
const status = `${goal.status}${state.enabled ? "" : " (paused)"}`;
|
|
2204
|
+
const lines = [`${fg256(39, status)} ${goal.objective}`];
|
|
2205
|
+
const usage = goalPanelUsage(goal);
|
|
2206
|
+
if (usage) {
|
|
2207
|
+
lines.push(fg256(244, usage));
|
|
2208
|
+
}
|
|
2209
|
+
if (goal.summary) {
|
|
2210
|
+
lines.push(`${fg256(39, "summary")} ${goal.summary}`);
|
|
2211
|
+
}
|
|
2212
|
+
if (goal.planning) {
|
|
2213
|
+
lines.push(`${fg256(39, "plan")} ${goalPlanningProgressSummary(goal.planning)}`);
|
|
2214
|
+
const active = goal.planning.active_step_id ? goal.planning.steps.find((step) => step.id === goal.planning?.active_step_id) : undefined;
|
|
2215
|
+
if (active) {
|
|
2216
|
+
lines.push(`${fg256(39, "now")} ${goalStepStatusMarker(active.status)} ${active.id} ${active.title}`);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
else {
|
|
2220
|
+
lines.push(fg256(244, "No internal plan yet."));
|
|
2221
|
+
}
|
|
2222
|
+
lines.push("", `${fg256(39, "/goal plan")} plan · ${fg256(39, "/goal complete")} complete · ${fg256(39, "/goal pause")} pause · ${fg256(39, "/goal drop")} drop`);
|
|
2223
|
+
this.renderPanel("Goal", lines);
|
|
2224
|
+
}
|
|
2225
|
+
async renderPlanView(args) {
|
|
2226
|
+
const parsed = parseModeAction(args, new Set(["show", "set", "pause", "resume", "approve", "drop"]));
|
|
2227
|
+
const existingSession = this.optionalSession();
|
|
2228
|
+
if (!existingSession && parsed.action === "show") {
|
|
2229
|
+
this.renderPanel("Plan", ["No active session yet. Use /plan <objective> to start one."]);
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const session = existingSession ?? this.createModeSession(titleFromPromptForMode(parsed.rest || "Plan"));
|
|
2233
|
+
const current = readPlanState(this.app.store, session.session_id);
|
|
2234
|
+
if (!parsed.action && !parsed.rest) {
|
|
2235
|
+
if (current) {
|
|
2236
|
+
this.renderPlanPanel(current);
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const objective = (await this.askModeObjective("Plan objective")).trim();
|
|
2240
|
+
if (!objective) {
|
|
2241
|
+
this.renderNotice("No plan objective entered.");
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
await this.startPlan(session, objective);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (!parsed.action || parsed.action === "set") {
|
|
2248
|
+
const objective = parsed.rest.trim() || (await this.askModeObjective("Plan objective", current?.plan.objective)).trim();
|
|
2249
|
+
if (!objective) {
|
|
2250
|
+
this.renderNotice("No plan objective entered.");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
await this.startPlan(session, objective);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (parsed.action === "show") {
|
|
2257
|
+
this.renderPlanPanel(current);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (!current) {
|
|
2261
|
+
this.renderPanel("Plan", ["No plan set. Use /plan <objective> to start one."]);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (parsed.action === "pause") {
|
|
2265
|
+
const next = clonePlanState(current);
|
|
2266
|
+
next.enabled = false;
|
|
2267
|
+
if (next.plan.status === "drafting") {
|
|
2268
|
+
next.plan.status = "paused";
|
|
2269
|
+
}
|
|
2270
|
+
next.plan.updated_at = new Date().toISOString();
|
|
2271
|
+
this.renderPlanPanel(writePlanState(this.app.store, session.session_id, next));
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (parsed.action === "resume") {
|
|
2275
|
+
const next = clonePlanState(current);
|
|
2276
|
+
if (next.plan.status === "approved" || next.plan.status === "dropped") {
|
|
2277
|
+
this.renderNotice(`Cannot resume an ${next.plan.status} plan.`);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
next.enabled = true;
|
|
2281
|
+
next.plan.status = "drafting";
|
|
2282
|
+
next.plan.updated_at = new Date().toISOString();
|
|
2283
|
+
const saved = writePlanState(this.app.store, session.session_id, next);
|
|
2284
|
+
this.renderPlanPanel(saved);
|
|
2285
|
+
this.enqueuePlanContinuation(saved.plan.objective);
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
if (parsed.action === "approve" || parsed.action === "drop") {
|
|
2289
|
+
const next = clonePlanState(current);
|
|
2290
|
+
if (parsed.action === "approve") {
|
|
2291
|
+
const blockMessage = planApprovalBlockMessage(next);
|
|
2292
|
+
if (blockMessage) {
|
|
2293
|
+
this.renderNotice(blockMessage);
|
|
2294
|
+
this.renderPlanPanel(current);
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const summary = parsed.rest.trim() || (await this.ask(parsed.action === "approve" ? "Approval summary" : "Drop reason", current.plan.summary)).trim();
|
|
2299
|
+
if (summary) {
|
|
2300
|
+
next.plan.summary = summary;
|
|
2301
|
+
}
|
|
2302
|
+
next.enabled = false;
|
|
2303
|
+
next.plan.status = parsed.action === "approve" ? "approved" : "dropped";
|
|
2304
|
+
next.plan.updated_at = new Date().toISOString();
|
|
2305
|
+
const saved = writePlanState(this.app.store, session.session_id, next);
|
|
2306
|
+
this.renderPlanPanel(saved);
|
|
2307
|
+
if (parsed.action === "approve") {
|
|
2308
|
+
this.attachApprovedPlanToGoal(saved, session.session_id);
|
|
2309
|
+
this.enqueueApprovedPlanExecution(saved);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
async startPlan(session, objective) {
|
|
2314
|
+
const state = writePlanState(this.app.store, session.session_id, createPlanState({ objective }));
|
|
2315
|
+
this.renderPlanPanel(state);
|
|
2316
|
+
this.enqueuePlanContinuation(objective);
|
|
2317
|
+
}
|
|
2318
|
+
enqueuePlanContinuation(objective) {
|
|
2319
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
2320
|
+
this.renderNotice("Plan is saved. Configure a model with /setup before triggering model work.");
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
this.enqueuePrompt([
|
|
2324
|
+
`Plan objective: ${objective}`,
|
|
2325
|
+
"Enter plan mode for the active plan. Do not call plan create again unless there is no active plan; use plan get/update for the existing draft. Inspect first and avoid state-changing actions while drafting unless the user explicitly requested that specific action. For non-trivial tasks, ask concise clarify questions before the final plan when scope, constraints, risk tolerance, tradeoffs, or execution preference could change the plan. Resolve open questions before finalizing. Keep the plan updated with the plan tool, then call plan approve as soon as the proposed plan is ready so the user can implement it or type revision feedback. If approval is declined, revise the plan from the feedback and ask again.",
|
|
2326
|
+
].join("\n"), { renderPrompt: false });
|
|
2327
|
+
}
|
|
2328
|
+
enqueueApprovedPlanExecution(state) {
|
|
2329
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
2330
|
+
this.renderNotice("Plan is approved. Configure a model with /setup before triggering model work.");
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
this.enqueuePrompt([
|
|
2334
|
+
`Approved plan objective: ${state.plan.objective}`,
|
|
2335
|
+
state.plan.summary ? `Plan summary: ${state.plan.summary}` : undefined,
|
|
2336
|
+
state.plan.body ? `Plan body:\n${state.plan.body}` : undefined,
|
|
2337
|
+
"Execute the approved plan. Keep todo state current and verify before final response.",
|
|
2338
|
+
]
|
|
2339
|
+
.filter((line) => Boolean(line))
|
|
2340
|
+
.join("\n\n"), { renderPrompt: false });
|
|
2341
|
+
}
|
|
2342
|
+
attachApprovedPlanToGoal(planState, sessionId) {
|
|
2343
|
+
const goalState = readGoalState(this.app.store, sessionId);
|
|
2344
|
+
if (!goalState || goalState.goal.status === "complete" || goalState.goal.status === "dropped") {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
writeGoalState(this.app.store, sessionId, attachGoalPlanSnapshot(goalState, {
|
|
2348
|
+
id: planState.plan.id,
|
|
2349
|
+
objective: planState.plan.objective,
|
|
2350
|
+
summary: planState.plan.summary,
|
|
2351
|
+
body: planState.plan.body,
|
|
2352
|
+
approved_at: planState.plan.updated_at,
|
|
2353
|
+
}));
|
|
2354
|
+
}
|
|
2355
|
+
renderPlanPanel(state) {
|
|
2356
|
+
if (!state) {
|
|
2357
|
+
this.renderPanel("Plan", ["No plan set."]);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
const plan = state.plan;
|
|
2361
|
+
const status = state.enabled ? plan.status : `${plan.status} inactive`;
|
|
2362
|
+
const header = `${fg256(75, "Plan")} ${fg256(244, "·")} ${fg256(252, truncateToWidth(plan.objective, Math.max(24, safeTerminalWidth() - 22)))} ${fg256(244, `· ${status}`)}`;
|
|
2363
|
+
const hasBody = Boolean(plan.body?.trim());
|
|
2364
|
+
const lines = hasBody
|
|
2365
|
+
? [
|
|
2366
|
+
header,
|
|
2367
|
+
"",
|
|
2368
|
+
...renderPlanDocumentSurface(plan, { width: safeTerminalWidth(), maxBodyLines: Number.POSITIVE_INFINITY, includeHeader: true }),
|
|
2369
|
+
"",
|
|
2370
|
+
`${fg256(244, "Review")} ${fg256(39, "/plan approve")} ${fg256(244, "or type requested changes when asked.")}`,
|
|
2371
|
+
]
|
|
2372
|
+
: [
|
|
2373
|
+
header,
|
|
2374
|
+
fg256(244, "Drafting. The agent will inspect, ask clarifying questions when needed, then present a plan for approval."),
|
|
2375
|
+
];
|
|
2376
|
+
this.writeTranscript(`${lines.join("\n")}\n\n`);
|
|
2377
|
+
}
|
|
2378
|
+
async renderAutoresearchView(args) {
|
|
2379
|
+
const parsed = parseModeAction(args, new Set(["status", "off", "clear"]));
|
|
2380
|
+
const existingSession = this.optionalSession();
|
|
2381
|
+
if (!existingSession && parsed.action === "status") {
|
|
2382
|
+
this.renderPanel("Autoresearch", ["No active session yet. Use /autoresearch <goal> to start one."]);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const session = existingSession ?? this.createModeSession(titleFromPromptForMode(parsed.rest || "Autoresearch"));
|
|
2386
|
+
const state = readAutoresearchState(this.app.store, session.session_id);
|
|
2387
|
+
if (parsed.action === "status") {
|
|
2388
|
+
this.renderAutoresearchPanel(state);
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
if (parsed.action === "off") {
|
|
2392
|
+
this.renderAutoresearchPanel(setAutoresearchMode(this.app.store, session.session_id, { mode: "off", goal: state.goal }));
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (parsed.action === "clear") {
|
|
2396
|
+
this.renderAutoresearchPanel(setAutoresearchMode(this.app.store, session.session_id, { mode: "clear" }));
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (!parsed.rest && state.enabled) {
|
|
2400
|
+
this.renderAutoresearchPanel(setAutoresearchMode(this.app.store, session.session_id, { mode: "off", goal: state.goal }));
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const goal = parsed.rest.trim() || (await this.askModeObjective("Autoresearch goal", state.goal)).trim();
|
|
2404
|
+
const next = setAutoresearchMode(this.app.store, session.session_id, { mode: "on", goal: goal || state.goal });
|
|
2405
|
+
await this.renderAutoresearchStartSummary(goal || state.goal, next);
|
|
2406
|
+
this.renderAutoresearchPanel(next);
|
|
2407
|
+
if (!this.app.config.model_setup.base_url || !this.app.config.model_setup.model) {
|
|
2408
|
+
this.renderNotice("Autoresearch is enabled. Configure a model with /setup before triggering model work.");
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
this.enqueuePrompt([
|
|
2412
|
+
goal ? `Autoresearch goal: ${goal}` : "Autoresearch is enabled.",
|
|
2413
|
+
"Set up or continue the benchmark-driven experiment loop. If no experiment exists, create ./autoresearch.sh, validate it, then call init_experiment.",
|
|
2414
|
+
].join("\n"));
|
|
2415
|
+
}
|
|
2416
|
+
async renderAutoresearchStartSummary(goal, state) {
|
|
2417
|
+
let harness = "missing";
|
|
2418
|
+
try {
|
|
2419
|
+
await fs.access(path.join(this.app.workspace.root, "autoresearch.sh"));
|
|
2420
|
+
harness = "present";
|
|
2421
|
+
}
|
|
2422
|
+
catch {
|
|
2423
|
+
harness = "missing";
|
|
2424
|
+
}
|
|
2425
|
+
const experiment = state.experiment;
|
|
2426
|
+
this.renderPanel("Autoresearch Preflight", [
|
|
2427
|
+
`${fg256(39, "Goal")} ${goal ?? experiment?.goal ?? "none"}`,
|
|
2428
|
+
`${fg256(39, "Harness")} ${harness} at ./autoresearch.sh`,
|
|
2429
|
+
`${fg256(39, "Pending run")} ${experiment?.pending_run ? `run ${experiment.pending_run.id} must be logged first` : "none"}`,
|
|
2430
|
+
`${fg256(39, "Prompt cache")} stable system prefix; autoresearch context is injected at the current turn tail`,
|
|
2431
|
+
`${fg256(39, "Loop")} validate harness -> run baseline -> log result -> iterate`,
|
|
2432
|
+
]);
|
|
2433
|
+
}
|
|
2434
|
+
renderAutoresearchPanel(state) {
|
|
2435
|
+
const experiment = state.experiment;
|
|
2436
|
+
if (!state.enabled && !experiment) {
|
|
2437
|
+
this.renderPanel("Autoresearch", ["disabled"]);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
const progress = experiment ? summarizeAutoresearchProgress(experiment) : undefined;
|
|
2441
|
+
this.renderPanel("Autoresearch", [
|
|
2442
|
+
`${fg256(39, "Mode")} ${state.enabled ? "on" : "off"}`,
|
|
2443
|
+
`${fg256(39, "Goal")} ${state.goal ?? experiment?.goal ?? "none"}`,
|
|
2444
|
+
...(experiment
|
|
2445
|
+
? [
|
|
2446
|
+
`${fg256(39, "Experiment")} ${experiment.name}`,
|
|
2447
|
+
`${fg256(39, "Metric")} ${experiment.primary_metric} (${experiment.metric_unit || "unitless"}, ${experiment.direction} is better)`,
|
|
2448
|
+
`${fg256(39, "Best")} ${experiment.best_metric ?? "none"}`,
|
|
2449
|
+
`${fg256(39, "Runs")} ${progress?.logged_runs ?? 0} logged · ${progress?.kept_runs ?? 0}${progress?.keep_cap ? `/${progress.keep_cap}` : ""} keep${experiment.pending_run ? ` · pending ${experiment.pending_run.id}` : ""}`,
|
|
2450
|
+
...(experiment.harness_status ? [`${fg256(39, "Harness")} ${experiment.harness_status.message}`] : []),
|
|
2451
|
+
]
|
|
2452
|
+
: [fg256(244, "Phase 1: create ./autoresearch.sh and call init_experiment.")]),
|
|
2453
|
+
"",
|
|
2454
|
+
`${fg256(39, "/autoresearch status")} show · ${fg256(39, "/autoresearch off")} disable · ${fg256(39, "/autoresearch clear")} clear`,
|
|
2455
|
+
]);
|
|
2456
|
+
}
|
|
2457
|
+
async manageSkillSelection(config, initialQuery = "") {
|
|
2458
|
+
const skills = await new SkillRegistry(this.app.workspace, config).discover();
|
|
2459
|
+
if (!skills.length) {
|
|
2460
|
+
this.renderPanel("Skills", ["No skills discovered."]);
|
|
2461
|
+
return config.skills.enabled;
|
|
2462
|
+
}
|
|
2463
|
+
const checked = new Set(config.skills.enabled);
|
|
2464
|
+
let query = initialQuery;
|
|
2465
|
+
let selected = 0;
|
|
2466
|
+
let renderedLines = 0;
|
|
2467
|
+
this.#rl?.pause();
|
|
2468
|
+
const filteredSkills = () => {
|
|
2469
|
+
const normalized = query.toLowerCase();
|
|
2470
|
+
return skills
|
|
2471
|
+
.filter((skill) => !normalized || `${skill.id} ${skill.name} ${skill.description}`.toLowerCase().includes(normalized))
|
|
2472
|
+
.sort((a, b) => {
|
|
2473
|
+
const aEnabled = checked.has(a.id) || checked.has(a.name);
|
|
2474
|
+
const bEnabled = checked.has(b.id) || checked.has(b.name);
|
|
2475
|
+
if (aEnabled !== bEnabled) {
|
|
2476
|
+
return aEnabled ? -1 : 1;
|
|
2477
|
+
}
|
|
2478
|
+
return a.name.localeCompare(b.name);
|
|
2479
|
+
});
|
|
2480
|
+
};
|
|
2481
|
+
return await new Promise((resolve, reject) => {
|
|
2482
|
+
const erase = () => {
|
|
2483
|
+
if (renderedLines) {
|
|
2484
|
+
stdout.write(`\x1b[${Math.max(0, renderedLines - 1)}A\r\x1b[J`);
|
|
2485
|
+
renderedLines = 0;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
const cleanup = () => {
|
|
2489
|
+
erase();
|
|
2490
|
+
stdin.off("data", onData);
|
|
2491
|
+
stdout.off("resize", onResize);
|
|
2492
|
+
if (stdin.isTTY) {
|
|
2493
|
+
stdin.setRawMode(false);
|
|
2494
|
+
}
|
|
2495
|
+
this.resumeReadline();
|
|
2496
|
+
};
|
|
2497
|
+
const finish = () => {
|
|
2498
|
+
cleanup();
|
|
2499
|
+
resolve([...checked].sort());
|
|
2500
|
+
};
|
|
2501
|
+
const cancel = () => {
|
|
2502
|
+
cleanup();
|
|
2503
|
+
reject(new Error("Selection cancelled"));
|
|
2504
|
+
};
|
|
2505
|
+
const render = () => {
|
|
2506
|
+
const filtered = filteredSkills();
|
|
2507
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, filtered.length - 1)));
|
|
2508
|
+
const width = terminalWidth();
|
|
2509
|
+
const start = Math.max(0, Math.min(selected - 4, Math.max(0, filtered.length - 9)));
|
|
2510
|
+
const visible = filtered.slice(start, start + 9);
|
|
2511
|
+
const lines = [
|
|
2512
|
+
bg256(236, padRight(" Skills", width)),
|
|
2513
|
+
bg256(236, padRight(` ${checked.size} enabled · ${skills.length} discovered · type to filter`, width)),
|
|
2514
|
+
bg256(236, padRight("", width)),
|
|
2515
|
+
bg256(236, padRight(` search ${query || fg256(244, "all skills")}`, width)),
|
|
2516
|
+
...visible.map((skill, offset) => {
|
|
2517
|
+
const index = start + offset;
|
|
2518
|
+
const active = index === selected;
|
|
2519
|
+
const enabled = checked.has(skill.id) || checked.has(skill.name);
|
|
2520
|
+
const marker = active ? fg256(75, "›") : " ";
|
|
2521
|
+
const box = enabled ? fg256(87, "[x]") : fg256(244, "[ ]");
|
|
2522
|
+
const name = active ? fg256(87, padRight(truncateToWidth(skill.name, 26), 28)) : fg256(250, padRight(truncateToWidth(skill.name, 26), 28));
|
|
2523
|
+
const desc = fg256(244, truncateToWidth(skill.description, Math.max(20, width - 42)));
|
|
2524
|
+
return bg256(236, padRight(`${marker} ${box} ${name} ${desc}`, width));
|
|
2525
|
+
}),
|
|
2526
|
+
bg256(236, padRight("", width)),
|
|
2527
|
+
fg256(244, " ↑/↓ move · space toggle · enter save · esc cancel"),
|
|
2528
|
+
];
|
|
2529
|
+
erase();
|
|
2530
|
+
renderedLines = lines.length;
|
|
2531
|
+
stdout.write(lines.join("\n"));
|
|
2532
|
+
};
|
|
2533
|
+
const toggle = () => {
|
|
2534
|
+
const skill = filteredSkills()[selected];
|
|
2535
|
+
if (!skill) {
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (checked.has(skill.id)) {
|
|
2539
|
+
checked.delete(skill.id);
|
|
2540
|
+
checked.delete(skill.name);
|
|
2541
|
+
}
|
|
2542
|
+
else {
|
|
2543
|
+
checked.add(skill.id);
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
const onData = (chunk) => {
|
|
2547
|
+
const key = chunk.toString("utf8");
|
|
2548
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
2549
|
+
cancel();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (key === "\u001b[A") {
|
|
2553
|
+
selected = (selected - 1 + filteredSkills().length) % Math.max(1, filteredSkills().length);
|
|
2554
|
+
render();
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (key === "\u001b[B") {
|
|
2558
|
+
selected = (selected + 1) % Math.max(1, filteredSkills().length);
|
|
2559
|
+
render();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (key === " ") {
|
|
2563
|
+
toggle();
|
|
2564
|
+
render();
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (key === "\r" || key === "\n") {
|
|
2568
|
+
finish();
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (key === "\u007f") {
|
|
2572
|
+
query = query.slice(0, -1);
|
|
2573
|
+
selected = 0;
|
|
2574
|
+
render();
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (isPrintableInput(key)) {
|
|
2578
|
+
query += printableText(key);
|
|
2579
|
+
selected = 0;
|
|
2580
|
+
render();
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
const onResize = () => {
|
|
2584
|
+
render();
|
|
2585
|
+
};
|
|
2586
|
+
stdin.setRawMode(true);
|
|
2587
|
+
stdin.resume();
|
|
2588
|
+
stdin.on("data", onData);
|
|
2589
|
+
stdout.on("resize", onResize);
|
|
2590
|
+
render();
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
async renderSessionsView(args) {
|
|
2594
|
+
const requested = args.trim();
|
|
2595
|
+
if (requested) {
|
|
2596
|
+
if (requested === "resume") {
|
|
2597
|
+
await this.renderResumeSessionView("");
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
if (requested === "new") {
|
|
2601
|
+
const session = await this.createTuiSession();
|
|
2602
|
+
this.renderPanel("Session Created", [this.sessionLabel(session)]);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (requested === "all") {
|
|
2606
|
+
this.renderSessionList(true);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
const sessions = this.app.store.listSessions(this.app.workspace.id, { includeArchived: true });
|
|
2610
|
+
const match = this.resolveSessionSelection(requested, sessions);
|
|
2611
|
+
if (!match) {
|
|
2612
|
+
this.renderNotice(`No session matched ${requested}.`);
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
this.resumeSession(match);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const action = await this.selectOption("Sessions", [
|
|
2619
|
+
{ value: "resume", label: "Resume", description: "Attach to an existing session." },
|
|
2620
|
+
{ value: "new", label: "New session", description: "Start a fresh session in this workspace." },
|
|
2621
|
+
{ value: "rename", label: "Rename", description: "Change a session title." },
|
|
2622
|
+
{ value: "archive", label: "Archive", description: "Hide a finished or stale session from the default list." },
|
|
2623
|
+
{ value: "all", label: "Show all", description: "Include archived sessions and lock state." },
|
|
2624
|
+
], 0, [fg256(244, "Use /clear, /resume, or /sessions all for direct actions.")]);
|
|
2625
|
+
if (action === "new") {
|
|
2626
|
+
const session = await this.createTuiSession();
|
|
2627
|
+
this.renderPanel("Session Created", [this.sessionLabel(session)]);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
if (action === "all") {
|
|
2631
|
+
this.renderSessionList(true);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const includeArchived = action === "rename";
|
|
2635
|
+
const target = action === "resume"
|
|
2636
|
+
? await this.chooseResumeSession("Resume Session", false)
|
|
2637
|
+
: await this.chooseSession(action === "rename" ? "Rename Session" : "Archive Session", includeArchived);
|
|
2638
|
+
if (!target) {
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (action === "resume") {
|
|
2642
|
+
this.resumeSession(target);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
if (action === "rename") {
|
|
2646
|
+
const title = await this.ask("Session title", target.title);
|
|
2647
|
+
const renamed = this.app.store.renameSession(target.session_id, title);
|
|
2648
|
+
if (this.#sessionId === target.session_id) {
|
|
2649
|
+
this.#sessionId = renamed.session_id;
|
|
2650
|
+
}
|
|
2651
|
+
this.renderPanel("Session Renamed", [this.sessionLabel(renamed)]);
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (action === "archive") {
|
|
2655
|
+
const confirmed = await this.confirm(`Archive ${target.session_id.slice(0, 12)}?`, false);
|
|
2656
|
+
if (!confirmed) {
|
|
2657
|
+
this.renderNotice("Archive cancelled.");
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const archived = this.app.store.archiveSession(target.session_id);
|
|
2661
|
+
if (this.#sessionId === archived.session_id) {
|
|
2662
|
+
this.#sessionId = undefined;
|
|
2663
|
+
}
|
|
2664
|
+
this.renderPanel("Session Archived", [this.sessionLabel(archived)]);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
async renderResumeSessionView(args) {
|
|
2668
|
+
const requested = args.trim();
|
|
2669
|
+
if (requested) {
|
|
2670
|
+
const sessions = this.app.store.listSessions(this.app.workspace.id, { includeArchived: true });
|
|
2671
|
+
const match = this.resolveSessionSelection(requested, sessions);
|
|
2672
|
+
if (!match) {
|
|
2673
|
+
this.renderNotice(`No session matched ${requested}.`);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
this.resumeSession(match);
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const target = await this.chooseResumeSession("Resume Session", false);
|
|
2680
|
+
if (!target) {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
this.resumeSession(target);
|
|
2684
|
+
}
|
|
2685
|
+
async startFreshSessionFromClear() {
|
|
2686
|
+
const session = this.app.store.createSession(this.app.workspace);
|
|
2687
|
+
this.#sessionId = session.session_id;
|
|
2688
|
+
this.resetVisibleSessionSurface();
|
|
2689
|
+
stdout.write(ansi.clear);
|
|
2690
|
+
this.writeHomeFrame();
|
|
2691
|
+
this.#hasTranscript = true;
|
|
2692
|
+
}
|
|
2693
|
+
resumeSession(session) {
|
|
2694
|
+
this.#sessionId = session.session_id;
|
|
2695
|
+
this.resetVisibleSessionSurface();
|
|
2696
|
+
stdout.write(ansi.clear);
|
|
2697
|
+
this.writeHomeFrame();
|
|
2698
|
+
const transcript = renderSessionTranscript(this.app.store.listEvents(session.session_id), safeTerminalWidth());
|
|
2699
|
+
if (transcript) {
|
|
2700
|
+
stdout.write(transcript);
|
|
2701
|
+
}
|
|
2702
|
+
else {
|
|
2703
|
+
stdout.write(`${fg256(244, "No prior chat history in this session.")}\n\n`);
|
|
2704
|
+
}
|
|
2705
|
+
this.#hasTranscript = true;
|
|
2706
|
+
}
|
|
2707
|
+
resetVisibleSessionSurface() {
|
|
2708
|
+
this.#inlineRenderedLines = 0;
|
|
2709
|
+
this.#inlinePanelStartRow = undefined;
|
|
2710
|
+
this.#composerFooter = undefined;
|
|
2711
|
+
this.#composerActivity = undefined;
|
|
2712
|
+
this.#composerQueue = undefined;
|
|
2713
|
+
this.#composerPanel = undefined;
|
|
2714
|
+
this.#hasTranscript = false;
|
|
2715
|
+
}
|
|
2716
|
+
async createTuiSession() {
|
|
2717
|
+
const title = await this.ask("New session title", "New session");
|
|
2718
|
+
const session = this.app.store.createSession(this.app.workspace, title || "New session");
|
|
2719
|
+
this.#sessionId = session.session_id;
|
|
2720
|
+
return session;
|
|
2721
|
+
}
|
|
2722
|
+
createModeSession(title) {
|
|
2723
|
+
const session = this.app.store.createSession(this.app.workspace, title);
|
|
2724
|
+
this.#sessionId = session.session_id;
|
|
2725
|
+
return session;
|
|
2726
|
+
}
|
|
2727
|
+
async chooseSession(title, includeArchived) {
|
|
2728
|
+
const sessions = this.app.store.listSessions(this.app.workspace.id, { includeArchived });
|
|
2729
|
+
if (!sessions.length) {
|
|
2730
|
+
this.renderPanel(title, [includeArchived ? "No sessions for this workspace." : "No active sessions for this workspace."]);
|
|
2731
|
+
return undefined;
|
|
2732
|
+
}
|
|
2733
|
+
const defaultIndex = Math.max(0, sessions.findIndex((session) => session.session_id === this.#sessionId));
|
|
2734
|
+
const selected = await this.selectOption(title, sessions.map((session) => ({
|
|
2735
|
+
value: session.session_id,
|
|
2736
|
+
label: `${session.session_id.slice(0, 12)} · ${truncateToWidth(session.title, 36)}`,
|
|
2737
|
+
description: this.sessionDescription(session),
|
|
2738
|
+
})), defaultIndex);
|
|
2739
|
+
return sessions.find((session) => session.session_id === selected);
|
|
2740
|
+
}
|
|
2741
|
+
async chooseResumeSession(title, includeArchived) {
|
|
2742
|
+
const sessions = this.app.store.listSessions(this.app.workspace.id, { includeArchived });
|
|
2743
|
+
if (!sessions.length) {
|
|
2744
|
+
this.renderPanel(title, [includeArchived ? "No sessions for this workspace." : "No active sessions for this workspace."]);
|
|
2745
|
+
return undefined;
|
|
2746
|
+
}
|
|
2747
|
+
const defaultIndex = Math.max(0, sessions.findIndex((session) => session.session_id === this.#sessionId));
|
|
2748
|
+
let pageIndex = Math.floor(defaultIndex / RESUME_SESSION_PAGE_SIZE);
|
|
2749
|
+
let selected = defaultIndex % RESUME_SESSION_PAGE_SIZE;
|
|
2750
|
+
this.#rl?.pause();
|
|
2751
|
+
const clampSelection = () => {
|
|
2752
|
+
const page = resumeSessionPage(sessions, pageIndex);
|
|
2753
|
+
pageIndex = page.pageIndex;
|
|
2754
|
+
selected = Math.max(0, Math.min(selected, Math.max(0, page.items.length - 1)));
|
|
2755
|
+
return page;
|
|
2756
|
+
};
|
|
2757
|
+
const render = () => {
|
|
2758
|
+
const page = clampSelection();
|
|
2759
|
+
const lines = page.items.map((session, index) => {
|
|
2760
|
+
const label = `${session.session_id.slice(0, 12)} · ${truncateToWidth(session.title, 44)}`;
|
|
2761
|
+
return renderSetupOptionLine(label, this.sessionDescription(session), index === selected);
|
|
2762
|
+
});
|
|
2763
|
+
const pageHint = `${page.pageIndex + 1}/${page.totalPages} · ${page.totalItems} sessions · ←/→ page · ↑/↓ move · enter resume · esc cancel`;
|
|
2764
|
+
this.renderCenteredPanel(title, [...lines, "", setupHint(pageHint)], true);
|
|
2765
|
+
};
|
|
2766
|
+
render();
|
|
2767
|
+
return await new Promise((resolve, reject) => {
|
|
2768
|
+
const cleanup = () => {
|
|
2769
|
+
stdin.off("data", onData);
|
|
2770
|
+
stdout.off("resize", onResize);
|
|
2771
|
+
if (stdin.isTTY) {
|
|
2772
|
+
stdin.setRawMode(false);
|
|
2773
|
+
}
|
|
2774
|
+
this.resumeReadline();
|
|
2775
|
+
};
|
|
2776
|
+
const finish = () => {
|
|
2777
|
+
const page = clampSelection();
|
|
2778
|
+
const session = page.items[selected];
|
|
2779
|
+
cleanup();
|
|
2780
|
+
stdout.write("\n");
|
|
2781
|
+
resolve(session);
|
|
2782
|
+
};
|
|
2783
|
+
const cancel = () => {
|
|
2784
|
+
cleanup();
|
|
2785
|
+
reject(new Error("Selection cancelled"));
|
|
2786
|
+
};
|
|
2787
|
+
const movePage = (delta) => {
|
|
2788
|
+
const current = resumeSessionPage(sessions, pageIndex);
|
|
2789
|
+
pageIndex = Math.max(0, Math.min(current.totalPages - 1, pageIndex + delta));
|
|
2790
|
+
clampSelection();
|
|
2791
|
+
render();
|
|
2792
|
+
};
|
|
2793
|
+
const onData = (chunk) => {
|
|
2794
|
+
for (const key of terminalInputTokens(chunk.toString("utf8"))) {
|
|
2795
|
+
if (key === "\u0003" || key === "\u001b") {
|
|
2796
|
+
cancel();
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
if (key === "\u001b[A" || key === "k") {
|
|
2800
|
+
const page = clampSelection();
|
|
2801
|
+
selected = (selected - 1 + page.items.length) % page.items.length;
|
|
2802
|
+
render();
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (key === "\u001b[B" || key === "j") {
|
|
2806
|
+
const page = clampSelection();
|
|
2807
|
+
selected = (selected + 1) % page.items.length;
|
|
2808
|
+
render();
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
if (key === "\u001b[D") {
|
|
2812
|
+
movePage(-1);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (key === "\u001b[C") {
|
|
2816
|
+
movePage(1);
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
if (key === " " || key === "\r" || key === "\n") {
|
|
2820
|
+
finish();
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
};
|
|
2825
|
+
const onResize = () => {
|
|
2826
|
+
render();
|
|
2827
|
+
};
|
|
2828
|
+
stdin.setRawMode(true);
|
|
2829
|
+
stdin.resume();
|
|
2830
|
+
stdin.on("data", onData);
|
|
2831
|
+
stdout.on("resize", onResize);
|
|
2832
|
+
render();
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
renderSessionList(includeArchived) {
|
|
2836
|
+
const sessions = this.app.store.listSessions(this.app.workspace.id, { includeArchived });
|
|
2837
|
+
this.renderPanel(includeArchived ? "All Sessions" : "Sessions", sessions.length
|
|
2838
|
+
? sessions.map((session, index) => ` ${index + 1}. ${this.sessionLabel(session)}`)
|
|
2839
|
+
: [includeArchived ? "No sessions for this workspace." : "No active sessions for this workspace."]);
|
|
2840
|
+
}
|
|
2841
|
+
async renderJobsView(args = "") {
|
|
2842
|
+
const requested = args.trim().toLowerCase();
|
|
2843
|
+
const action = requested
|
|
2844
|
+
? requested
|
|
2845
|
+
: await this.selectOption("Jobs", [
|
|
2846
|
+
{ value: "status", label: "Status", description: "Show daemon and job state." },
|
|
2847
|
+
{ value: "queue", label: "Queue run", description: "Start a supervised background task." },
|
|
2848
|
+
{ value: "attach", label: "Attach", description: "Inspect a job and recent session events." },
|
|
2849
|
+
{ value: "detach", label: "Detach", description: "Leave a queued or running job supervised." },
|
|
2850
|
+
{ value: "cancel", label: "Cancel", description: "Request cancellation for an active job." },
|
|
2851
|
+
], 0, [fg256(244, "Jobs use the same durable session event log as chat.")]);
|
|
2852
|
+
if (!["status", "queue", "attach", "detach", "cancel"].includes(action)) {
|
|
2853
|
+
this.renderNotice(`Unknown jobs action ${args}.`);
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
switch (action) {
|
|
2857
|
+
case "status":
|
|
2858
|
+
await this.renderDaemonStatusPanel();
|
|
2859
|
+
return;
|
|
2860
|
+
case "queue":
|
|
2861
|
+
await this.queueDaemonRunFromTui();
|
|
2862
|
+
return;
|
|
2863
|
+
case "attach":
|
|
2864
|
+
await this.attachDaemonJobFromTui();
|
|
2865
|
+
return;
|
|
2866
|
+
case "detach":
|
|
2867
|
+
await this.detachDaemonJobFromTui();
|
|
2868
|
+
return;
|
|
2869
|
+
case "cancel":
|
|
2870
|
+
await this.cancelDaemonJobFromTui();
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
async renderDaemonStatusPanel() {
|
|
2875
|
+
const status = await daemonStatus(this.options.stateDir);
|
|
2876
|
+
this.renderPanel("Jobs", [
|
|
2877
|
+
`${fg256(39, "Daemon")} ${status.alive ? `alive pid ${status.pid}` : "not running"}`,
|
|
2878
|
+
"",
|
|
2879
|
+
...(status.jobs.length ? status.jobs.map((job) => ` ${this.jobLabel(job)}`) : [" no jobs"]),
|
|
2880
|
+
]);
|
|
2881
|
+
}
|
|
2882
|
+
async queueDaemonRunFromTui() {
|
|
2883
|
+
const prompt = await this.ask("Background task", "Run repository validation and record evidence");
|
|
2884
|
+
const trimmed = prompt.trim();
|
|
2885
|
+
if (!trimmed) {
|
|
2886
|
+
this.renderNotice("No background task queued.");
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
const job = await queueDaemonRun({
|
|
2890
|
+
stateDir: this.options.stateDir,
|
|
2891
|
+
workspaceRoot: this.app.workspace.root,
|
|
2892
|
+
sessionId: this.#sessionId,
|
|
2893
|
+
prompt: trimmed,
|
|
2894
|
+
title: titleFromPrompt(trimmed),
|
|
2895
|
+
});
|
|
2896
|
+
const status = await startDaemon({ stateDir: this.options.stateDir });
|
|
2897
|
+
this.#sessionId = job.session_id;
|
|
2898
|
+
this.renderPanel("Job Queued", [
|
|
2899
|
+
`${fg256(48, "•")} ${this.jobLabel(job)}`,
|
|
2900
|
+
`${fg256(39, "Daemon")} ${status.alive ? `alive pid ${status.pid}` : "start requested"}`,
|
|
2901
|
+
]);
|
|
2902
|
+
}
|
|
2903
|
+
async attachDaemonJobFromTui() {
|
|
2904
|
+
const job = await this.chooseDaemonJob("Attach Job");
|
|
2905
|
+
if (!job) {
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
const attached = await attachDaemonJob(this.options.stateDir, job.job_id);
|
|
2909
|
+
this.#sessionId = attached.job.session_id;
|
|
2910
|
+
const events = attached.events.slice(-10);
|
|
2911
|
+
this.renderPanel("Job Attached", [
|
|
2912
|
+
this.jobLabel(attached.job),
|
|
2913
|
+
"",
|
|
2914
|
+
fg256(39, "Recent events"),
|
|
2915
|
+
...(events.length ? events.map((event) => ` ${renderCompactEventLine(event)}`) : [" no events"]),
|
|
2916
|
+
]);
|
|
2917
|
+
}
|
|
2918
|
+
async detachDaemonJobFromTui() {
|
|
2919
|
+
const job = await this.chooseDaemonJob("Detach Job", (item) => item.status === "queued" || item.status === "running" || item.status === "detached");
|
|
2920
|
+
if (!job) {
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const detached = await detachDaemonJob(this.options.stateDir, job.job_id);
|
|
2924
|
+
this.renderPanel("Job Detached", [this.jobLabel(detached)]);
|
|
2925
|
+
}
|
|
2926
|
+
async cancelDaemonJobFromTui() {
|
|
2927
|
+
const job = await this.chooseDaemonJob("Cancel Job", isCancellableJob);
|
|
2928
|
+
if (!job) {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
const confirmed = await this.confirm(`Cancel ${job.job_id.slice(0, 12)}?`, false);
|
|
2932
|
+
if (!confirmed) {
|
|
2933
|
+
this.renderNotice("Cancel skipped.");
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const cancelled = await cancelDaemonJob(this.options.stateDir, job.job_id);
|
|
2937
|
+
this.renderPanel("Job Cancel", [this.jobLabel(cancelled)]);
|
|
2938
|
+
}
|
|
2939
|
+
async chooseDaemonJob(title, filter = () => true) {
|
|
2940
|
+
const jobs = (await daemonStatus(this.options.stateDir)).jobs.filter(filter);
|
|
2941
|
+
if (!jobs.length) {
|
|
2942
|
+
this.renderPanel(title, ["No matching jobs."]);
|
|
2943
|
+
return undefined;
|
|
2944
|
+
}
|
|
2945
|
+
const selected = await this.selectOption(title, jobs.map((job) => ({
|
|
2946
|
+
value: job.job_id,
|
|
2947
|
+
label: `${job.job_id.slice(0, 12)} · ${job.status}`,
|
|
2948
|
+
description: `${job.session_id.slice(0, 12)} · ${truncateToWidth(job.prompt, 70)}`,
|
|
2949
|
+
})));
|
|
2950
|
+
return jobs.find((job) => job.job_id === selected);
|
|
2951
|
+
}
|
|
2952
|
+
jobLabel(job) {
|
|
2953
|
+
const session = this.app.store.getSession(job.session_id);
|
|
2954
|
+
const sessionLabel = session ? `${session.session_id.slice(0, 12)} · ${session.title}` : job.session_id.slice(0, 12);
|
|
2955
|
+
return `${job.status.padEnd(16)} ${job.job_id.slice(0, 12)} · ${sessionLabel} · ${truncateToWidth(job.prompt, Math.max(24, terminalWidth() - 54))}`;
|
|
2956
|
+
}
|
|
2957
|
+
renderFormattedEventView(title, filter, render) {
|
|
2958
|
+
const session = this.optionalSession();
|
|
2959
|
+
if (!session) {
|
|
2960
|
+
this.renderPanel(title, ["No active session yet."]);
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
this.renderPanel(title, render(this.app.store.listEvents(session.session_id).filter(filter)));
|
|
2964
|
+
}
|
|
2965
|
+
async renderAcceptanceView(args = "") {
|
|
2966
|
+
const action = args.trim().toLowerCase();
|
|
2967
|
+
if (action === "run") {
|
|
2968
|
+
await this.runAcceptanceFromTui();
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
if (action && action !== "status") {
|
|
2972
|
+
this.renderNotice(`Unknown acceptance action ${args}. Use /acceptance status or /acceptance run.`);
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const directConfigured = Boolean(this.app.config.model_setup.base_url && this.app.config.model_setup.model);
|
|
2976
|
+
const omni = this.app.config.omni.endpoints;
|
|
2977
|
+
const lines = [
|
|
2978
|
+
`${checkbox(directConfigured)} coding endpoint configured`,
|
|
2979
|
+
...TUI_OMNI_SETUP_CAPABILITIES.map((capability) => {
|
|
2980
|
+
const endpoint = omni[capability.name];
|
|
2981
|
+
const configured = Boolean(this.app.config.omni.enabled && endpoint?.base_url && endpoint.model);
|
|
2982
|
+
const suffix = capability.requiredForAcceptance ? fg256(244, "required") : fg256(244, "optional");
|
|
2983
|
+
return `${checkbox(configured)} Omni ${capability.label} configured · ${suffix}`;
|
|
2984
|
+
}),
|
|
2985
|
+
`${checkbox(false)} AMD direct vLLM deployment check`,
|
|
2986
|
+
`${checkbox(false)} AMD vLLM-Omni deployment check`,
|
|
2987
|
+
`${checkbox(false)} TUI-driven coding task with tools`,
|
|
2988
|
+
`${checkbox(false)} context compression and continuation`,
|
|
2989
|
+
`${checkbox(false)} daemon attach/detach/status/cancel on final task`,
|
|
2990
|
+
"",
|
|
2991
|
+
`${fg256(39, "/acceptance run")} runs the real endpoint workflow from inside the TUI.`,
|
|
2992
|
+
fg256(243, "It fails fast until direct vLLM and all Omni endpoints are configured."),
|
|
2993
|
+
];
|
|
2994
|
+
this.renderPanel("Final Acceptance", lines);
|
|
2995
|
+
}
|
|
2996
|
+
async runAcceptanceFromTui() {
|
|
2997
|
+
this.renderPanel("Final Acceptance", [
|
|
2998
|
+
`${fg256(75, "•")} starting real endpoint workflow`,
|
|
2999
|
+
fg256(243, "Using configured chat, Omni, session, context, tool, activity, and daemon paths."),
|
|
3000
|
+
]);
|
|
3001
|
+
try {
|
|
3002
|
+
const result = await runFinalAcceptance({
|
|
3003
|
+
workspaceRoot: this.app.workspace.root,
|
|
3004
|
+
stateDir: this.options.stateDir,
|
|
3005
|
+
daemon: true,
|
|
3006
|
+
});
|
|
3007
|
+
if (result.session_id) {
|
|
3008
|
+
this.#sessionId = result.session_id;
|
|
3009
|
+
}
|
|
3010
|
+
const evidence = result.evidence;
|
|
3011
|
+
const toolCalls = Array.isArray(evidence.tool_calls) ? evidence.tool_calls : [];
|
|
3012
|
+
const cachedEvidence = Array.isArray(evidence.direct_cached_token_evidence) ? evidence.direct_cached_token_evidence : [];
|
|
3013
|
+
const report = stringField(evidence.report_path);
|
|
3014
|
+
const lines = [
|
|
3015
|
+
result.ok ? fg256(48, "✓ passed") : fg256(203, "× failed"),
|
|
3016
|
+
...(result.session_id ? [`${fg256(39, "session")} ${result.session_id}`] : []),
|
|
3017
|
+
...(report ? [`${fg256(39, "report")} ${report}`] : []),
|
|
3018
|
+
`${fg256(39, "tools")} ${toolCalls.length}`,
|
|
3019
|
+
`${fg256(39, "direct cache samples")} ${cachedEvidence.length}`,
|
|
3020
|
+
"",
|
|
3021
|
+
...(result.failures.length
|
|
3022
|
+
? [
|
|
3023
|
+
fg256(203, "Failures"),
|
|
3024
|
+
...result.failures.slice(0, 12).map((failure) => ` ${failure}`),
|
|
3025
|
+
...(result.failures.length > 12 ? [fg256(244, ` ... ${result.failures.length - 12} more`)] : []),
|
|
3026
|
+
]
|
|
3027
|
+
: [fg256(48, "All acceptance checks passed.")]),
|
|
3028
|
+
];
|
|
3029
|
+
this.renderPanel("Final Acceptance", lines);
|
|
3030
|
+
}
|
|
3031
|
+
catch (error) {
|
|
3032
|
+
this.renderPanel("Final Acceptance", [fg256(203, error instanceof Error ? error.message : String(error))]);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
renderHelp() {
|
|
3036
|
+
this.renderPanel("Help", [
|
|
3037
|
+
fg256(39, "Keyboard"),
|
|
3038
|
+
" Enter sends a prompt",
|
|
3039
|
+
" / opens product commands",
|
|
3040
|
+
" $ opens the skill catalog",
|
|
3041
|
+
" Esc interrupts the active loop when the composer is empty",
|
|
3042
|
+
" Ctrl+T expands compact tool traces",
|
|
3043
|
+
"",
|
|
3044
|
+
fg256(39, "Commands"),
|
|
3045
|
+
...SLASH_COMMANDS.map((command) => ` /${command.name.padEnd(11)} ${command.description}`),
|
|
3046
|
+
"",
|
|
3047
|
+
fg256(39, "Subcommands"),
|
|
3048
|
+
` /skills list · manage`,
|
|
3049
|
+
` /goal show · set · pause · resume · budget · complete · drop`,
|
|
3050
|
+
` /plan show · set · pause · resume · approve · drop`,
|
|
3051
|
+
` /autoresearch status · off · clear`,
|
|
3052
|
+
` /tools expand · compact · last`,
|
|
3053
|
+
` /jobs status · queue · attach · detach · cancel`,
|
|
3054
|
+
` /sessions resume · new · all`,
|
|
3055
|
+
` /acceptance status · run`,
|
|
3056
|
+
]);
|
|
3057
|
+
}
|
|
3058
|
+
async submitPrompt(prompt, options = {}) {
|
|
3059
|
+
if (!(await this.waitForCodeIntelligenceBeforeChat())) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
if (options.renderPrompt !== false) {
|
|
3063
|
+
this.renderSubmittedPrompt(prompt);
|
|
3064
|
+
}
|
|
3065
|
+
const startedAt = Date.now();
|
|
3066
|
+
const markdown = new MarkdownStreamRenderer({ width: Math.max(40, terminalWidth() - 4) });
|
|
3067
|
+
const renderState = {
|
|
3068
|
+
lastSegment: "none",
|
|
3069
|
+
};
|
|
3070
|
+
const liveToolCallIds = new Set();
|
|
3071
|
+
const activity = this.startActivityIndicator("Prefill with Inferoa");
|
|
3072
|
+
let sawModelDelta = false;
|
|
3073
|
+
const abort = new AbortController();
|
|
3074
|
+
this.#activeAbort = abort;
|
|
3075
|
+
try {
|
|
3076
|
+
const result = await this.app.runtime.run({
|
|
3077
|
+
prompt,
|
|
3078
|
+
session_id: this.#sessionId,
|
|
3079
|
+
client_id: randomId("tui"),
|
|
3080
|
+
signal: abort.signal,
|
|
3081
|
+
onDelta: (text) => {
|
|
3082
|
+
if (!sawModelDelta) {
|
|
3083
|
+
sawModelDelta = true;
|
|
3084
|
+
activity.status("Decode with Inferoa");
|
|
3085
|
+
}
|
|
3086
|
+
const rendered = markdown.write(text);
|
|
3087
|
+
if (!rendered) {
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
activity.pauseForOutput({ redraw: false });
|
|
3091
|
+
if (renderState.lastSegment === "tool") {
|
|
3092
|
+
this.writeTranscript("\n");
|
|
3093
|
+
}
|
|
3094
|
+
this.writeTranscript(rendered);
|
|
3095
|
+
renderState.lastSegment = "assistant";
|
|
3096
|
+
},
|
|
3097
|
+
onStatus: (event) => {
|
|
3098
|
+
if (event.type === "model_retry") {
|
|
3099
|
+
activity.status(`Retrying Inferoa in ${formatDuration(event.delay_ms)}`);
|
|
3100
|
+
}
|
|
3101
|
+
if (event.type === "compression_start") {
|
|
3102
|
+
activity.status(formatCompressionStartActivity(event));
|
|
3103
|
+
}
|
|
3104
|
+
if (event.type === "compression_end") {
|
|
3105
|
+
activity.record(formatCompressionActivityLine(event));
|
|
3106
|
+
}
|
|
3107
|
+
if (event.type === "tool_start") {
|
|
3108
|
+
activity.status(event.summary ?? toolActivityAction(event.tool_name));
|
|
3109
|
+
}
|
|
3110
|
+
if (event.type === "tool_end") {
|
|
3111
|
+
let output = "";
|
|
3112
|
+
const flushed = markdown.flush();
|
|
3113
|
+
if (flushed) {
|
|
3114
|
+
if (renderState.lastSegment === "tool") {
|
|
3115
|
+
output += "\n";
|
|
3116
|
+
}
|
|
3117
|
+
output += flushed;
|
|
3118
|
+
renderState.lastSegment = "assistant";
|
|
3119
|
+
}
|
|
3120
|
+
const toolBlock = this.toolTraceForCallBlock(event.session_id, event.run_id, event.tool_call_id, renderState.lastSegment === "assistant");
|
|
3121
|
+
if (toolBlock) {
|
|
3122
|
+
output += toolBlock;
|
|
3123
|
+
liveToolCallIds.add(event.tool_call_id);
|
|
3124
|
+
renderState.lastSegment = "tool";
|
|
3125
|
+
}
|
|
3126
|
+
if (output) {
|
|
3127
|
+
activity.pauseForOutput({ redraw: false });
|
|
3128
|
+
this.writeTranscript(output);
|
|
3129
|
+
}
|
|
3130
|
+
else {
|
|
3131
|
+
activity.status(formatToolActivityLine(event.tool_name, event.ok, event.summary, event.duration_ms));
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
onClarify: async (request) => {
|
|
3136
|
+
activity.pauseForOutput({ redraw: false });
|
|
3137
|
+
return await this.askClarification(request);
|
|
3138
|
+
},
|
|
3139
|
+
});
|
|
3140
|
+
activity.stop({ redraw: false });
|
|
3141
|
+
let finalOutput = "";
|
|
3142
|
+
const flushed = markdown.flush();
|
|
3143
|
+
if (flushed) {
|
|
3144
|
+
if (renderState.lastSegment === "tool") {
|
|
3145
|
+
finalOutput += "\n";
|
|
3146
|
+
}
|
|
3147
|
+
finalOutput += flushed;
|
|
3148
|
+
renderState.lastSegment = "assistant";
|
|
3149
|
+
}
|
|
3150
|
+
this.#sessionId = result.session.session_id;
|
|
3151
|
+
const evidence = this.latestTurnEvidence(result.session.session_id, result.run_id);
|
|
3152
|
+
const toolSummary = this.toolSummaryBlock(result.session.session_id, result.run_id, renderState.lastSegment === "assistant", liveToolCallIds);
|
|
3153
|
+
if (toolSummary) {
|
|
3154
|
+
finalOutput += toolSummary;
|
|
3155
|
+
renderState.lastSegment = "tool";
|
|
3156
|
+
}
|
|
3157
|
+
const footer = renderCacheFooter({
|
|
3158
|
+
...evidence,
|
|
3159
|
+
latencyMs: Date.now() - startedAt,
|
|
3160
|
+
cacheKind: cacheTurnKind(this.app.store.listEvents(result.session.session_id), result.run_id),
|
|
3161
|
+
});
|
|
3162
|
+
this.#composerFooter = footer || undefined;
|
|
3163
|
+
this.writeTranscript(withConversationGap(finalOutput));
|
|
3164
|
+
}
|
|
3165
|
+
catch (error) {
|
|
3166
|
+
activity.stop({ redraw: false });
|
|
3167
|
+
const flushed = markdown.flush();
|
|
3168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3169
|
+
const renderedError = isAbortError(error) ? fg256(244, `Interrupted current loop: ${message}`) : fg256(203, message);
|
|
3170
|
+
this.writeTranscript(`${flushed}${flushed ? "\n" : ""}${renderedError}\n\n`);
|
|
3171
|
+
}
|
|
3172
|
+
finally {
|
|
3173
|
+
if (this.#activeAbort === abort) {
|
|
3174
|
+
this.#activeAbort = undefined;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
startActivityIndicator(label) {
|
|
3179
|
+
let active = false;
|
|
3180
|
+
let frameIndex = 0;
|
|
3181
|
+
let currentLabel = label;
|
|
3182
|
+
let startedAt = Date.now();
|
|
3183
|
+
let hasStarted = false;
|
|
3184
|
+
let timer;
|
|
3185
|
+
const render = () => {
|
|
3186
|
+
if (!active) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
const now = Date.now();
|
|
3190
|
+
const width = safeTerminalWidth();
|
|
3191
|
+
this.#composerActivity = renderActivityLine(currentLabel, now - startedAt, frameIndex, width);
|
|
3192
|
+
frameIndex += 1;
|
|
3193
|
+
if (!this.#activeComposerActivityRedraw?.()) {
|
|
3194
|
+
this.#activeComposerRedraw?.();
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
const ensure = () => {
|
|
3198
|
+
if (active) {
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
active = true;
|
|
3202
|
+
if (!hasStarted) {
|
|
3203
|
+
startedAt = Date.now();
|
|
3204
|
+
hasStarted = true;
|
|
3205
|
+
}
|
|
3206
|
+
render();
|
|
3207
|
+
timer = setInterval(render, 140);
|
|
3208
|
+
timer.unref?.();
|
|
3209
|
+
};
|
|
3210
|
+
const clear = (options = {}) => {
|
|
3211
|
+
if (!active && !this.#composerActivity) {
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
active = false;
|
|
3215
|
+
if (timer) {
|
|
3216
|
+
clearInterval(timer);
|
|
3217
|
+
timer = undefined;
|
|
3218
|
+
}
|
|
3219
|
+
const hadActivity = Boolean(this.#composerActivity);
|
|
3220
|
+
if (this.#composerActivity) {
|
|
3221
|
+
this.#composerActivity = undefined;
|
|
3222
|
+
}
|
|
3223
|
+
if (hadActivity && options.redraw !== false) {
|
|
3224
|
+
this.#activeComposerRedraw?.();
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
ensure();
|
|
3228
|
+
const writeTranscript = (text) => this.writeTranscript(text);
|
|
3229
|
+
return {
|
|
3230
|
+
status(nextLabel) {
|
|
3231
|
+
currentLabel = nextLabel;
|
|
3232
|
+
ensure();
|
|
3233
|
+
render();
|
|
3234
|
+
},
|
|
3235
|
+
record(line) {
|
|
3236
|
+
clear({ redraw: false });
|
|
3237
|
+
writeTranscript(`${line}\n\n`);
|
|
3238
|
+
ensure();
|
|
3239
|
+
},
|
|
3240
|
+
pauseForOutput(options) {
|
|
3241
|
+
clear(options);
|
|
3242
|
+
},
|
|
3243
|
+
stop(options) {
|
|
3244
|
+
clear(options);
|
|
3245
|
+
},
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
renderSubmittedPrompt(prompt) {
|
|
3249
|
+
if (!this.#hasTranscript && !this.#sessionId) {
|
|
3250
|
+
this.writeHomeFrame();
|
|
3251
|
+
}
|
|
3252
|
+
const width = safeTerminalWidth();
|
|
3253
|
+
const maxPromptLines = 10;
|
|
3254
|
+
const rawLines = prompt.split(/\r?\n/);
|
|
3255
|
+
const promptLines = rawLines.slice(0, maxPromptLines);
|
|
3256
|
+
if (rawLines.length > maxPromptLines) {
|
|
3257
|
+
promptLines.push(`... ${rawLines.length - maxPromptLines} more lines`);
|
|
3258
|
+
}
|
|
3259
|
+
const body = promptLines.length ? promptLines : [""];
|
|
3260
|
+
const lines = [
|
|
3261
|
+
bgLine(236, "", width),
|
|
3262
|
+
...body.map((line, index) => {
|
|
3263
|
+
const prefix = index === 0 ? "› " : " ";
|
|
3264
|
+
return bgLine(236, `${prefix}${truncateToWidth(line, Math.max(10, width - visibleWidth(prefix) - 1))}`, width);
|
|
3265
|
+
}),
|
|
3266
|
+
bgLine(236, "", width),
|
|
3267
|
+
];
|
|
3268
|
+
this.writeTranscript(withConversationGap(lines.join("\n")));
|
|
3269
|
+
}
|
|
3270
|
+
writeTranscript(text) {
|
|
3271
|
+
if (!text) {
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
this.#hasTranscript = true;
|
|
3275
|
+
const erase = this.#activeComposerErase;
|
|
3276
|
+
const redraw = this.#activeComposerRedraw;
|
|
3277
|
+
if (!erase || !redraw) {
|
|
3278
|
+
stdout.write(text);
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
erase();
|
|
3282
|
+
stdout.write(text);
|
|
3283
|
+
if (!text.endsWith("\n")) {
|
|
3284
|
+
stdout.write("\n");
|
|
3285
|
+
}
|
|
3286
|
+
redraw();
|
|
3287
|
+
}
|
|
3288
|
+
toolTraceForCallBlock(sessionId, runId, toolCallId, leadingGap = true) {
|
|
3289
|
+
const events = this.app.store
|
|
3290
|
+
.listEvents(sessionId)
|
|
3291
|
+
.filter((event) => event.run_id === runId &&
|
|
3292
|
+
(event.type === "tool.call" || event.type === "tool.result") &&
|
|
3293
|
+
stringField(event.data.tool_call_id) === toolCallId);
|
|
3294
|
+
if (!events.some((event) => event.type === "tool.result")) {
|
|
3295
|
+
return undefined;
|
|
3296
|
+
}
|
|
3297
|
+
const lines = renderToolCards(events, this.app.store, { collapseCompact: false });
|
|
3298
|
+
return `${leadingGap ? "\n" : ""}${lines.join("\n")}\n`;
|
|
3299
|
+
}
|
|
3300
|
+
toolSummaryBlock(sessionId, runId, leadingGap = true, excludeToolCallIds = new Set()) {
|
|
3301
|
+
const events = this.app.store.listEvents(sessionId).filter((event) => {
|
|
3302
|
+
if (event.run_id !== runId || (event.type !== "tool.call" && event.type !== "tool.result")) {
|
|
3303
|
+
return false;
|
|
3304
|
+
}
|
|
3305
|
+
const toolCallId = stringField(event.data.tool_call_id);
|
|
3306
|
+
return !toolCallId || !excludeToolCallIds.has(toolCallId);
|
|
3307
|
+
});
|
|
3308
|
+
if (!events.length) {
|
|
3309
|
+
return undefined;
|
|
3310
|
+
}
|
|
3311
|
+
const lines = renderToolCards(events, this.app.store, { collapseCompact: this.#toolTraceMode === "compact" });
|
|
3312
|
+
return `${leadingGap ? "\n" : ""}${lines.join("\n")}\n`;
|
|
3313
|
+
}
|
|
3314
|
+
latestTurnEvidence(sessionId, runId) {
|
|
3315
|
+
const evidence = this.app.store.listEndpointEvidence(sessionId).slice().reverse().find((item) => item.run_id === runId) ?? {};
|
|
3316
|
+
const events = this.app.store.listEvents(sessionId).filter((event) => event.run_id === runId && event.type === "model.response.settled");
|
|
3317
|
+
const settled = events.at(-1)?.data ?? {};
|
|
3318
|
+
return {
|
|
3319
|
+
usage: settled.usage ?? evidence.usage,
|
|
3320
|
+
requestId: stringField(settled.request_id) ?? stringField(evidence.request_id),
|
|
3321
|
+
responseId: stringField(settled.response_id) ?? stringField(evidence.response_id),
|
|
3322
|
+
model: stringField(settled.model) ?? stringField(evidence.model),
|
|
3323
|
+
mode: this.app.config.model_setup.mode,
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
renderPanel(title, body) {
|
|
3327
|
+
if (this.#inlineMode) {
|
|
3328
|
+
this.renderInlinePanel(title, body);
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
stdout.write("\n");
|
|
3332
|
+
stdout.write(frame(title, body).join("\n"));
|
|
3333
|
+
stdout.write("\n\n");
|
|
3334
|
+
}
|
|
3335
|
+
renderCenteredPanel(title, body, clear = false) {
|
|
3336
|
+
if (this.#inlineMode) {
|
|
3337
|
+
this.renderInlineCenteredPanel(title, body);
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
const panelWidth = setupDialogTitle(title) ? setupDialogFrameWidth() : Math.min(terminalWidth() - 4, 86);
|
|
3341
|
+
const lines = commandDeckFrame(title, body, panelWidth);
|
|
3342
|
+
const topPad = Math.max(1, Math.floor((terminalHeight() - lines.length) / 2));
|
|
3343
|
+
if (clear) {
|
|
3344
|
+
stdout.write(ansi.clear);
|
|
3345
|
+
}
|
|
3346
|
+
else {
|
|
3347
|
+
stdout.write("\n");
|
|
3348
|
+
}
|
|
3349
|
+
stdout.write("\n".repeat(topPad));
|
|
3350
|
+
stdout.write(centerBlock(lines, terminalWidth()).join("\n"));
|
|
3351
|
+
stdout.write("\n\n");
|
|
3352
|
+
}
|
|
3353
|
+
renderInlinePanel(title, body) {
|
|
3354
|
+
this.eraseInlinePanel();
|
|
3355
|
+
const width = safeTerminalWidth();
|
|
3356
|
+
const lines = [
|
|
3357
|
+
bg256(236, padRight(` ${title}`, width)),
|
|
3358
|
+
bg256(236, padRight("", width)),
|
|
3359
|
+
...body.map((line) => bg256(236, padRight(` ${line}`, width))),
|
|
3360
|
+
bg256(236, padRight("", width)),
|
|
3361
|
+
];
|
|
3362
|
+
this.#inlineRenderedLines = lines.length;
|
|
3363
|
+
this.#inlinePanelStartRow = undefined;
|
|
3364
|
+
stdout.write(lines.join("\n"));
|
|
3365
|
+
stdout.write("\n");
|
|
3366
|
+
}
|
|
3367
|
+
renderInlineCenteredPanel(title, body) {
|
|
3368
|
+
this.eraseInlinePanel();
|
|
3369
|
+
if (!setupDialogTitle(title)) {
|
|
3370
|
+
this.renderInlinePanel(title, body);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
const panelWidth = setupDialogTitle(title)
|
|
3374
|
+
? setupDialogFrameWidth()
|
|
3375
|
+
: Math.min(Math.max(52, Math.floor(terminalWidth() * 0.58)), 96, Math.max(52, terminalWidth() - 6));
|
|
3376
|
+
const lines = commandDeckFrame(title, body, panelWidth);
|
|
3377
|
+
const startRow = Math.max(1, terminalHeight() - lines.length - 2);
|
|
3378
|
+
this.#inlineRenderedLines = terminalHeight() - startRow + 1;
|
|
3379
|
+
this.#inlinePanelStartRow = startRow;
|
|
3380
|
+
stdout.write(`\x1b[${startRow};1H\x1b[J`);
|
|
3381
|
+
stdout.write(lines.join("\n"));
|
|
3382
|
+
}
|
|
3383
|
+
eraseInlinePanel() {
|
|
3384
|
+
if (!this.#inlineRenderedLines) {
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
if (this.#inlinePanelStartRow !== undefined) {
|
|
3388
|
+
stdout.write(`\x1b[${this.#inlinePanelStartRow};1H\x1b[J`);
|
|
3389
|
+
}
|
|
3390
|
+
else {
|
|
3391
|
+
stdout.write(`\x1b[${Math.max(0, this.#inlineRenderedLines)}A\r\x1b[J`);
|
|
3392
|
+
}
|
|
3393
|
+
this.#inlineRenderedLines = 0;
|
|
3394
|
+
this.#inlinePanelStartRow = undefined;
|
|
3395
|
+
}
|
|
3396
|
+
resumeReadline() {
|
|
3397
|
+
try {
|
|
3398
|
+
this.#rl?.resume();
|
|
3399
|
+
}
|
|
3400
|
+
catch {
|
|
3401
|
+
// Ctrl+C can close readline before raw-mode cleanup runs.
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
renderNotice(message) {
|
|
3405
|
+
this.renderPanel("Notice", [fg256(203, message)]);
|
|
3406
|
+
}
|
|
3407
|
+
renderUnknownSlashCommand(input) {
|
|
3408
|
+
if (!this.#hasTranscript && !this.#sessionId) {
|
|
3409
|
+
this.writeHomeFrame();
|
|
3410
|
+
}
|
|
3411
|
+
const command = input.trim().slice(1).split(/\s+/)[0] ?? "";
|
|
3412
|
+
this.writeTranscript(withConversationGap(renderUnknownSlashCommandNotice(command)));
|
|
3413
|
+
}
|
|
3414
|
+
optionalSession() {
|
|
3415
|
+
return this.#sessionId ? this.app.store.getSession(this.#sessionId) : undefined;
|
|
3416
|
+
}
|
|
3417
|
+
requiredSession() {
|
|
3418
|
+
const session = this.optionalSession();
|
|
3419
|
+
if (!session) {
|
|
3420
|
+
throw new Error("No active session");
|
|
3421
|
+
}
|
|
3422
|
+
return session;
|
|
3423
|
+
}
|
|
3424
|
+
resolveModelSelection(input, models) {
|
|
3425
|
+
const index = Number.parseInt(input, 10) - 1;
|
|
3426
|
+
if (models[index]) {
|
|
3427
|
+
return models[index];
|
|
3428
|
+
}
|
|
3429
|
+
return input.trim() || undefined;
|
|
3430
|
+
}
|
|
3431
|
+
resolveSessionSelection(input, sessions) {
|
|
3432
|
+
const index = Number.parseInt(input, 10) - 1;
|
|
3433
|
+
if (sessions[index]) {
|
|
3434
|
+
return sessions[index];
|
|
3435
|
+
}
|
|
3436
|
+
const matches = sessions.filter((session) => session.session_id.startsWith(input));
|
|
3437
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
3438
|
+
}
|
|
3439
|
+
sessionLabel(session) {
|
|
3440
|
+
return `${session.session_id.slice(0, 12)} · ${session.title} · ${this.sessionDescription(session)} · ${session.updated_at}`;
|
|
3441
|
+
}
|
|
3442
|
+
sessionDescription(session) {
|
|
3443
|
+
const lock = this.app.store.getLock(session.session_id);
|
|
3444
|
+
const lockLabel = lock ? `locked ${lock.owner_kind} ${formatAge(lock.heartbeat_at)}` : "unlocked";
|
|
3445
|
+
return `${session.status} · ${lockLabel}`;
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
function parseModeAction(args, actions) {
|
|
3449
|
+
const trimmed = args.trim();
|
|
3450
|
+
if (!trimmed) {
|
|
3451
|
+
return { rest: "" };
|
|
3452
|
+
}
|
|
3453
|
+
const [head = "", ...tail] = trimmed.split(/\s+/);
|
|
3454
|
+
const normalized = head.toLowerCase();
|
|
3455
|
+
if (actions.has(normalized)) {
|
|
3456
|
+
return { action: normalized, rest: tail.join(" ").trim() };
|
|
3457
|
+
}
|
|
3458
|
+
return { rest: trimmed };
|
|
3459
|
+
}
|
|
3460
|
+
function goalPanelUsage(goal) {
|
|
3461
|
+
const parts = [];
|
|
3462
|
+
if (goal.token_budget !== undefined || goal.tokens_used > 0) {
|
|
3463
|
+
parts.push(goal.token_budget === undefined ? `${goal.tokens_used} tokens` : `${goal.tokens_used}/${goal.token_budget} tokens`);
|
|
3464
|
+
}
|
|
3465
|
+
if (goal.tool_rounds_used > 0 || goal.tool_calls_used > 0) {
|
|
3466
|
+
parts.push(`${goal.tool_rounds_used} loops · ${goal.tool_calls_used} tools`);
|
|
3467
|
+
}
|
|
3468
|
+
if (goal.time_used_ms > 0) {
|
|
3469
|
+
parts.push(formatDuration(goal.time_used_ms));
|
|
3470
|
+
}
|
|
3471
|
+
return parts.length ? parts.join(" · ") : undefined;
|
|
3472
|
+
}
|
|
3473
|
+
function goalStepStatusMarker(status) {
|
|
3474
|
+
switch (status) {
|
|
3475
|
+
case "completed":
|
|
3476
|
+
return fg256(48, "x");
|
|
3477
|
+
case "in_progress":
|
|
3478
|
+
return fg256(220, "*");
|
|
3479
|
+
case "blocked":
|
|
3480
|
+
return fg256(203, "!");
|
|
3481
|
+
case "skipped":
|
|
3482
|
+
return fg256(244, "-");
|
|
3483
|
+
default:
|
|
3484
|
+
return fg256(244, " ");
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
function titleFromPromptForMode(prompt) {
|
|
3488
|
+
return prompt.trim().replace(/\s+/g, " ").slice(0, 80) || "New session";
|
|
3489
|
+
}
|
|
3490
|
+
function modelsFromSnapshot(snapshot) {
|
|
3491
|
+
return (snapshot.models ?? [])
|
|
3492
|
+
.map((model) => stringField(model.id) ?? stringField(model.name))
|
|
3493
|
+
.filter((model) => Boolean(model));
|
|
3494
|
+
}
|
|
3495
|
+
function dataAsJsonObjects(value) {
|
|
3496
|
+
return value.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
3497
|
+
}
|
|
3498
|
+
function defaultBaseUrl(provider) {
|
|
3499
|
+
switch (provider) {
|
|
3500
|
+
case "auto":
|
|
3501
|
+
return "http://localhost:8899/v1";
|
|
3502
|
+
case "external":
|
|
3503
|
+
return "https://api.openai.com/v1";
|
|
3504
|
+
case "direct":
|
|
3505
|
+
default:
|
|
3506
|
+
return "http://localhost:8000/v1";
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
export function normalizeContextWindowInput(input, fallback) {
|
|
3510
|
+
const trimmed = input.trim().toLowerCase();
|
|
3511
|
+
const raw = trimmed || String(fallback);
|
|
3512
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)(k|m)?$/);
|
|
3513
|
+
if (!match?.[1]) {
|
|
3514
|
+
throw new Error("Context window must be a token count such as 32768, 128k, or 1m.");
|
|
3515
|
+
}
|
|
3516
|
+
const value = Number(match[1]);
|
|
3517
|
+
const suffix = match[2];
|
|
3518
|
+
const multiplier = suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
|
|
3519
|
+
const tokens = Math.floor(value * multiplier);
|
|
3520
|
+
if (!Number.isFinite(tokens) || tokens < 1024) {
|
|
3521
|
+
throw new Error("Context window must be at least 1024 tokens.");
|
|
3522
|
+
}
|
|
3523
|
+
return tokens;
|
|
3524
|
+
}
|
|
3525
|
+
export function describeModelSetupForDisplay(setup) {
|
|
3526
|
+
const contextWindow = setup.context_window ? ` · ctx ${setup.context_window}` : "";
|
|
3527
|
+
return `${setup.mode} · ${setup.provider ?? setup.router ?? "unknown"} · ${setup.model ?? "unconfigured"} · ${setup.base_url ?? "unconfigured"}${contextWindow}`;
|
|
3528
|
+
}
|
|
3529
|
+
export function endpointStatusLinesForDisplay(snapshot, config, webDescription) {
|
|
3530
|
+
const omni = Object.entries(config.omni.endpoints).map(([name, endpoint]) => ` ${name}: ${endpoint?.base_url && endpoint.model ? `${endpoint.base_url} · ${endpoint.model}` : "unconfigured"}`);
|
|
3531
|
+
return [
|
|
3532
|
+
`${fg256(39, "Mode")} ${snapshot.mode}`,
|
|
3533
|
+
`${fg256(39, "Provider")} ${snapshot.provider_id}`,
|
|
3534
|
+
`${fg256(39, "Base URL")} ${snapshot.base_url ?? "unconfigured"}`,
|
|
3535
|
+
`${fg256(39, "Model")} ${snapshot.model ?? "unconfigured"}`,
|
|
3536
|
+
`${fg256(39, "Web")} ${webDescription}`,
|
|
3537
|
+
"",
|
|
3538
|
+
fg256(39, "Omni endpoints"),
|
|
3539
|
+
...(omni.length ? omni : [" none"]),
|
|
3540
|
+
...(snapshot.errors?.length ? ["", fg256(203, "Errors"), ...snapshot.errors.map((error) => ` ${error}`)] : []),
|
|
3541
|
+
];
|
|
3542
|
+
}
|
|
3543
|
+
export function setupReviewLinesForDisplay(config, contentWidth = setupDialogContentWidth()) {
|
|
3544
|
+
const chat = config.model_setup;
|
|
3545
|
+
const lines = [
|
|
3546
|
+
setupProgress(SETUP_TOTAL_STEPS, SETUP_TOTAL_STEPS, "review"),
|
|
3547
|
+
"",
|
|
3548
|
+
`${fg256(244, "chat")} ${chat.mode}`,
|
|
3549
|
+
];
|
|
3550
|
+
appendReviewField(lines, "provider", chat.provider ?? chat.router ?? "unknown", contentWidth);
|
|
3551
|
+
appendReviewField(lines, "model", chat.model ?? "unconfigured", contentWidth);
|
|
3552
|
+
appendReviewField(lines, "endpoint", chat.base_url ?? "unconfigured", contentWidth);
|
|
3553
|
+
appendReviewField(lines, "context", String(chat.context_window ?? config.context.context_window), contentWidth);
|
|
3554
|
+
lines.push(`${fg256(244, "auth")} ${chat.api_key_ref ? "local vault" : "none"}`, "");
|
|
3555
|
+
const web = config.web_search;
|
|
3556
|
+
lines.push(`${fg256(244, "web")} ${webSearchProviderLabel(web.provider)}`);
|
|
3557
|
+
if (web.base_url) {
|
|
3558
|
+
appendReviewField(lines, "endpoint", web.base_url, contentWidth);
|
|
3559
|
+
}
|
|
3560
|
+
appendReviewField(lines, "mode", webSearchModeSummary(web), contentWidth);
|
|
3561
|
+
lines.push("");
|
|
3562
|
+
const omniEndpoints = Object.entries(config.omni.endpoints).filter(([, endpoint]) => endpoint?.base_url || endpoint?.model);
|
|
3563
|
+
lines.push(`${fg256(244, "omni")} ${config.omni.enabled && omniEndpoints.length ? "enabled" : "disabled"}`);
|
|
3564
|
+
if (!config.omni.enabled || !omniEndpoints.length) {
|
|
3565
|
+
lines.push(" none");
|
|
3566
|
+
}
|
|
3567
|
+
else {
|
|
3568
|
+
for (const [name, endpoint] of omniEndpoints) {
|
|
3569
|
+
lines.push(` ${name}`);
|
|
3570
|
+
appendReviewField(lines, "model", endpoint?.model ?? "unconfigured", contentWidth, 4);
|
|
3571
|
+
appendReviewField(lines, "endpoint", endpoint?.base_url ?? "no url", contentWidth, 4);
|
|
3572
|
+
appendReviewField(lines, "auth", endpoint?.api_key_ref ? "vault auth" : "no auth", contentWidth, 4);
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
lines.push("");
|
|
3576
|
+
appendReviewText(lines, "Config will store endpoints, selected models, and vault references only.", contentWidth, 244);
|
|
3577
|
+
return lines;
|
|
3578
|
+
}
|
|
3579
|
+
export function webSearchProviderSetupOptions() {
|
|
3580
|
+
return [
|
|
3581
|
+
{ value: "auto", label: "Auto chain", description: "Use configured provider when supported, otherwise zero-key HTTP fallback" },
|
|
3582
|
+
{ value: "brave", label: "Brave", description: "Requires Brave Search API key" },
|
|
3583
|
+
{ value: "jina", label: "Jina", description: "Optional Jina API key; public endpoint works as fallback" },
|
|
3584
|
+
{ value: "searxng", label: "SearXNG", description: "Use a SearXNG JSON endpoint" },
|
|
3585
|
+
{ value: "custom", label: "Custom", description: "Use a SearXNG-compatible /search JSON endpoint" },
|
|
3586
|
+
];
|
|
3587
|
+
}
|
|
3588
|
+
function appendReviewField(lines, label, value, contentWidth, indent = 2) {
|
|
3589
|
+
const prefixText = `${" ".repeat(indent)}${label.padEnd(8)} `;
|
|
3590
|
+
const prefix = `${" ".repeat(indent)}${fg256(244, label.padEnd(8))} `;
|
|
3591
|
+
const continuation = " ".repeat(visibleWidth(prefixText));
|
|
3592
|
+
const chunks = wrapPlainText(value, Math.max(8, contentWidth - visibleWidth(prefixText)));
|
|
3593
|
+
lines.push(`${prefix}${chunks[0] ?? ""}`);
|
|
3594
|
+
for (const chunk of chunks.slice(1)) {
|
|
3595
|
+
lines.push(`${continuation}${chunk}`);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
function appendReviewText(lines, text, contentWidth, color) {
|
|
3599
|
+
for (const chunk of wrapPlainText(text, Math.max(8, contentWidth))) {
|
|
3600
|
+
lines.push(color === undefined ? chunk : fg256(color, chunk));
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
function wrapPlainText(text, width) {
|
|
3604
|
+
if (visibleWidth(text) <= width) {
|
|
3605
|
+
return [text];
|
|
3606
|
+
}
|
|
3607
|
+
const lines = [];
|
|
3608
|
+
let rest = text;
|
|
3609
|
+
while (visibleWidth(rest) > width) {
|
|
3610
|
+
const hardEnd = fittingPlainTextEnd(rest, width);
|
|
3611
|
+
const end = preferredPlainTextWrapEnd(rest, hardEnd, width);
|
|
3612
|
+
const slice = rest.slice(0, end).trimEnd();
|
|
3613
|
+
lines.push(slice);
|
|
3614
|
+
rest = rest.slice(end).trimStart();
|
|
3615
|
+
}
|
|
3616
|
+
if (rest) {
|
|
3617
|
+
lines.push(rest);
|
|
3618
|
+
}
|
|
3619
|
+
return lines;
|
|
3620
|
+
}
|
|
3621
|
+
function fittingPlainTextEnd(text, width) {
|
|
3622
|
+
let count = 0;
|
|
3623
|
+
let end = 0;
|
|
3624
|
+
for (const char of text) {
|
|
3625
|
+
const next = count + visibleWidth(char);
|
|
3626
|
+
if (next > width) {
|
|
3627
|
+
break;
|
|
3628
|
+
}
|
|
3629
|
+
count = next;
|
|
3630
|
+
end += char.length;
|
|
3631
|
+
}
|
|
3632
|
+
return Math.max(1, end);
|
|
3633
|
+
}
|
|
3634
|
+
function preferredPlainTextWrapEnd(text, hardEnd, width) {
|
|
3635
|
+
const candidate = text.slice(0, hardEnd);
|
|
3636
|
+
const minWidth = Math.max(8, Math.floor(width * 0.45));
|
|
3637
|
+
let separatorEnd = 0;
|
|
3638
|
+
let whitespaceEnd = 0;
|
|
3639
|
+
for (let index = 0; index < candidate.length;) {
|
|
3640
|
+
const char = [...candidate.slice(index)][0] ?? "";
|
|
3641
|
+
if (!char) {
|
|
3642
|
+
break;
|
|
3643
|
+
}
|
|
3644
|
+
const nextIndex = index + char.length;
|
|
3645
|
+
const beforeWidth = visibleWidth(candidate.slice(0, index));
|
|
3646
|
+
if (beforeWidth >= minWidth && /\s/.test(char)) {
|
|
3647
|
+
whitespaceEnd = index;
|
|
3648
|
+
}
|
|
3649
|
+
if (beforeWidth >= minWidth && /[\/._?&=-]/.test(char)) {
|
|
3650
|
+
separatorEnd = nextIndex;
|
|
3651
|
+
}
|
|
3652
|
+
index = nextIndex;
|
|
3653
|
+
}
|
|
3654
|
+
return whitespaceEnd || separatorEnd || hardEnd;
|
|
3655
|
+
}
|
|
3656
|
+
function webSearchModeSummary(web) {
|
|
3657
|
+
if (web.provider === "auto") {
|
|
3658
|
+
return "fallback ready";
|
|
3659
|
+
}
|
|
3660
|
+
if (web.provider === "off") {
|
|
3661
|
+
return "zero-key fallback";
|
|
3662
|
+
}
|
|
3663
|
+
return web.api_key_ref ? "vault auth" : web.provider === "jina" ? "public/no auth" : "no auth";
|
|
3664
|
+
}
|
|
3665
|
+
function isEnvVarName(value) {
|
|
3666
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
3667
|
+
}
|
|
3668
|
+
function looksLikeApiKey(value) {
|
|
3669
|
+
return /^(sk-|sk_|xai-|ak-|rk-|AIza|ya29\.|ghp_|github_pat_)/i.test(value) || /^bearer\s+/i.test(value);
|
|
3670
|
+
}
|
|
3671
|
+
function isPrintableInput(value) {
|
|
3672
|
+
return printableText(value).length > 0;
|
|
3673
|
+
}
|
|
3674
|
+
function printableText(value) {
|
|
3675
|
+
return [...value].filter((char) => {
|
|
3676
|
+
const code = char.codePointAt(0) ?? 0;
|
|
3677
|
+
return code >= 0x20 && code !== 0x7f && code !== 0x1b;
|
|
3678
|
+
}).join("");
|
|
3679
|
+
}
|
|
3680
|
+
function terminalInputTokens(value, pasteState) {
|
|
3681
|
+
const tokens = [];
|
|
3682
|
+
const shiftEnterSequences = ["\u001b[13;2u", "\u001b[13;2~", "\u001b[27;2;13~"];
|
|
3683
|
+
const knownSequences = [
|
|
3684
|
+
"\u001b[A",
|
|
3685
|
+
"\u001b[B",
|
|
3686
|
+
"\u001b[C",
|
|
3687
|
+
"\u001b[D",
|
|
3688
|
+
"\u001b[H",
|
|
3689
|
+
"\u001b[F",
|
|
3690
|
+
"\u001b[1~",
|
|
3691
|
+
"\u001b[4~",
|
|
3692
|
+
];
|
|
3693
|
+
for (let index = 0; index < value.length;) {
|
|
3694
|
+
if (pasteState?.pending !== undefined) {
|
|
3695
|
+
const end = value.indexOf(BRACKETED_PASTE_END, index);
|
|
3696
|
+
if (end < 0) {
|
|
3697
|
+
pasteState.pending += value.slice(index);
|
|
3698
|
+
break;
|
|
3699
|
+
}
|
|
3700
|
+
tokens.push(pasteToken(pasteState.pending + value.slice(index, end)));
|
|
3701
|
+
pasteState.pending = undefined;
|
|
3702
|
+
index = end + BRACKETED_PASTE_END.length;
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
const rest = value.slice(index);
|
|
3706
|
+
if (rest.startsWith(BRACKETED_PASTE_START)) {
|
|
3707
|
+
const contentStart = index + BRACKETED_PASTE_START.length;
|
|
3708
|
+
const end = value.indexOf(BRACKETED_PASTE_END, contentStart);
|
|
3709
|
+
if (end < 0) {
|
|
3710
|
+
if (pasteState) {
|
|
3711
|
+
pasteState.pending = value.slice(contentStart);
|
|
3712
|
+
}
|
|
3713
|
+
break;
|
|
3714
|
+
}
|
|
3715
|
+
tokens.push(pasteToken(value.slice(contentStart, end)));
|
|
3716
|
+
index = end + BRACKETED_PASTE_END.length;
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
const shiftEnter = shiftEnterSequences.find((sequence) => rest.startsWith(sequence));
|
|
3720
|
+
if (shiftEnter) {
|
|
3721
|
+
tokens.push("shift-enter");
|
|
3722
|
+
index += shiftEnter.length;
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
const knownSequence = knownSequences.find((sequence) => rest.startsWith(sequence));
|
|
3726
|
+
if (knownSequence) {
|
|
3727
|
+
tokens.push(knownSequence);
|
|
3728
|
+
index += knownSequence.length;
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
const char = [...rest][0] ?? "";
|
|
3732
|
+
if (!char) {
|
|
3733
|
+
break;
|
|
3734
|
+
}
|
|
3735
|
+
if (char === "\r" || char === "\n") {
|
|
3736
|
+
tokens.push("\r");
|
|
3737
|
+
}
|
|
3738
|
+
else if (char === "\t") {
|
|
3739
|
+
tokens.push("\t");
|
|
3740
|
+
}
|
|
3741
|
+
else if (char === "\u0003" || char === "\u001b" || char === "\u007f") {
|
|
3742
|
+
tokens.push(char);
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
tokens.push(char);
|
|
3746
|
+
}
|
|
3747
|
+
index += char.length;
|
|
3748
|
+
}
|
|
3749
|
+
return tokens;
|
|
3750
|
+
}
|
|
3751
|
+
function pasteToken(content) {
|
|
3752
|
+
return `${PASTE_TOKEN_PREFIX}${content}`;
|
|
3753
|
+
}
|
|
3754
|
+
function pasteTokenContent(token) {
|
|
3755
|
+
return token.startsWith(PASTE_TOKEN_PREFIX) ? token.slice(PASTE_TOKEN_PREFIX.length) : undefined;
|
|
3756
|
+
}
|
|
3757
|
+
function stripFrontmatter(body) {
|
|
3758
|
+
return body.replace(/^---[\s\S]*?---\s*/, "");
|
|
3759
|
+
}
|
|
3760
|
+
function providerLabel(provider) {
|
|
3761
|
+
switch (provider) {
|
|
3762
|
+
case "auto":
|
|
3763
|
+
return "Semantic Router";
|
|
3764
|
+
case "external":
|
|
3765
|
+
return "External provider";
|
|
3766
|
+
case "direct":
|
|
3767
|
+
default:
|
|
3768
|
+
return "vLLM";
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
function webSearchProviderLabel(provider) {
|
|
3772
|
+
switch (provider) {
|
|
3773
|
+
case "auto":
|
|
3774
|
+
return "Auto chain";
|
|
3775
|
+
case "off":
|
|
3776
|
+
return "Fallback";
|
|
3777
|
+
case "brave":
|
|
3778
|
+
return "Brave";
|
|
3779
|
+
case "jina":
|
|
3780
|
+
return "Jina";
|
|
3781
|
+
case "searxng":
|
|
3782
|
+
return "SearXNG";
|
|
3783
|
+
case "custom":
|
|
3784
|
+
return "Custom search";
|
|
3785
|
+
case "exa":
|
|
3786
|
+
return "Exa";
|
|
3787
|
+
case "perplexity":
|
|
3788
|
+
return "Perplexity";
|
|
3789
|
+
case "kimi":
|
|
3790
|
+
return "Kimi";
|
|
3791
|
+
case "openai":
|
|
3792
|
+
return "OpenAI";
|
|
3793
|
+
case "anthropic":
|
|
3794
|
+
return "Anthropic";
|
|
3795
|
+
case "gemini":
|
|
3796
|
+
return "Gemini";
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
function setupProgress(step, total, label) {
|
|
3800
|
+
const clamped = Math.max(1, Math.min(step, total));
|
|
3801
|
+
const railWidth = 18;
|
|
3802
|
+
const active = Math.max(1, Math.round((clamped / total) * railWidth));
|
|
3803
|
+
const rail = Array.from({ length: railWidth }, (_, index) => fg256(index < active ? 75 : 238, "━")).join("");
|
|
3804
|
+
return `${fg256(244, `setup ${clamped}/${total}`)} ${fg256(252, label)} ${rail}`;
|
|
3805
|
+
}
|
|
3806
|
+
function setupHint(text) {
|
|
3807
|
+
return fg256(244, text);
|
|
3808
|
+
}
|
|
3809
|
+
function renderSetupOptionLine(label, description, active) {
|
|
3810
|
+
const marker = active ? fg256(75, "›") : fg256(238, " ");
|
|
3811
|
+
const name = active ? fg256(252, label) : fg256(248, label);
|
|
3812
|
+
const detail = description ? ` ${fg256(244, description)}` : "";
|
|
3813
|
+
return `${marker} ${name}${detail}`;
|
|
3814
|
+
}
|
|
3815
|
+
function commandScore(name, description, query) {
|
|
3816
|
+
if (!query) {
|
|
3817
|
+
return 0;
|
|
3818
|
+
}
|
|
3819
|
+
const lowerName = name.toLowerCase();
|
|
3820
|
+
const lowerDescription = description.toLowerCase();
|
|
3821
|
+
if (lowerName.startsWith(query)) {
|
|
3822
|
+
return 0;
|
|
3823
|
+
}
|
|
3824
|
+
if (lowerName.includes(query)) {
|
|
3825
|
+
return 1;
|
|
3826
|
+
}
|
|
3827
|
+
if (lowerDescription.includes(query)) {
|
|
3828
|
+
return 2;
|
|
3829
|
+
}
|
|
3830
|
+
return 3;
|
|
3831
|
+
}
|
|
3832
|
+
function stringField(value) {
|
|
3833
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3834
|
+
}
|
|
3835
|
+
function numberField(value) {
|
|
3836
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3837
|
+
}
|
|
3838
|
+
function numericField(value) {
|
|
3839
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3840
|
+
return value;
|
|
3841
|
+
}
|
|
3842
|
+
if (typeof value === "string" && value.trim()) {
|
|
3843
|
+
const parsed = Number(value);
|
|
3844
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
3845
|
+
}
|
|
3846
|
+
return undefined;
|
|
3847
|
+
}
|
|
3848
|
+
function objectField(value) {
|
|
3849
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3850
|
+
}
|
|
3851
|
+
export function cacheEvidenceOverview(evidence, events = []) {
|
|
3852
|
+
let promptTurns = 0;
|
|
3853
|
+
let cacheTurns = 0;
|
|
3854
|
+
let cachePromptTokens = 0;
|
|
3855
|
+
let cachedPromptTokens = 0;
|
|
3856
|
+
for (const item of evidence) {
|
|
3857
|
+
if (cacheTurnKind(events, stringField(item.run_id)) === "warmup") {
|
|
3858
|
+
continue;
|
|
3859
|
+
}
|
|
3860
|
+
const usage = objectField(item.usage);
|
|
3861
|
+
const prompt = numericField(usage.prompt_tokens);
|
|
3862
|
+
const cached = numericField(usage.cached_prompt_tokens);
|
|
3863
|
+
if (prompt !== undefined) {
|
|
3864
|
+
promptTurns += 1;
|
|
3865
|
+
}
|
|
3866
|
+
if (prompt !== undefined && cached !== undefined) {
|
|
3867
|
+
cacheTurns += 1;
|
|
3868
|
+
cachePromptTokens += prompt;
|
|
3869
|
+
cachedPromptTokens += cached;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
const lines = [`${fg256(39, "turns")} ${evidence.length}`];
|
|
3873
|
+
if (cacheTurns > 0 && cachePromptTokens > 0) {
|
|
3874
|
+
const hit = Math.max(0, Math.min(1, cachedPromptTokens / cachePromptTokens));
|
|
3875
|
+
lines.push(`${fg256(39, "usage cache")} cached ${cachedPromptTokens}/${cachePromptTokens} · hit ${(hit * 100).toFixed(1)}% · ${cacheTurns}/${promptTurns} turns exposed`);
|
|
3876
|
+
}
|
|
3877
|
+
else if (evidence.some((item) => cacheTurnKind(events, stringField(item.run_id)) === "warmup")) {
|
|
3878
|
+
lines.push(fg256(244, "usage cache is warming up; no steady-state turns yet"));
|
|
3879
|
+
}
|
|
3880
|
+
else if (promptTurns > 0) {
|
|
3881
|
+
lines.push(fg256(244, "usage cache fields were not exposed by recent responses"));
|
|
3882
|
+
}
|
|
3883
|
+
else {
|
|
3884
|
+
lines.push(fg256(244, "No usage token evidence yet."));
|
|
3885
|
+
}
|
|
3886
|
+
const metrics = cacheMetricLines(evidence);
|
|
3887
|
+
if (metrics.length) {
|
|
3888
|
+
lines.push("", fg256(39, "Endpoint prefix-cache metrics"), ...metrics);
|
|
3889
|
+
}
|
|
3890
|
+
return lines;
|
|
3891
|
+
}
|
|
3892
|
+
function isActivityEvent(event) {
|
|
3893
|
+
return (event.type.includes("evidence") ||
|
|
3894
|
+
event.type === "resource.created" ||
|
|
3895
|
+
event.type === "goal.completion_report" ||
|
|
3896
|
+
event.type === "run.completed" ||
|
|
3897
|
+
event.type === "run.stopped" ||
|
|
3898
|
+
event.type === "run.failed");
|
|
3899
|
+
}
|
|
3900
|
+
function cacheMetricLines(evidence) {
|
|
3901
|
+
const metrics = evidence
|
|
3902
|
+
.slice()
|
|
3903
|
+
.reverse()
|
|
3904
|
+
.map((item) => objectField(item.cache_metrics))
|
|
3905
|
+
.find((item) => Object.keys(item).length > 0);
|
|
3906
|
+
if (!metrics) {
|
|
3907
|
+
return [];
|
|
3908
|
+
}
|
|
3909
|
+
const entries = Object.entries(metrics)
|
|
3910
|
+
.map(([key, value]) => [key, numericField(value)])
|
|
3911
|
+
.filter((entry) => entry[1] !== undefined && /prefix_cache|prompt_tokens_cached|cached_prompt|cache_hit|local_cache/i.test(entry[0]));
|
|
3912
|
+
if (!entries.length) {
|
|
3913
|
+
return [];
|
|
3914
|
+
}
|
|
3915
|
+
const queryTotal = sumMatchingMetrics(entries, /prefix_cache_queries/i);
|
|
3916
|
+
const hitTotal = sumMatchingMetrics(entries, /prefix_cache_hits/i);
|
|
3917
|
+
const lines = [];
|
|
3918
|
+
if (queryTotal && hitTotal !== undefined) {
|
|
3919
|
+
lines.push(` prefix cache hit ${hitTotal}/${queryTotal} · ${((hitTotal / queryTotal) * 100).toFixed(1)}%`);
|
|
3920
|
+
}
|
|
3921
|
+
lines.push(...entries
|
|
3922
|
+
.filter(([key]) => !/prefix_cache_queries|prefix_cache_hits/i.test(key))
|
|
3923
|
+
.slice(0, 6)
|
|
3924
|
+
.map(([key, value]) => ` ${truncateToWidth(key, Math.max(24, terminalWidth() - 20))} ${formatMetricNumber(value)}`));
|
|
3925
|
+
return lines.length ? lines : entries.slice(0, 6).map(([key, value]) => ` ${truncateToWidth(key, Math.max(24, terminalWidth() - 20))} ${formatMetricNumber(value)}`);
|
|
3926
|
+
}
|
|
3927
|
+
function sumMatchingMetrics(entries, pattern) {
|
|
3928
|
+
let total = 0;
|
|
3929
|
+
let matched = false;
|
|
3930
|
+
for (const [key, value] of entries) {
|
|
3931
|
+
if (pattern.test(key)) {
|
|
3932
|
+
total += value;
|
|
3933
|
+
matched = true;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return matched ? total : undefined;
|
|
3937
|
+
}
|
|
3938
|
+
function formatMetricNumber(value) {
|
|
3939
|
+
if (Number.isInteger(value)) {
|
|
3940
|
+
return String(value);
|
|
3941
|
+
}
|
|
3942
|
+
return value.toFixed(3).replace(/\.?0+$/, "");
|
|
3943
|
+
}
|
|
3944
|
+
function isContextCompressionEvent(event) {
|
|
3945
|
+
return event.type === "context.compacted" || event.type === "evidence.context_compression" || event.type.includes("compaction");
|
|
3946
|
+
}
|
|
3947
|
+
function formatTokenPressure(estimatedTokens, thresholdTokens) {
|
|
3948
|
+
if (thresholdTokens <= 0) {
|
|
3949
|
+
return `${estimatedTokens} tokens`;
|
|
3950
|
+
}
|
|
3951
|
+
const pct = Math.round((estimatedTokens / thresholdTokens) * 100);
|
|
3952
|
+
return `${estimatedTokens}/${thresholdTokens} tokens · ${pct}%`;
|
|
3953
|
+
}
|
|
3954
|
+
function formatCompressionStartActivity(event) {
|
|
3955
|
+
return [
|
|
3956
|
+
"Compressing context",
|
|
3957
|
+
compressionReasonLabel(event.reason),
|
|
3958
|
+
formatTokenPressure(event.estimated_tokens, event.threshold_tokens),
|
|
3959
|
+
].filter(Boolean).join(" · ");
|
|
3960
|
+
}
|
|
3961
|
+
function formatCompressionActivityLine(event) {
|
|
3962
|
+
const detail = [
|
|
3963
|
+
compressionReasonLabel(event.reason),
|
|
3964
|
+
`${event.archived_events} archived`,
|
|
3965
|
+
`${event.protected_tail_events} prompts kept`,
|
|
3966
|
+
formatTokenPressure(event.estimated_tokens, event.threshold_tokens),
|
|
3967
|
+
].filter(Boolean).join(" · ");
|
|
3968
|
+
return renderActivityRecordLine({
|
|
3969
|
+
marker: "•",
|
|
3970
|
+
markerColor: 75,
|
|
3971
|
+
action: "Compacted context",
|
|
3972
|
+
actionColor: 75,
|
|
3973
|
+
detail,
|
|
3974
|
+
detailColor: 250,
|
|
3975
|
+
width: terminalWidth(),
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
function compressionReasonLabel(reason) {
|
|
3979
|
+
const normalized = reason.replace(/^post-run:/, "");
|
|
3980
|
+
switch (normalized) {
|
|
3981
|
+
case "threshold":
|
|
3982
|
+
return "token-threshold";
|
|
3983
|
+
case "forced-by-config":
|
|
3984
|
+
return "forced";
|
|
3985
|
+
default:
|
|
3986
|
+
return normalized;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
function titleFromPrompt(prompt) {
|
|
3990
|
+
const firstLine = prompt.trim().split(/\r?\n/)[0] ?? "";
|
|
3991
|
+
return firstLine ? `daemon:${truncateToWidth(firstLine, 48)}` : "daemon task";
|
|
3992
|
+
}
|
|
3993
|
+
function safeTerminalWidth() {
|
|
3994
|
+
return Math.max(20, terminalWidth() - 1);
|
|
3995
|
+
}
|
|
3996
|
+
function safeTerminalHeight() {
|
|
3997
|
+
return Math.max(12, terminalHeight());
|
|
3998
|
+
}
|
|
3999
|
+
function isCancellableJob(job) {
|
|
4000
|
+
return job.status === "queued" || job.status === "running" || job.status === "detached" || job.status === "cancel_requested";
|
|
4001
|
+
}
|
|
4002
|
+
function formatToolActivityLine(toolName, ok, summary, durationMs) {
|
|
4003
|
+
const duration = durationMs >= 1000 ? `${(durationMs / 1000).toFixed(1)}s` : `${durationMs}ms`;
|
|
4004
|
+
const action = toolActivityAction(toolName, ok);
|
|
4005
|
+
return renderActivityRecordLine({
|
|
4006
|
+
marker: ok ? "•" : "×",
|
|
4007
|
+
markerColor: ok ? 48 : 203,
|
|
4008
|
+
action,
|
|
4009
|
+
actionColor: ok ? 75 : 203,
|
|
4010
|
+
detail: compactToolSummary(summary),
|
|
4011
|
+
detailColor: ok ? 250 : 203,
|
|
4012
|
+
suffix: duration,
|
|
4013
|
+
width: terminalWidth(),
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
4016
|
+
function codeIntelligenceActivityLabel(status) {
|
|
4017
|
+
if (status.state === "ready") {
|
|
4018
|
+
return "Context ready";
|
|
4019
|
+
}
|
|
4020
|
+
if (status.state === "degraded") {
|
|
4021
|
+
return `Context degraded${status.error ? ` · ${status.error}` : ""}`;
|
|
4022
|
+
}
|
|
4023
|
+
if (status.state === "syncing") {
|
|
4024
|
+
return `Syncing context ${codeIntelligenceProgress(status)}`;
|
|
4025
|
+
}
|
|
4026
|
+
if (status.state === "off") {
|
|
4027
|
+
return "Context off";
|
|
4028
|
+
}
|
|
4029
|
+
return `Indexing context ${codeIntelligenceProgress(status)}`;
|
|
4030
|
+
}
|
|
4031
|
+
function codeIntelligenceProgress(status) {
|
|
4032
|
+
const phase = status.phase ? `${status.phase} ` : "";
|
|
4033
|
+
if (status.current !== undefined && status.total !== undefined && status.total > 0) {
|
|
4034
|
+
return `${phase}${status.current}/${status.total}`;
|
|
4035
|
+
}
|
|
4036
|
+
if (status.files !== undefined) {
|
|
4037
|
+
return `${phase}${status.files} files`;
|
|
4038
|
+
}
|
|
4039
|
+
return `${phase || status.state}`.trim();
|
|
4040
|
+
}
|
|
4041
|
+
function toolActivityAction(name, ok = true) {
|
|
4042
|
+
const failed = ok ? "" : " failed";
|
|
4043
|
+
switch (name) {
|
|
4044
|
+
case "run_command":
|
|
4045
|
+
return `Ran command${failed}`;
|
|
4046
|
+
case "file_search":
|
|
4047
|
+
return `Searched workspace${failed}`;
|
|
4048
|
+
case "glob":
|
|
4049
|
+
return `Scanned files${failed}`;
|
|
4050
|
+
case "list_dir":
|
|
4051
|
+
return `Listed directory${failed}`;
|
|
4052
|
+
case "read_file":
|
|
4053
|
+
case "read_resource":
|
|
4054
|
+
return `Read file${failed}`;
|
|
4055
|
+
case "codegraph_explore":
|
|
4056
|
+
return `Explored context${failed}`;
|
|
4057
|
+
case "codegraph_search":
|
|
4058
|
+
return `Searched semantic index${failed}`;
|
|
4059
|
+
case "codegraph_node":
|
|
4060
|
+
return `Read indexed symbol${failed}`;
|
|
4061
|
+
case "codegraph_callers":
|
|
4062
|
+
case "codegraph_callees":
|
|
4063
|
+
case "codegraph_impact":
|
|
4064
|
+
return `Traced semantic index${failed}`;
|
|
4065
|
+
case "codegraph_files":
|
|
4066
|
+
case "codegraph_status":
|
|
4067
|
+
return `Checked context engine${failed}`;
|
|
4068
|
+
case "write_file":
|
|
4069
|
+
return `Wrote file${failed}`;
|
|
4070
|
+
case "edit_file":
|
|
4071
|
+
case "ast_edit":
|
|
4072
|
+
return `Edited file${failed}`;
|
|
4073
|
+
case "apply_patch":
|
|
4074
|
+
return `Applied patch${failed}`;
|
|
4075
|
+
case "git_status":
|
|
4076
|
+
return `Checked git status${failed}`;
|
|
4077
|
+
case "git_diff":
|
|
4078
|
+
case "git_show":
|
|
4079
|
+
return `Read git data${failed}`;
|
|
4080
|
+
case "todo_write":
|
|
4081
|
+
return `Updated todo${failed}`;
|
|
4082
|
+
case "goal":
|
|
4083
|
+
return `Updated goal${failed}`;
|
|
4084
|
+
case "plan":
|
|
4085
|
+
return `Updated plan${failed}`;
|
|
4086
|
+
case "complete_step":
|
|
4087
|
+
return `Recorded evidence${failed}`;
|
|
4088
|
+
case "web_search":
|
|
4089
|
+
return `Searched web${failed}`;
|
|
4090
|
+
case "web_fetch":
|
|
4091
|
+
return `Fetched URL${failed}`;
|
|
4092
|
+
case "web_open":
|
|
4093
|
+
return `Opened URL${failed}`;
|
|
4094
|
+
default:
|
|
4095
|
+
if (name.includes("skill")) {
|
|
4096
|
+
return `Updated skills${failed}`;
|
|
4097
|
+
}
|
|
4098
|
+
if (name.includes("image") || name.includes("video") || name.includes("vision") || name.includes("audio")) {
|
|
4099
|
+
return `Used Omni${failed}`;
|
|
4100
|
+
}
|
|
4101
|
+
return `Used tool${failed}`;
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
function compactToolSummary(summary) {
|
|
4105
|
+
return summary
|
|
4106
|
+
.replace(/^Command exited\s+0$/i, "exited 0")
|
|
4107
|
+
.replace(/^Command exited\s+(\d+)$/i, "exited $1")
|
|
4108
|
+
.replace(/\s+/g, " ")
|
|
4109
|
+
.trim();
|
|
4110
|
+
}
|
|
4111
|
+
function formatAge(iso) {
|
|
4112
|
+
const ms = Date.now() - Date.parse(iso);
|
|
4113
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
4114
|
+
return "now";
|
|
4115
|
+
}
|
|
4116
|
+
const seconds = Math.floor(ms / 1000);
|
|
4117
|
+
if (seconds < 60) {
|
|
4118
|
+
return `${seconds}s`;
|
|
4119
|
+
}
|
|
4120
|
+
const minutes = Math.floor(seconds / 60);
|
|
4121
|
+
if (minutes < 60) {
|
|
4122
|
+
return `${minutes}m`;
|
|
4123
|
+
}
|
|
4124
|
+
const hours = Math.floor(minutes / 60);
|
|
4125
|
+
return `${hours}h`;
|
|
4126
|
+
}
|
|
4127
|
+
function checkbox(ok) {
|
|
4128
|
+
return ok ? `${fg256(48, "✓")} ` : `${fg256(203, "□")} `;
|
|
4129
|
+
}
|
|
4130
|
+
function permissionColor(permission) {
|
|
4131
|
+
switch (permission) {
|
|
4132
|
+
case "read":
|
|
4133
|
+
return 75;
|
|
4134
|
+
case "write":
|
|
4135
|
+
return 220;
|
|
4136
|
+
case "shell":
|
|
4137
|
+
return 171;
|
|
4138
|
+
case "network":
|
|
4139
|
+
return 45;
|
|
4140
|
+
case "destructive":
|
|
4141
|
+
return 203;
|
|
4142
|
+
default:
|
|
4143
|
+
return 248;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
function displayToolName(name) {
|
|
4147
|
+
switch (name) {
|
|
4148
|
+
case "codegraph_explore":
|
|
4149
|
+
return "context_explore";
|
|
4150
|
+
case "codegraph_search":
|
|
4151
|
+
return "context_search";
|
|
4152
|
+
case "codegraph_node":
|
|
4153
|
+
return "context_symbol";
|
|
4154
|
+
case "codegraph_callers":
|
|
4155
|
+
return "context_callers";
|
|
4156
|
+
case "codegraph_callees":
|
|
4157
|
+
return "context_callees";
|
|
4158
|
+
case "codegraph_impact":
|
|
4159
|
+
return "context_impact";
|
|
4160
|
+
case "codegraph_files":
|
|
4161
|
+
return "context_files";
|
|
4162
|
+
case "codegraph_status":
|
|
4163
|
+
return "context_status";
|
|
4164
|
+
default:
|
|
4165
|
+
return name;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
function setupDialogTitle(title) {
|
|
4169
|
+
return /setup|provider|model|endpoint|omni|vault|review|web search/i.test(title);
|
|
4170
|
+
}
|
|
4171
|
+
function setupDialogFrameWidth() {
|
|
4172
|
+
return Math.max(52, safeTerminalWidth());
|
|
4173
|
+
}
|
|
4174
|
+
function setupDialogContentWidth() {
|
|
4175
|
+
return Math.max(20, setupDialogFrameWidth() - 3);
|
|
4176
|
+
}
|
|
4177
|
+
export function commandDeckFrame(title, body, width) {
|
|
4178
|
+
const safeWidth = Math.max(52, width);
|
|
4179
|
+
const inner = safeWidth - 1;
|
|
4180
|
+
const rail = fg256(75, "▌");
|
|
4181
|
+
const row = (line = "") => `${rail}${bg256(236, padRight(` ${line}`, inner))}`;
|
|
4182
|
+
return [
|
|
4183
|
+
row(`${fg256(75, "Inferoa")} ${fg256(238, "/")} ${fg256(252, title)}`),
|
|
4184
|
+
row(),
|
|
4185
|
+
...body.map((line) => row(line)),
|
|
4186
|
+
row(),
|
|
4187
|
+
];
|
|
4188
|
+
}
|
|
4189
|
+
function compactWorkspacePath(root) {
|
|
4190
|
+
const home = process.env.HOME;
|
|
4191
|
+
if (home && (root === home || root.startsWith(`${home}${path.sep}`))) {
|
|
4192
|
+
return `~${root.slice(home.length)}`;
|
|
4193
|
+
}
|
|
4194
|
+
return root;
|
|
4195
|
+
}
|
|
4196
|
+
function moveCursorVertical(delta) {
|
|
4197
|
+
if (delta > 0) {
|
|
4198
|
+
stdout.write(`\x1b[${delta}B`);
|
|
4199
|
+
}
|
|
4200
|
+
else if (delta < 0) {
|
|
4201
|
+
stdout.write(`\x1b[${Math.abs(delta)}A`);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
//# sourceMappingURL=app.js.map
|