@codemap-ai/cli 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/.claude-plugin/plugin.json +12 -0
- package/.codex-plugin/plugin.json +35 -0
- package/.cursor-plugin/plugin.json +16 -0
- package/agent-pack/README.md +44 -0
- package/agent-pack/agents/codemap-explorer.md +12 -0
- package/agent-pack/commands/feature-area.md +8 -0
- package/agent-pack/rules/install.md +35 -0
- package/agent-pack/rules/mcp-first.md +18 -0
- package/agent-pack/rules/task-lifecycle.md +11 -0
- package/agent-pack/rules/workflow-skills.md +97 -0
- package/agent-pack/skills/brainstorming/SKILL.md +41 -0
- package/agent-pack/skills/executing-plans/SKILL.md +26 -0
- package/agent-pack/skills/feature-area-investigation/SKILL.md +15 -0
- package/agent-pack/skills/interpreting-codemap-output/SKILL.md +29 -0
- package/agent-pack/skills/mcp-first-exploration/SKILL.md +10 -0
- package/agent-pack/skills/safe-edit-and-reimport/SKILL.md +18 -0
- package/agent-pack/skills/symbol-level-debugging/SKILL.md +15 -0
- package/agent-pack/skills/test-driven-development/SKILL.md +36 -0
- package/agent-pack/skills/token-efficient-code-review/SKILL.md +15 -0
- package/agent-pack/skills/verification-before-completion/SKILL.md +25 -0
- package/agent-pack/skills/writing-plans/SKILL.md +32 -0
- package/agent-pack/templates/AGENTS.md +21 -0
- package/agent-pack/templates/CLAUDE.md +19 -0
- package/agent-pack/templates/COPILOT.md +75 -0
- package/agent-pack/templates/GEMINI.md +77 -0
- package/agent-pack/templates/design-spec.md +34 -0
- package/agent-pack/templates/implementation-plan.md +26 -0
- package/agent-pack/templates/opencode-AGENTS.md +76 -0
- package/agent-pack/templates/opencode-INSTALL.md +5 -0
- package/agent-pack/templates/verification-report.md +18 -0
- package/dist/chat-terminal-DFFYQ6AF.js +1743 -0
- package/dist/chunk-A2QHID6K.js +34 -0
- package/dist/chunk-AXGGL47C.js +457 -0
- package/dist/chunk-FFFJKKKM.js +218262 -0
- package/dist/chunk-KWYP3ADN.js +1742 -0
- package/dist/chunk-WNJNT3FC.js +216 -0
- package/dist/chunk-YRXVMFXS.js +99 -0
- package/dist/index.js +9225 -0
- package/dist/login-screen-2DJBDM47.js +143 -0
- package/dist/pi-tui-app-GVZ3AN42.js +2073 -0
- package/package.json +59 -0
|
@@ -0,0 +1,2073 @@
|
|
|
1
|
+
import{createRequire as __cmr}from"node:module";import{fileURLToPath as __f2p}from"node:url";import{dirname as __dn}from"node:path";var require=__cmr(import.meta.url);var __filename=__f2p(import.meta.url);var __dirname=__dn(__filename);
|
|
2
|
+
import {
|
|
3
|
+
isGatewayOffline
|
|
4
|
+
} from "./chunk-A2QHID6K.js";
|
|
5
|
+
import {
|
|
6
|
+
getCommandList,
|
|
7
|
+
searchIndexedFiles
|
|
8
|
+
} from "./chunk-KWYP3ADN.js";
|
|
9
|
+
import {
|
|
10
|
+
getMastraMessages
|
|
11
|
+
} from "./chunk-FFFJKKKM.js";
|
|
12
|
+
import {
|
|
13
|
+
BG_USER,
|
|
14
|
+
BOLD,
|
|
15
|
+
C_ACTION,
|
|
16
|
+
C_AI,
|
|
17
|
+
C_ARCH,
|
|
18
|
+
C_CYAN,
|
|
19
|
+
C_ERROR,
|
|
20
|
+
C_GRAY,
|
|
21
|
+
C_GREEN,
|
|
22
|
+
C_MUTED,
|
|
23
|
+
C_PURPLE,
|
|
24
|
+
C_RED,
|
|
25
|
+
C_SUCCESS,
|
|
26
|
+
C_WARNING,
|
|
27
|
+
C_WHITE,
|
|
28
|
+
DIM,
|
|
29
|
+
RESET,
|
|
30
|
+
SPINNER,
|
|
31
|
+
formatElapsed,
|
|
32
|
+
formatTime,
|
|
33
|
+
formatTokenCount,
|
|
34
|
+
gradientStr,
|
|
35
|
+
truncate
|
|
36
|
+
} from "./chunk-YRXVMFXS.js";
|
|
37
|
+
import "./chunk-AXGGL47C.js";
|
|
38
|
+
|
|
39
|
+
// src/cli-agent/chat/ui/pi-tui-app.ts
|
|
40
|
+
import {
|
|
41
|
+
Editor,
|
|
42
|
+
Key,
|
|
43
|
+
matchesKey,
|
|
44
|
+
ProcessTerminal,
|
|
45
|
+
TUI
|
|
46
|
+
} from "@earendil-works/pi-tui";
|
|
47
|
+
|
|
48
|
+
// src/cli-agent/chat/ui/pi-tui/message-renderer.ts
|
|
49
|
+
import cfonts from "cfonts";
|
|
50
|
+
import { truncateToWidth, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
|
|
51
|
+
|
|
52
|
+
// src/cli-agent/chat/ui/pi-tui/text.ts
|
|
53
|
+
import { CURSOR_MARKER, visibleWidth } from "@earendil-works/pi-tui";
|
|
54
|
+
import { DiffLineType, DiffParser } from "@git-diff-view/core";
|
|
55
|
+
import { Marked } from "marked";
|
|
56
|
+
import TerminalRenderer from "marked-terminal";
|
|
57
|
+
|
|
58
|
+
// src/cli-agent/chat/ui/pi-tui/shiki-highlight.ts
|
|
59
|
+
var THEME = "dracula";
|
|
60
|
+
var LANGS = [
|
|
61
|
+
"typescript",
|
|
62
|
+
"javascript",
|
|
63
|
+
"dart",
|
|
64
|
+
"php",
|
|
65
|
+
"python",
|
|
66
|
+
"java",
|
|
67
|
+
"kotlin",
|
|
68
|
+
"json",
|
|
69
|
+
"bash",
|
|
70
|
+
"diff",
|
|
71
|
+
"sql",
|
|
72
|
+
"markdown",
|
|
73
|
+
"yaml",
|
|
74
|
+
"toml",
|
|
75
|
+
"html",
|
|
76
|
+
"css",
|
|
77
|
+
"rust",
|
|
78
|
+
"go",
|
|
79
|
+
"ruby",
|
|
80
|
+
"swift",
|
|
81
|
+
"c",
|
|
82
|
+
"cpp",
|
|
83
|
+
"vue"
|
|
84
|
+
];
|
|
85
|
+
var LANG_ALIASES = {
|
|
86
|
+
ts: "typescript",
|
|
87
|
+
tsx: "typescript",
|
|
88
|
+
js: "javascript",
|
|
89
|
+
jsx: "javascript",
|
|
90
|
+
mjs: "javascript",
|
|
91
|
+
cjs: "javascript",
|
|
92
|
+
shell: "bash",
|
|
93
|
+
sh: "bash",
|
|
94
|
+
kt: "kotlin",
|
|
95
|
+
kts: "kotlin",
|
|
96
|
+
py: "python",
|
|
97
|
+
jsonc: "json",
|
|
98
|
+
md: "markdown",
|
|
99
|
+
mdx: "markdown",
|
|
100
|
+
yml: "yaml",
|
|
101
|
+
rs: "rust",
|
|
102
|
+
rb: "ruby",
|
|
103
|
+
cs: "c"
|
|
104
|
+
// C# approximation
|
|
105
|
+
};
|
|
106
|
+
var highlighter = null;
|
|
107
|
+
var initPromise = null;
|
|
108
|
+
async function initShiki() {
|
|
109
|
+
if (highlighter || initPromise) return;
|
|
110
|
+
initPromise = (async () => {
|
|
111
|
+
const { createHighlighter } = await import("shiki");
|
|
112
|
+
highlighter = await createHighlighter({ themes: [THEME], langs: [...LANGS] });
|
|
113
|
+
})();
|
|
114
|
+
return initPromise;
|
|
115
|
+
}
|
|
116
|
+
function hexToAnsi(hex) {
|
|
117
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
118
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
119
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
120
|
+
return `\x1B[38;2;${r};${g};${b}m`;
|
|
121
|
+
}
|
|
122
|
+
function resolveLang(raw) {
|
|
123
|
+
const lower = raw.toLowerCase().trim();
|
|
124
|
+
const resolved = LANG_ALIASES[lower] ?? lower;
|
|
125
|
+
return LANGS.includes(resolved) ? resolved : null;
|
|
126
|
+
}
|
|
127
|
+
function highlightBlock(code, rawLang) {
|
|
128
|
+
const lang = resolveLang(rawLang);
|
|
129
|
+
if (!highlighter || !lang) return code.split("\n");
|
|
130
|
+
try {
|
|
131
|
+
const tokenLines = highlighter.codeToTokensBase(code, { lang, theme: THEME });
|
|
132
|
+
return tokenLines.map((tokens) => {
|
|
133
|
+
if (tokens.length === 0) return "";
|
|
134
|
+
return tokens.map(
|
|
135
|
+
(t) => t.color ? `${hexToAnsi(t.color)}${t.content}${RESET}` : t.content
|
|
136
|
+
).join("") + RESET;
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
return code.split("\n");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isShikiReady() {
|
|
143
|
+
return highlighter !== null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/cli-agent/chat/ui/pi-tui/text.ts
|
|
147
|
+
function workspaceStateCardLines(state, mode, width) {
|
|
148
|
+
const project = state.projectName ?? state.repoName ?? "local workspace";
|
|
149
|
+
const branch = state.branch ?? "unknown branch";
|
|
150
|
+
const indexColor = state.isIndexStale ? C_WARNING : state.indexStatus === "failed" ? C_ERROR : C_SUCCESS;
|
|
151
|
+
const index = `${indexColor}${state.indexStatus}${RESET}${state.indexUpdatedAt ? `${C_GRAY} \xB7 ${state.indexUpdatedAt}${RESET}` : ""}`;
|
|
152
|
+
const changes = state.hasLocalChanges ? `${C_WARNING}${state.changedFilesCount} files${RESET}` : `${C_SUCCESS}none${RESET}`;
|
|
153
|
+
const basis = state.includeDiff ? "indexed + local diff" : "indexed only";
|
|
154
|
+
const lines = [
|
|
155
|
+
`${BOLD}Workspace${RESET} ${C_GRAY}Project:${RESET} ${project} ${C_GRAY}Branch:${RESET} ${branch} ${C_GRAY}Mode:${RESET} ${mode}`,
|
|
156
|
+
`${C_GRAY}Index:${RESET} ${index} ${C_GRAY}Local changes:${RESET} ${changes} ${C_GRAY}Basis:${RESET} ${basis}`
|
|
157
|
+
];
|
|
158
|
+
if (state.isIndexStale) lines.push(`${C_WARNING}Index may be stale. Use current diff or refresh index before relying on graph-only answers.${RESET}`);
|
|
159
|
+
return lines.flatMap((line) => wrapPlain(line, Math.max(20, width)));
|
|
160
|
+
}
|
|
161
|
+
var RENDERER_METHODS = [
|
|
162
|
+
"blockquote",
|
|
163
|
+
"br",
|
|
164
|
+
"code",
|
|
165
|
+
"codespan",
|
|
166
|
+
"del",
|
|
167
|
+
"em",
|
|
168
|
+
"heading",
|
|
169
|
+
"hr",
|
|
170
|
+
"html",
|
|
171
|
+
"image",
|
|
172
|
+
"link",
|
|
173
|
+
"list",
|
|
174
|
+
"listitem",
|
|
175
|
+
"paragraph",
|
|
176
|
+
"strong",
|
|
177
|
+
"table",
|
|
178
|
+
"text"
|
|
179
|
+
];
|
|
180
|
+
var BG_DIFF_DELETE = "\x1B[48;2;69;10;10m";
|
|
181
|
+
var BG_DIFF_ADD = "\x1B[48;2;0;55;18m";
|
|
182
|
+
function stripAnsi(s) {
|
|
183
|
+
return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(CURSOR_MARKER, "");
|
|
184
|
+
}
|
|
185
|
+
function padToWidth(line, width) {
|
|
186
|
+
const pad = Math.max(0, width - visibleWidth(line));
|
|
187
|
+
return line + " ".repeat(pad);
|
|
188
|
+
}
|
|
189
|
+
function truncateVisible(line, width) {
|
|
190
|
+
if (visibleWidth(line) <= width) return line;
|
|
191
|
+
let out = "";
|
|
192
|
+
for (const ch of line) {
|
|
193
|
+
if (visibleWidth(out + ch) > Math.max(0, width - 1)) break;
|
|
194
|
+
out += ch;
|
|
195
|
+
}
|
|
196
|
+
return out + "\u2026";
|
|
197
|
+
}
|
|
198
|
+
function fitLine(line, width) {
|
|
199
|
+
return padToWidth(truncateVisible(line, width), width);
|
|
200
|
+
}
|
|
201
|
+
function wrapPlain(text, width) {
|
|
202
|
+
const max = Math.max(1, width);
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const para of text.split("\n")) {
|
|
205
|
+
let remaining = para;
|
|
206
|
+
if (remaining.length === 0) {
|
|
207
|
+
out.push("");
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
while (visibleWidth(remaining) > max) {
|
|
211
|
+
let vis = 0;
|
|
212
|
+
let i = 0;
|
|
213
|
+
let lastSpaceI = -1;
|
|
214
|
+
while (i < remaining.length) {
|
|
215
|
+
if (remaining.charCodeAt(i) === 27 && remaining[i + 1] === "[") {
|
|
216
|
+
const end = remaining.indexOf("m", i + 2);
|
|
217
|
+
if (end !== -1) {
|
|
218
|
+
i = end + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (remaining[i] === " ") lastSpaceI = i;
|
|
223
|
+
vis++;
|
|
224
|
+
i++;
|
|
225
|
+
if (vis >= max) break;
|
|
226
|
+
}
|
|
227
|
+
const breakAt = lastSpaceI > 0 ? lastSpaceI : i;
|
|
228
|
+
out.push(remaining.slice(0, breakAt).trimEnd());
|
|
229
|
+
remaining = remaining.slice(breakAt).trimStart();
|
|
230
|
+
}
|
|
231
|
+
out.push(remaining);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
var CODE_LANG_ALIASES = {
|
|
236
|
+
bash: "sh",
|
|
237
|
+
shell: "sh",
|
|
238
|
+
javascript: "js",
|
|
239
|
+
jsx: "js",
|
|
240
|
+
jsonc: "json",
|
|
241
|
+
kotlin: "kt",
|
|
242
|
+
typescript: "ts",
|
|
243
|
+
tsx: "ts"
|
|
244
|
+
};
|
|
245
|
+
var KEYWORD_LANGS = /* @__PURE__ */ new Set(["ts", "js", "dart", "java", "kt", "kotlin"]);
|
|
246
|
+
var KEYWORDS = /\b(abstract|as|async|await|break|case|catch|class|const|continue|default|do|else|enum|export|extends|final|finally|for|fun|function|if|implements|import|in|interface|let|new|null|override|package|private|protected|public|return|static|super|switch|this|throw|throws|try|type|val|var|void|when|while|yield)\b/g;
|
|
247
|
+
var STRING_RE = /("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|`([^`\\]|\\.)*`)/g;
|
|
248
|
+
var COMMENT_RE = /(\/\/.*|#.*)$/;
|
|
249
|
+
function normalizeLang(lang) {
|
|
250
|
+
const lower = lang.toLowerCase().trim();
|
|
251
|
+
return CODE_LANG_ALIASES[lower] ?? lower;
|
|
252
|
+
}
|
|
253
|
+
function langFromPath(path) {
|
|
254
|
+
const clean = path.replace(/^[ab]\//, "").split("?")[0] ?? "";
|
|
255
|
+
const ext = clean.split(".").pop()?.toLowerCase() ?? "";
|
|
256
|
+
const byExt = {
|
|
257
|
+
bash: "sh",
|
|
258
|
+
cjs: "js",
|
|
259
|
+
css: "css",
|
|
260
|
+
dart: "dart",
|
|
261
|
+
java: "java",
|
|
262
|
+
js: "js",
|
|
263
|
+
json: "json",
|
|
264
|
+
jsonc: "json",
|
|
265
|
+
jsx: "js",
|
|
266
|
+
kt: "kt",
|
|
267
|
+
kts: "kt",
|
|
268
|
+
md: "markdown",
|
|
269
|
+
mdx: "markdown",
|
|
270
|
+
mjs: "js",
|
|
271
|
+
php: "php",
|
|
272
|
+
py: "python",
|
|
273
|
+
sh: "sh",
|
|
274
|
+
ts: "ts",
|
|
275
|
+
tsx: "ts",
|
|
276
|
+
yaml: "yaml",
|
|
277
|
+
yml: "yaml",
|
|
278
|
+
toml: "toml",
|
|
279
|
+
sql: "sql",
|
|
280
|
+
html: "html",
|
|
281
|
+
xml: "xml",
|
|
282
|
+
rs: "rust",
|
|
283
|
+
go: "go",
|
|
284
|
+
rb: "ruby",
|
|
285
|
+
swift: "swift",
|
|
286
|
+
cs: "csharp",
|
|
287
|
+
cpp: "cpp",
|
|
288
|
+
c: "c",
|
|
289
|
+
vue: "vue"
|
|
290
|
+
};
|
|
291
|
+
return byExt[ext] ?? "";
|
|
292
|
+
}
|
|
293
|
+
function highlightCodeLineFallback(raw, lang) {
|
|
294
|
+
const normalized = normalizeLang(lang);
|
|
295
|
+
if (normalized === "diff") {
|
|
296
|
+
if (raw.startsWith("+") && !raw.startsWith("+++"))
|
|
297
|
+
return `${C_SUCCESS}${raw}${RESET}`;
|
|
298
|
+
if (raw.startsWith("-") && !raw.startsWith("---"))
|
|
299
|
+
return `${C_ERROR}${raw}${RESET}`;
|
|
300
|
+
if (raw.startsWith("@@")) return `${C_ACTION}${raw}${RESET}`;
|
|
301
|
+
if (raw.startsWith("diff ") || raw.startsWith("index ") || raw.startsWith("---") || raw.startsWith("+++")) {
|
|
302
|
+
return `${C_WARNING}${raw}${RESET}`;
|
|
303
|
+
}
|
|
304
|
+
return `${C_MUTED}${raw}${RESET}`;
|
|
305
|
+
}
|
|
306
|
+
if (normalized === "json") {
|
|
307
|
+
return raw.replace(
|
|
308
|
+
/("(?:[^"\\]|\\.)*")(\s*:)?/g,
|
|
309
|
+
(_m, key, colon) => colon ? `${C_ACTION}${key}${RESET}${colon}` : `${C_SUCCESS}${key}${RESET}`
|
|
310
|
+
).replace(/\b(true|false|null)\b/g, `${C_WARNING}$1${RESET}`);
|
|
311
|
+
}
|
|
312
|
+
if (normalized === "sh") {
|
|
313
|
+
const comment = raw.match(COMMENT_RE);
|
|
314
|
+
const body = comment && comment.index !== void 0 ? raw.slice(0, comment.index) : raw;
|
|
315
|
+
const suffix = comment ? `${C_GRAY}${comment[0]}${RESET}` : "";
|
|
316
|
+
return body.replace(
|
|
317
|
+
/\b(cd|cp|echo|export|find|git|grep|ls|mkdir|npm|pnpm|rm|sed|yarn)\b/g,
|
|
318
|
+
`${C_ACTION}$1${RESET}`
|
|
319
|
+
).replace(STRING_RE, `${C_SUCCESS}$1${RESET}`) + suffix;
|
|
320
|
+
}
|
|
321
|
+
if (KEYWORD_LANGS.has(normalized)) {
|
|
322
|
+
const comment = raw.match(COMMENT_RE);
|
|
323
|
+
const body = comment && comment.index !== void 0 ? raw.slice(0, comment.index) : raw;
|
|
324
|
+
const suffix = comment ? `${C_GRAY}${comment[0]}${RESET}` : "";
|
|
325
|
+
return body.replace(STRING_RE, `${C_SUCCESS}$1${RESET}`).replace(KEYWORDS, `${C_ACTION}$1${RESET}`) + suffix;
|
|
326
|
+
}
|
|
327
|
+
return raw;
|
|
328
|
+
}
|
|
329
|
+
function isToken(value) {
|
|
330
|
+
return typeof value === "object" && value !== null;
|
|
331
|
+
}
|
|
332
|
+
function tokenText(value) {
|
|
333
|
+
if (!isToken(value)) return String(value ?? "");
|
|
334
|
+
const text = value.text;
|
|
335
|
+
if (typeof text === "string") return text;
|
|
336
|
+
const raw = value.raw;
|
|
337
|
+
return typeof raw === "string" ? raw : "";
|
|
338
|
+
}
|
|
339
|
+
function stripTrailingBlankLines(lines) {
|
|
340
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
341
|
+
return lines;
|
|
342
|
+
}
|
|
343
|
+
function reapplyBackground(line, bg) {
|
|
344
|
+
return bg + line.replace(/\x1b\[0m/g, `${RESET}${bg}`) + RESET;
|
|
345
|
+
}
|
|
346
|
+
function parseDiffFilePath(header) {
|
|
347
|
+
const plus = header.split("\n").find((line) => line.startsWith("+++ "));
|
|
348
|
+
const minus = header.split("\n").find((line) => line.startsWith("--- "));
|
|
349
|
+
const raw = (plus ?? minus ?? "").replace(/^[+-]{3}\s+/, "").trim();
|
|
350
|
+
if (!raw || raw === "/dev/null") return "";
|
|
351
|
+
return raw.replace(/^[ab]\//, "");
|
|
352
|
+
}
|
|
353
|
+
function langFromHunkHeader(hunkText) {
|
|
354
|
+
const match = hunkText.match(/@@ .+? @@ (.+?)(?::\d+(?:-\d+)?)?$/);
|
|
355
|
+
return match?.[1] ? langFromPath(match[1].trim()) : "";
|
|
356
|
+
}
|
|
357
|
+
function renderUnifiedDiff(source, width, noHighlight) {
|
|
358
|
+
const cleanSource = source.split("\n").filter((l) => !/^\.\.\. \d+ more line\(s\)/.test(l)).join("\n");
|
|
359
|
+
let parsed = null;
|
|
360
|
+
try {
|
|
361
|
+
parsed = new DiffParser().parse(cleanSource);
|
|
362
|
+
} catch {
|
|
363
|
+
parsed = null;
|
|
364
|
+
}
|
|
365
|
+
if (!parsed || parsed.hunks.length === 0) {
|
|
366
|
+
const srcLines = cleanSource.split("\n");
|
|
367
|
+
const hunkIdx = srcLines.findIndex((l) => l.startsWith("@@"));
|
|
368
|
+
if (hunkIdx !== -1) {
|
|
369
|
+
const body = srcLines.slice(hunkIdx + 1);
|
|
370
|
+
const adds = body.filter((l) => l.startsWith("+")).length;
|
|
371
|
+
const dels = body.filter((l) => l.startsWith("-")).length;
|
|
372
|
+
const oldRange = dels > 0 ? `1,${dels}` : "0,0";
|
|
373
|
+
const newRange = adds > 0 ? `1,${adds}` : "0,0";
|
|
374
|
+
const ctx = (srcLines[hunkIdx] ?? "").replace(/^@@ .+? @@/, "");
|
|
375
|
+
const fixedHunk = `@@ -${oldRange} +${newRange} @@${ctx}`;
|
|
376
|
+
const fixedSrc = [...srcLines.slice(0, hunkIdx), fixedHunk, ...body].join("\n");
|
|
377
|
+
try {
|
|
378
|
+
parsed = new DiffParser().parse(fixedSrc);
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!parsed || parsed.hunks.length === 0) return null;
|
|
384
|
+
}
|
|
385
|
+
const filePath = parseDiffFilePath(parsed.header);
|
|
386
|
+
const language = langFromPath(filePath) || langFromHunkHeader(parsed.hunks[0]?.lines[0]?.text ?? "");
|
|
387
|
+
const contentTexts = [];
|
|
388
|
+
for (const hunk of parsed.hunks) {
|
|
389
|
+
for (const line of hunk.lines.slice(1)) {
|
|
390
|
+
contentTexts.push(line.text.replace(/\n$/, ""));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
let preHighlighted;
|
|
394
|
+
if (!noHighlight && isShikiReady() && language) {
|
|
395
|
+
preHighlighted = highlightBlock(contentTexts.join("\n"), language);
|
|
396
|
+
} else if (!noHighlight) {
|
|
397
|
+
preHighlighted = contentTexts.map(
|
|
398
|
+
(t) => highlightCodeLineFallback(t.length === 0 ? " " : t, language)
|
|
399
|
+
);
|
|
400
|
+
} else {
|
|
401
|
+
preHighlighted = contentTexts;
|
|
402
|
+
}
|
|
403
|
+
const hasContextLines = parsed.hunks.some(
|
|
404
|
+
(h) => h.lines.slice(1).some((l) => l.type !== DiffLineType.Add && l.type !== DiffLineType.Delete)
|
|
405
|
+
);
|
|
406
|
+
let maxLineNo = 0;
|
|
407
|
+
if (!hasContextLines) {
|
|
408
|
+
for (const hunk of parsed.hunks)
|
|
409
|
+
for (const line of hunk.lines.slice(1)) {
|
|
410
|
+
const n = (line.type === DiffLineType.Add ? line.newLineNumber : line.oldLineNumber) ?? 0;
|
|
411
|
+
if (n > maxLineNo) maxLineNo = n;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const lineNoW = maxLineNo > 0 ? Math.max(2, String(maxLineNo).length) : 0;
|
|
415
|
+
const gutterWidth = lineNoW > 0 ? 1 + lineNoW + 3 : 2;
|
|
416
|
+
const codeWidth = Math.max(8, width - gutterWidth);
|
|
417
|
+
const out = [];
|
|
418
|
+
if (filePath) out.push(`${C_GRAY}${filePath}${RESET}`);
|
|
419
|
+
let lineIdx = 0;
|
|
420
|
+
for (const hunk of parsed.hunks) {
|
|
421
|
+
out.push(`${C_ACTION}${hunk.lines[0]?.text ?? ""}${RESET}`);
|
|
422
|
+
for (const line of hunk.lines.slice(1)) {
|
|
423
|
+
const isAdd = line.type === DiffLineType.Add;
|
|
424
|
+
const isDelete = line.type === DiffLineType.Delete;
|
|
425
|
+
const marker = isAdd ? "+" : isDelete ? "-" : " ";
|
|
426
|
+
const markerColor = isAdd ? C_SUCCESS : isDelete ? C_ERROR : C_MUTED;
|
|
427
|
+
const highlighted = preHighlighted[lineIdx++] ?? "";
|
|
428
|
+
const wrapped = wrapPlain(highlighted, codeWidth);
|
|
429
|
+
const bg = isAdd ? BG_DIFF_ADD : isDelete ? BG_DIFF_DELETE : "";
|
|
430
|
+
for (const [index, segment] of wrapped.entries()) {
|
|
431
|
+
let gutter;
|
|
432
|
+
if (index === 0 && lineNoW > 0) {
|
|
433
|
+
const lineNo = (isAdd ? line.newLineNumber : line.oldLineNumber) ?? null;
|
|
434
|
+
const num = lineNo != null ? String(lineNo).padStart(lineNoW) : " ".repeat(lineNoW);
|
|
435
|
+
gutter = `${markerColor}${marker}${RESET}${C_GRAY} ${num} \u2502${RESET}${bg} `;
|
|
436
|
+
} else {
|
|
437
|
+
gutter = index === 0 ? `${markerColor}${marker}${RESET} ` : " ".repeat(gutterWidth);
|
|
438
|
+
}
|
|
439
|
+
const rendered = `${gutter}${segment}${RESET}`;
|
|
440
|
+
out.push(
|
|
441
|
+
bg ? reapplyBackground(padToWidth(rendered, width), bg) : rendered
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
var CodeMapTerminalRenderer = class extends TerminalRenderer {
|
|
449
|
+
constructor(width, noHighlight) {
|
|
450
|
+
super({
|
|
451
|
+
reflowText: false,
|
|
452
|
+
showSectionPrefix: false,
|
|
453
|
+
width
|
|
454
|
+
});
|
|
455
|
+
this.width = width;
|
|
456
|
+
this.noHighlight = noHighlight;
|
|
457
|
+
}
|
|
458
|
+
width;
|
|
459
|
+
noHighlight;
|
|
460
|
+
code(code, lang) {
|
|
461
|
+
const source = isToken(code) ? tokenText(code) : String(code ?? "");
|
|
462
|
+
const language = isToken(code) && typeof code.lang === "string" ? code.lang : lang ?? "";
|
|
463
|
+
if (normalizeLang(language) === "diff") {
|
|
464
|
+
const diffLines = renderUnifiedDiff(source, this.width, this.noHighlight);
|
|
465
|
+
if (diffLines) return diffLines.join("\n") + "\n";
|
|
466
|
+
}
|
|
467
|
+
const separator = `${C_MUTED}${"-".repeat(Math.min(this.width, 40))}${RESET}`;
|
|
468
|
+
const useShiki = !this.noHighlight && isShikiReady() && normalizeLang(language) !== "diff";
|
|
469
|
+
const highlighted = useShiki ? highlightBlock(source, language) : source.split("\n").map(
|
|
470
|
+
(line) => this.noHighlight ? line : highlightCodeLineFallback(
|
|
471
|
+
line.length === 0 ? " " : line,
|
|
472
|
+
language
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
const codeWidth = Math.max(8, this.width - 4);
|
|
476
|
+
const body = highlighted.flatMap(
|
|
477
|
+
(line) => wrapPlain(line, codeWidth).map((wrapped) => ` ${wrapped}${RESET}`)
|
|
478
|
+
);
|
|
479
|
+
return [separator, ...body, separator].join("\n") + "\n";
|
|
480
|
+
}
|
|
481
|
+
heading(heading, level) {
|
|
482
|
+
const depth = isToken(heading) && typeof heading.depth === "number" ? heading.depth : level ?? 1;
|
|
483
|
+
const text = this.inlineFrom(heading);
|
|
484
|
+
const color = depth <= 2 ? `${C_ACTION}${BOLD}` : `${C_AI}${BOLD}`;
|
|
485
|
+
const lines = wrapPlain(text, Math.max(8, this.width - 2));
|
|
486
|
+
return lines.map(
|
|
487
|
+
(line, index) => index === 0 ? `${color}${line}${RESET}` : ` ${line}`
|
|
488
|
+
).join("\n") + "\n";
|
|
489
|
+
}
|
|
490
|
+
paragraph(paragraph) {
|
|
491
|
+
const text = this.inlineFrom(paragraph);
|
|
492
|
+
return wrapPlain(text, this.width).join("\n") + "\n";
|
|
493
|
+
}
|
|
494
|
+
list(list, ordered) {
|
|
495
|
+
if (isToken(list) && Array.isArray(list.items)) {
|
|
496
|
+
const isOrdered = Boolean(list.ordered);
|
|
497
|
+
const start = typeof list.start === "number" ? list.start : 1;
|
|
498
|
+
const rendered = list.items.map((item, index) => {
|
|
499
|
+
const marker = isOrdered ? `${start + index}. ` : "- ";
|
|
500
|
+
return this.formatListItem(marker, item);
|
|
501
|
+
});
|
|
502
|
+
return rendered.join("\n") + "\n";
|
|
503
|
+
}
|
|
504
|
+
const body = String(list ?? "").trim();
|
|
505
|
+
if (!body) return "";
|
|
506
|
+
return body.split("\n").map((line, index) => {
|
|
507
|
+
const marker = ordered ? `${index + 1}. ` : "- ";
|
|
508
|
+
return this.formatListLine(marker, line);
|
|
509
|
+
}).join("\n") + "\n";
|
|
510
|
+
}
|
|
511
|
+
listitem(item) {
|
|
512
|
+
return this.formatListItem("- ", item);
|
|
513
|
+
}
|
|
514
|
+
blockquote(quote) {
|
|
515
|
+
const text = isToken(quote) && Array.isArray(quote.tokens) ? this.parser.parse(quote.tokens).trim() : String(quote ?? "").trim();
|
|
516
|
+
if (!text) return "";
|
|
517
|
+
return text.split("\n").flatMap(
|
|
518
|
+
(line) => wrapPlain(line.trimStart(), Math.max(8, this.width - 2)).map(
|
|
519
|
+
(wrapped) => wrapped ? `${C_MUTED}\u2502${RESET} ${C_GRAY}${wrapped}${RESET}` : ""
|
|
520
|
+
)
|
|
521
|
+
).join("\n") + "\n";
|
|
522
|
+
}
|
|
523
|
+
hr() {
|
|
524
|
+
return `${C_MUTED}${"\u2500".repeat(Math.min(this.width, 60))}${RESET}
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
codespan(code) {
|
|
528
|
+
return `${C_ACTION}${tokenText(code)}${RESET}`;
|
|
529
|
+
}
|
|
530
|
+
strong(strong) {
|
|
531
|
+
return `${BOLD}${this.inlineFrom(strong)}${RESET}`;
|
|
532
|
+
}
|
|
533
|
+
em(emphasis) {
|
|
534
|
+
return this.inlineFrom(emphasis);
|
|
535
|
+
}
|
|
536
|
+
del(deleted) {
|
|
537
|
+
return this.inlineFrom(deleted);
|
|
538
|
+
}
|
|
539
|
+
link(hrefOrToken, _title, text) {
|
|
540
|
+
const href = isToken(hrefOrToken) && typeof hrefOrToken.href === "string" ? hrefOrToken.href : String(hrefOrToken ?? "");
|
|
541
|
+
const label = isToken(hrefOrToken) ? this.inlineFrom(hrefOrToken) : text ?? href;
|
|
542
|
+
if (!href || label === href) return `${C_GRAY}${label || href}${RESET}`;
|
|
543
|
+
return `${label} ${C_MUTED}(${href})${RESET}`;
|
|
544
|
+
}
|
|
545
|
+
image(hrefOrToken, title, text) {
|
|
546
|
+
const href = isToken(hrefOrToken) && typeof hrefOrToken.href === "string" ? hrefOrToken.href : String(hrefOrToken ?? "");
|
|
547
|
+
const label = isToken(hrefOrToken) ? tokenText(hrefOrToken) : text ?? "image";
|
|
548
|
+
const suffix = title ? ` - ${title}` : "";
|
|
549
|
+
return `${C_GRAY}![${label}${suffix}]${RESET}${href ? ` ${C_MUTED}(${href})${RESET}` : ""}`;
|
|
550
|
+
}
|
|
551
|
+
text(text) {
|
|
552
|
+
return tokenText(text);
|
|
553
|
+
}
|
|
554
|
+
br() {
|
|
555
|
+
return "\n";
|
|
556
|
+
}
|
|
557
|
+
html() {
|
|
558
|
+
return "";
|
|
559
|
+
}
|
|
560
|
+
table(token) {
|
|
561
|
+
if (!isToken(token)) return "";
|
|
562
|
+
const header = Array.isArray(token.header) ? token.header : [];
|
|
563
|
+
const rows = Array.isArray(token.rows) ? token.rows : [];
|
|
564
|
+
const renderCell = (cell) => {
|
|
565
|
+
if (isToken(cell) && Array.isArray(cell.tokens)) {
|
|
566
|
+
return this.parser.parseInline(cell.tokens);
|
|
567
|
+
}
|
|
568
|
+
return tokenText(cell);
|
|
569
|
+
};
|
|
570
|
+
const headerTexts = header.map(renderCell);
|
|
571
|
+
const bodyTexts = rows.map(
|
|
572
|
+
(row) => row.map(renderCell)
|
|
573
|
+
);
|
|
574
|
+
const colCount = headerTexts.length;
|
|
575
|
+
const colWidths = Array.from({ length: colCount }, (_, i) => {
|
|
576
|
+
const candidates = [
|
|
577
|
+
visibleWidth(stripAnsi(headerTexts[i] ?? "")),
|
|
578
|
+
...bodyTexts.map((row) => visibleWidth(stripAnsi(row[i] ?? "")))
|
|
579
|
+
];
|
|
580
|
+
return Math.max(3, ...candidates);
|
|
581
|
+
});
|
|
582
|
+
const totalNeeded = colWidths.reduce((s, w) => s + w + 3, 1);
|
|
583
|
+
if (totalNeeded > this.width) {
|
|
584
|
+
const available = Math.max(colCount * 3, this.width - colCount * 3 - 1);
|
|
585
|
+
const total = colWidths.reduce((s, w) => s + w, 0);
|
|
586
|
+
for (let i = 0; i < colWidths.length; i++) {
|
|
587
|
+
colWidths[i] = Math.max(
|
|
588
|
+
3,
|
|
589
|
+
Math.floor((colWidths[i] ?? 3) / total * available)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const sep = (l, m, r) => `${C_MUTED}${l}${colWidths.map((w) => "\u2500".repeat(w + 2)).join(m)}${r}${RESET}`;
|
|
594
|
+
const renderRow = (cells, isHeader) => {
|
|
595
|
+
const rendered = cells.map((text, i) => {
|
|
596
|
+
const w = colWidths[i] ?? 3;
|
|
597
|
+
const plain = stripAnsi(text);
|
|
598
|
+
const display = isHeader ? `${BOLD}${plain}${RESET}` : text;
|
|
599
|
+
const pad = Math.max(0, w - visibleWidth(plain));
|
|
600
|
+
return ` ${display}${" ".repeat(pad)} `;
|
|
601
|
+
});
|
|
602
|
+
return `${C_MUTED}\u2502${RESET}${rendered.join(`${C_MUTED}\u2502${RESET}`)}${C_MUTED}\u2502${RESET}`;
|
|
603
|
+
};
|
|
604
|
+
return [
|
|
605
|
+
sep("\u250C", "\u252C", "\u2510"),
|
|
606
|
+
renderRow(headerTexts, true),
|
|
607
|
+
sep("\u251C", "\u253C", "\u2524"),
|
|
608
|
+
...bodyTexts.map((row) => renderRow(row, false)),
|
|
609
|
+
sep("\u2514", "\u2534", "\u2518"),
|
|
610
|
+
""
|
|
611
|
+
].join("\n");
|
|
612
|
+
}
|
|
613
|
+
inlineFrom(value) {
|
|
614
|
+
if (isToken(value) && Array.isArray(value.tokens)) {
|
|
615
|
+
return this.parser.parseInline(value.tokens);
|
|
616
|
+
}
|
|
617
|
+
return tokenText(value);
|
|
618
|
+
}
|
|
619
|
+
formatListItem(marker, item) {
|
|
620
|
+
const checkbox = item.task ? `[${item.checked ? "x" : " "}] ` : "";
|
|
621
|
+
const text = this.listItemText(item);
|
|
622
|
+
return this.formatListLine(marker, `${checkbox}${text}`);
|
|
623
|
+
}
|
|
624
|
+
listItemText(item) {
|
|
625
|
+
if (!Array.isArray(item.tokens)) return tokenText(item);
|
|
626
|
+
const inlineLines = item.tokens.map((token) => {
|
|
627
|
+
if (!isToken(token) || token.type !== "text") return null;
|
|
628
|
+
return Array.isArray(token.tokens) ? this.parser.parseInline(token.tokens) : tokenText(token);
|
|
629
|
+
});
|
|
630
|
+
if (inlineLines.every((line) => line !== null)) {
|
|
631
|
+
return inlineLines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
return this.parser.parse(item.tokens, Boolean(item.loose)).trim();
|
|
634
|
+
}
|
|
635
|
+
formatListLine(marker, text) {
|
|
636
|
+
const markerWidth = visibleWidth(marker);
|
|
637
|
+
const markerColor = marker.trim() === "-" ? `${C_ACTION}-${RESET} ` : `${C_ACTION}${marker}${RESET}`;
|
|
638
|
+
const continuation = " ".repeat(markerWidth);
|
|
639
|
+
const bodyWidth = Math.max(8, this.width - markerWidth);
|
|
640
|
+
const lines = stripTrailingBlankLines(text.split("\n"));
|
|
641
|
+
const out = [];
|
|
642
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
643
|
+
const wrapped = wrapPlain(line, bodyWidth);
|
|
644
|
+
for (const [wrapIndex, wrappedLine] of wrapped.entries()) {
|
|
645
|
+
if (lineIndex === 0 && wrapIndex === 0) {
|
|
646
|
+
out.push(`${markerColor}${wrappedLine}`);
|
|
647
|
+
} else {
|
|
648
|
+
out.push(`${continuation}${wrappedLine}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return out.join("\n");
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
function renderMarkdownish(text, width, options) {
|
|
656
|
+
const renderer = new CodeMapTerminalRenderer(
|
|
657
|
+
width,
|
|
658
|
+
options?.noHighlight ?? false
|
|
659
|
+
);
|
|
660
|
+
const rendererMethods = {};
|
|
661
|
+
for (const method of RENDERER_METHODS) {
|
|
662
|
+
rendererMethods[method] = function(...args) {
|
|
663
|
+
renderer.options = this.options;
|
|
664
|
+
renderer.parser = this.parser;
|
|
665
|
+
const fn = renderer[method];
|
|
666
|
+
return typeof fn === "function" ? fn.apply(renderer, args) : "";
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const parser = new Marked({
|
|
670
|
+
async: false,
|
|
671
|
+
breaks: false,
|
|
672
|
+
gfm: true
|
|
673
|
+
});
|
|
674
|
+
parser.use({ renderer: rendererMethods, useNewRenderer: true });
|
|
675
|
+
const rendered = parser.parse(text);
|
|
676
|
+
const lines = stripTrailingBlankLines(rendered.split("\n"));
|
|
677
|
+
return lines.reduce((acc, line) => {
|
|
678
|
+
if (line === "" && acc[acc.length - 1] === "") return acc;
|
|
679
|
+
acc.push(line);
|
|
680
|
+
return acc;
|
|
681
|
+
}, []);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/cli-agent/core/html-utils.ts
|
|
685
|
+
function normalizeHtml(text) {
|
|
686
|
+
return text.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_match, body) => {
|
|
687
|
+
const rows = [];
|
|
688
|
+
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
689
|
+
let row;
|
|
690
|
+
while ((row = rowRe.exec(body)) !== null) {
|
|
691
|
+
const cells = [];
|
|
692
|
+
const cellRe = /<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi;
|
|
693
|
+
let cell;
|
|
694
|
+
while ((cell = cellRe.exec(row[1] ?? "")) !== null) {
|
|
695
|
+
cells.push((cell[1] ?? "").replace(/<[^>]+>/g, "").trim());
|
|
696
|
+
}
|
|
697
|
+
if (cells.length > 0) rows.push(cells.join(" | "));
|
|
698
|
+
}
|
|
699
|
+
return rows.join("\n");
|
|
700
|
+
}).replace(/<table[^>]*>[\s\S]*/gi, "").replace(/<[^>]+>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/cli-agent/chat/ui/pi-tui/message-renderer.ts
|
|
704
|
+
function formatExpandedContent(content, width, opts = {}) {
|
|
705
|
+
try {
|
|
706
|
+
const parsed = JSON.parse(content);
|
|
707
|
+
if (parsed && typeof parsed === "object") {
|
|
708
|
+
const record = parsed;
|
|
709
|
+
const structured = record.structuredContent && typeof record.structuredContent === "object" && !Array.isArray(record.structuredContent) ? record.structuredContent : record;
|
|
710
|
+
const summary = structured.summary;
|
|
711
|
+
const data = structured.data;
|
|
712
|
+
const error = structured.error;
|
|
713
|
+
const lines = [];
|
|
714
|
+
if (typeof summary === "string" && (data !== void 0 || error !== void 0)) {
|
|
715
|
+
const summaryLines = renderMarkdownish(summary, width);
|
|
716
|
+
lines.push(...summaryLines);
|
|
717
|
+
const raw = data !== void 0 ? data : error;
|
|
718
|
+
const verbosity = getToolResultVerbosity(structured, raw);
|
|
719
|
+
const showRaw = opts.showRawData || verbosity === "verbose" || verbosity === "debug";
|
|
720
|
+
if (showRaw) {
|
|
721
|
+
lines.push("");
|
|
722
|
+
lines.push(`${C_MUTED}Raw data${RESET}`);
|
|
723
|
+
const jsonStr = JSON.stringify(raw, null, 2);
|
|
724
|
+
const highlighted = highlightBlock(jsonStr, "json");
|
|
725
|
+
const codeWidth = Math.max(8, width - 4);
|
|
726
|
+
for (const line of highlighted) {
|
|
727
|
+
lines.push(...wrapPlain(line, codeWidth));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return lines;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
return renderMarkdownish(content, width);
|
|
736
|
+
}
|
|
737
|
+
function getToolResultVerbosity(...values) {
|
|
738
|
+
for (const value of values) {
|
|
739
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
740
|
+
const record = value;
|
|
741
|
+
const direct = record.verbosity;
|
|
742
|
+
if (typeof direct === "string") return direct;
|
|
743
|
+
if (record.verbose === true) return "verbose";
|
|
744
|
+
const meta = record.meta;
|
|
745
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
746
|
+
const metaVerbosity = meta.verbosity;
|
|
747
|
+
if (typeof metaVerbosity === "string") return metaVerbosity;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
function generateBanner() {
|
|
753
|
+
const result = cfonts.render("CODEMAP", {
|
|
754
|
+
font: "simple3d",
|
|
755
|
+
gradient: ["cyan", "magenta"],
|
|
756
|
+
env: "node"
|
|
757
|
+
});
|
|
758
|
+
const raw = result.string ?? "";
|
|
759
|
+
const lines = raw.split("\n");
|
|
760
|
+
let start = 0;
|
|
761
|
+
let end = lines.length - 1;
|
|
762
|
+
while (start <= end && stripAnsi(lines[start] ?? "").trim() === "") start++;
|
|
763
|
+
while (end >= start && stripAnsi(lines[end] ?? "").trim() === "") end--;
|
|
764
|
+
return lines.slice(start, end + 1);
|
|
765
|
+
}
|
|
766
|
+
var BANNER_LINES = generateBanner();
|
|
767
|
+
function renderPreviewLines(preview, bodyW, prefixW) {
|
|
768
|
+
const normalized = preview.replace(/^~~~([^\n]*)/gm, "```$1").replace(/^~~~$/gm, "```");
|
|
769
|
+
const body = normalized.split("\n").filter((line) => !line.startsWith("File:"));
|
|
770
|
+
const added = body.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
771
|
+
const removed = body.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
772
|
+
const sourceLines = body.filter(
|
|
773
|
+
(line) => line.trim() !== "" && !line.startsWith("```") && !line.startsWith("---") && !line.startsWith("+++")
|
|
774
|
+
).length;
|
|
775
|
+
const summary = added > 0 || removed > 0 ? `+${added} -${removed} lines` : `${sourceLines} line${sourceLines === 1 ? "" : "s"}`;
|
|
776
|
+
const previewIndentW = 3;
|
|
777
|
+
const renderW = Math.max(20, bodyW - previewIndentW);
|
|
778
|
+
const rendered = renderMarkdownish(normalized, renderW);
|
|
779
|
+
const indent = " ".repeat(prefixW);
|
|
780
|
+
const fitPreviewLine = (line) => truncateVisible(`${indent}${line}`, prefixW + bodyW);
|
|
781
|
+
const out = [fitPreviewLine(`${C_MUTED}\u23BF ${summary}${RESET}`)];
|
|
782
|
+
out.push(...rendered.map((line) => fitPreviewLine(`${" ".repeat(previewIndentW)}${line}`)));
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
function headerLines(state) {
|
|
786
|
+
const workspace = state.workspace?.repoName ?? state.config.model;
|
|
787
|
+
if (state.messages.length > 0) {
|
|
788
|
+
return [
|
|
789
|
+
`${C_ACTION}${BOLD}codemap${RESET} ${C_GRAY}${workspace}${RESET} ${C_MUTED}|${RESET} ${C_WHITE}${truncate(state.config.model, 28)}${RESET} ${C_MUTED}|${RESET} ${C_ACTION}MCP connected${RESET}`,
|
|
790
|
+
`${C_MUTED}${"-".repeat(72)}${RESET}`
|
|
791
|
+
];
|
|
792
|
+
}
|
|
793
|
+
return [
|
|
794
|
+
"",
|
|
795
|
+
...BANNER_LINES,
|
|
796
|
+
gradientStr(
|
|
797
|
+
" AI-POWERED CODE INTELLIGENCE & AGENT PLATFORM",
|
|
798
|
+
{ r: 88, g: 213, b: 247 },
|
|
799
|
+
{ r: 244, g: 114, b: 182 },
|
|
800
|
+
false
|
|
801
|
+
),
|
|
802
|
+
"",
|
|
803
|
+
[
|
|
804
|
+
`${C_ARCH}o${RESET} ${C_WHITE}v0.1.0${RESET}`,
|
|
805
|
+
`${C_ACTION}@${RESET} ${C_WHITE}${truncate(workspace, 20)}${RESET}`,
|
|
806
|
+
`${C_AI}*${RESET} ${C_WHITE}${truncate(state.config.model, 28)}${RESET}`,
|
|
807
|
+
`${C_ACTION}#${RESET} ${C_ACTION}Connected${RESET}`
|
|
808
|
+
].join(` ${C_MUTED}|${RESET} `).padStart(2),
|
|
809
|
+
` ${C_ACTION}${BOLD}> Quick Start:${RESET} ${C_GRAY}/help for commands . @ for files . Tab for suggestions . Ctrl+C cancel${RESET}`,
|
|
810
|
+
""
|
|
811
|
+
];
|
|
812
|
+
}
|
|
813
|
+
function stripBase64Images(text) {
|
|
814
|
+
return text.replace(
|
|
815
|
+
/!\[([^\]]*)\]\(data:image\/[^;]+;base64,[a-zA-Z0-9+/=\s]+\)/g,
|
|
816
|
+
(_match, alt) => `[image${alt ? `: ${alt}` : ""}]`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
function safeRender(content, width, opts) {
|
|
820
|
+
const cleaned = normalizeHtml(stripBase64Images(content));
|
|
821
|
+
let lines;
|
|
822
|
+
try {
|
|
823
|
+
lines = renderMarkdownish(cleaned, width, opts);
|
|
824
|
+
} catch {
|
|
825
|
+
lines = cleaned.split("\n").flatMap((l) => wrapPlain(l, width));
|
|
826
|
+
}
|
|
827
|
+
return lines.map(
|
|
828
|
+
(line) => visibleWidth2(line) > width ? truncateToWidth(line, width) : line
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
function messageLines(messages, width, frame = 0, opts = {}) {
|
|
832
|
+
return renderMessageLines(messages, width, frame, opts);
|
|
833
|
+
}
|
|
834
|
+
function renderMessageLines(messages, width, frame = 0, opts = {}) {
|
|
835
|
+
if (messages.length === 0) {
|
|
836
|
+
return [
|
|
837
|
+
`${C_ACTION}${BOLD}Welcome to CodeMap Agent${RESET}`,
|
|
838
|
+
`${C_GRAY}Ask a question, mention files with @, or type /help.${RESET}`,
|
|
839
|
+
""
|
|
840
|
+
];
|
|
841
|
+
}
|
|
842
|
+
const out = [];
|
|
843
|
+
for (const msg of messages) {
|
|
844
|
+
const time = `${C_MUTED}${formatTime(msg.timestamp)}${RESET}`;
|
|
845
|
+
if (msg.role === "user") {
|
|
846
|
+
const bg = (raw) => BG_USER + padToWidth(raw, width).replace(/\x1b\[0m/g, `\x1B[0m${BG_USER}`) + RESET;
|
|
847
|
+
const prefixW = 11;
|
|
848
|
+
const bodyW = Math.max(20, width - prefixW - 2);
|
|
849
|
+
const lines = safeRender(msg.content, bodyW);
|
|
850
|
+
out.push(bg(`${time} ${C_ACTION}>${RESET} ${lines[0] ?? ""}`));
|
|
851
|
+
for (const line of lines.slice(1))
|
|
852
|
+
out.push(bg(`${" ".repeat(prefixW)}${line}`));
|
|
853
|
+
} else if (msg.role === "assistant") {
|
|
854
|
+
if (!msg.content?.trim()) continue;
|
|
855
|
+
const prefixW = 9;
|
|
856
|
+
const bodyW = Math.max(20, width - prefixW);
|
|
857
|
+
const lines = safeRender(stripAnsi(msg.content), bodyW);
|
|
858
|
+
out.push(`${time} ${lines[0] ?? ""}`);
|
|
859
|
+
for (const line of lines.slice(1))
|
|
860
|
+
out.push(`${" ".repeat(prefixW)}${line}`);
|
|
861
|
+
} else if (msg.role === "tool_call") {
|
|
862
|
+
const toolName = truncate(msg.name ?? "tool", 20);
|
|
863
|
+
const isPlan = (msg.name ?? "").toLowerCase() === "plan";
|
|
864
|
+
const prefixW = 9;
|
|
865
|
+
const bodyW = Math.max(20, width - prefixW);
|
|
866
|
+
const rawContent = msg.content;
|
|
867
|
+
const hasResult = (msg.toolResults?.length ?? 0) > 0;
|
|
868
|
+
const hasFailed = (msg.toolResults ?? []).some((r) => !r.success);
|
|
869
|
+
const allSucceeded = hasResult && !hasFailed;
|
|
870
|
+
let icon;
|
|
871
|
+
let toolColor;
|
|
872
|
+
if (msg.expanded) {
|
|
873
|
+
icon = "\u2193";
|
|
874
|
+
toolColor = isPlan ? C_AI : C_WARNING;
|
|
875
|
+
} else if (hasFailed) {
|
|
876
|
+
icon = "\u2717";
|
|
877
|
+
toolColor = C_ERROR;
|
|
878
|
+
} else if (allSucceeded) {
|
|
879
|
+
icon = "\u2713";
|
|
880
|
+
toolColor = C_SUCCESS;
|
|
881
|
+
} else {
|
|
882
|
+
icon = SPINNER[frame % SPINNER.length] ?? "\u2026";
|
|
883
|
+
toolColor = isPlan ? C_AI : C_WARNING;
|
|
884
|
+
}
|
|
885
|
+
const canExpand = !hasFailed && (hasResult || !!msg.expandedContent);
|
|
886
|
+
const suffix = canExpand ? " (Ctrl+O to expand)" : "";
|
|
887
|
+
const headerPrefix = `${time} ${toolColor}${icon}${RESET} ${toolColor}${toolName}:${RESET} `;
|
|
888
|
+
const headerSuffix = `${C_MUTED}${suffix}${RESET}`;
|
|
889
|
+
const headerBodyW = Math.max(
|
|
890
|
+
0,
|
|
891
|
+
width - visibleWidth2(headerPrefix) - visibleWidth2(headerSuffix)
|
|
892
|
+
);
|
|
893
|
+
const lines = safeRender(rawContent, Math.max(20, bodyW));
|
|
894
|
+
const headerContent = truncateToWidth(lines[0] ?? "", headerBodyW);
|
|
895
|
+
out.push(`${headerPrefix}${headerContent}${headerSuffix}`);
|
|
896
|
+
if (msg.previewContent)
|
|
897
|
+
out.push(...renderPreviewLines(msg.previewContent, bodyW, prefixW));
|
|
898
|
+
if (msg.expanded && msg.expandedContent) {
|
|
899
|
+
const expandedLines = formatExpandedContent(
|
|
900
|
+
stripAnsi(msg.expandedContent),
|
|
901
|
+
bodyW,
|
|
902
|
+
{ showRawData: opts.showRawToolData }
|
|
903
|
+
);
|
|
904
|
+
out.push(
|
|
905
|
+
`${" ".repeat(prefixW)}${C_MUTED}${"\u2500".repeat(Math.min(bodyW, 60))}${RESET}`
|
|
906
|
+
);
|
|
907
|
+
for (const line of expandedLines)
|
|
908
|
+
out.push(`${" ".repeat(prefixW)}${C_GRAY}${line}${RESET}`);
|
|
909
|
+
out.push(
|
|
910
|
+
`${" ".repeat(prefixW)}${C_MUTED}${"\u2500".repeat(Math.min(bodyW, 60))}${RESET}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
} else if (msg.role === "system") {
|
|
914
|
+
const prefixW = 17;
|
|
915
|
+
const bodyW = Math.max(20, width - prefixW);
|
|
916
|
+
const hasAnsi = /\x1b\[/.test(msg.content);
|
|
917
|
+
const lines = hasAnsi ? msg.content.split("\n").flatMap((l) => wrapPlain(l, bodyW)) : safeRender(msg.content, bodyW);
|
|
918
|
+
out.push(`${time} ${C_MUTED}system:${RESET} ${lines[0] ?? ""}`);
|
|
919
|
+
for (const line of lines.slice(1))
|
|
920
|
+
out.push(`${" ".repeat(prefixW)}${line}`);
|
|
921
|
+
} else {
|
|
922
|
+
out.push(...safeRender(msg.content, width));
|
|
923
|
+
}
|
|
924
|
+
out.push("");
|
|
925
|
+
}
|
|
926
|
+
return out.map(
|
|
927
|
+
(line) => visibleWidth2(line) > width ? truncateToWidth(line, width) : line
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/cli-agent/chat/ui/pi-tui/input.ts
|
|
932
|
+
var ModelPickerProvider = class {
|
|
933
|
+
constructor(getModels, getCurrentModel, onSelect, onClose) {
|
|
934
|
+
this.getModels = getModels;
|
|
935
|
+
this.getCurrentModel = getCurrentModel;
|
|
936
|
+
this.onSelect = onSelect;
|
|
937
|
+
this.onClose = onClose;
|
|
938
|
+
}
|
|
939
|
+
getModels;
|
|
940
|
+
getCurrentModel;
|
|
941
|
+
onSelect;
|
|
942
|
+
onClose;
|
|
943
|
+
async getSuggestions(lines, cursorLine, cursorCol, _options) {
|
|
944
|
+
const current = this.getCurrentModel();
|
|
945
|
+
const available = this.getModels();
|
|
946
|
+
if (available.length === 0) return null;
|
|
947
|
+
const query = (lines[cursorLine] ?? "").slice(0, cursorCol).toLowerCase();
|
|
948
|
+
const filtered = query ? available.filter((m) => m.id.toLowerCase().includes(query)) : available;
|
|
949
|
+
if (filtered.length === 0) {
|
|
950
|
+
return { prefix: query, items: [{ value: "", label: "No matches", description: "" }] };
|
|
951
|
+
}
|
|
952
|
+
const items = filtered.map((m) => ({
|
|
953
|
+
value: m.id,
|
|
954
|
+
label: m.id,
|
|
955
|
+
description: m.id === current ? "\u2713 active" : m.ownedBy ?? ""
|
|
956
|
+
}));
|
|
957
|
+
return { prefix: query, items };
|
|
958
|
+
}
|
|
959
|
+
applyCompletion(lines, cursorLine, _cursorCol, item, _prefix) {
|
|
960
|
+
if (!item.value) {
|
|
961
|
+
return { lines: [...lines], cursorLine, cursorCol: 0 };
|
|
962
|
+
}
|
|
963
|
+
this.onSelect(item.value);
|
|
964
|
+
setTimeout(() => this.onClose(), 0);
|
|
965
|
+
const newLines = [...lines];
|
|
966
|
+
newLines[cursorLine] = "";
|
|
967
|
+
return { lines: newLines, cursorLine, cursorCol: 0 };
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
var AT_DELIMITERS = /* @__PURE__ */ new Set([" ", " ", '"', "'", "="]);
|
|
971
|
+
function extractAtPrefix(textBeforeCursor) {
|
|
972
|
+
let lastDelimIdx = -1;
|
|
973
|
+
for (let i = textBeforeCursor.length - 1; i >= 0; i--) {
|
|
974
|
+
if (AT_DELIMITERS.has(textBeforeCursor[i] ?? "")) {
|
|
975
|
+
lastDelimIdx = i;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const tokenStart = lastDelimIdx === -1 ? 0 : lastDelimIdx + 1;
|
|
980
|
+
if (textBeforeCursor[tokenStart] === "@") {
|
|
981
|
+
return textBeforeCursor.slice(tokenStart);
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
var MentionAutocompleteProvider = class {
|
|
986
|
+
commands;
|
|
987
|
+
constructor(commandsOrOptions = []) {
|
|
988
|
+
if (Array.isArray(commandsOrOptions)) {
|
|
989
|
+
this.commands = commandsOrOptions;
|
|
990
|
+
} else {
|
|
991
|
+
this.commands = commandsOrOptions.commands ?? [];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
995
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
996
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
997
|
+
const atPrefix = extractAtPrefix(textBeforeCursor);
|
|
998
|
+
if (atPrefix !== null) {
|
|
999
|
+
const query = atPrefix.slice(1);
|
|
1000
|
+
const files = await searchIndexedFiles(query);
|
|
1001
|
+
if (options.signal.aborted) return null;
|
|
1002
|
+
if (files.length === 0) return null;
|
|
1003
|
+
return {
|
|
1004
|
+
prefix: atPrefix,
|
|
1005
|
+
items: files.map((f) => ({
|
|
1006
|
+
value: f.path,
|
|
1007
|
+
label: f.label ?? f.path
|
|
1008
|
+
}))
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (!options.force && textBeforeCursor.trimStart().startsWith("/")) {
|
|
1012
|
+
const spaceIdx = textBeforeCursor.indexOf(" ");
|
|
1013
|
+
if (spaceIdx === -1) {
|
|
1014
|
+
const typed = textBeforeCursor.trimStart();
|
|
1015
|
+
const filtered = this.commands.filter((c) => c.value.startsWith(typed));
|
|
1016
|
+
if (filtered.length === 0) return null;
|
|
1017
|
+
return {
|
|
1018
|
+
prefix: typed,
|
|
1019
|
+
items: filtered.map((c) => ({
|
|
1020
|
+
value: c.value,
|
|
1021
|
+
label: c.value,
|
|
1022
|
+
description: c.description
|
|
1023
|
+
}))
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
1030
|
+
const line = lines[cursorLine] ?? "";
|
|
1031
|
+
const before = line.slice(0, cursorCol - prefix.length);
|
|
1032
|
+
const after = line.slice(cursorCol);
|
|
1033
|
+
const replacement = prefix.startsWith("@") ? `@${item.value} ` : `${item.value} `;
|
|
1034
|
+
const newLine = before + replacement + after;
|
|
1035
|
+
const newLines = [...lines];
|
|
1036
|
+
newLines[cursorLine] = newLine;
|
|
1037
|
+
return { lines: newLines, cursorLine, cursorCol: before.length + replacement.length };
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/cli-agent/chat/ui/pi-tui/image-paste.ts
|
|
1042
|
+
import { readFile, stat } from "fs/promises";
|
|
1043
|
+
import { execFile } from "child_process";
|
|
1044
|
+
import { fileURLToPath } from "url";
|
|
1045
|
+
import { basename, extname, isAbsolute, resolve } from "path";
|
|
1046
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
1047
|
+
var MIME_BY_EXT = {
|
|
1048
|
+
".gif": "image/gif",
|
|
1049
|
+
".heic": "image/heic",
|
|
1050
|
+
".jpeg": "image/jpeg",
|
|
1051
|
+
".jpg": "image/jpeg",
|
|
1052
|
+
".png": "image/png",
|
|
1053
|
+
".webp": "image/webp"
|
|
1054
|
+
};
|
|
1055
|
+
async function readClipboardImage() {
|
|
1056
|
+
if (process.platform !== "darwin") return null;
|
|
1057
|
+
return new Promise((resolve2) => {
|
|
1058
|
+
execFile(
|
|
1059
|
+
"osascript",
|
|
1060
|
+
["-e", 'set imgData to the clipboard as \xABclass PNGf\xBB\nset out to do shell script "xxd -p" with input imgData\nreturn out'],
|
|
1061
|
+
{ maxBuffer: MAX_IMAGE_BYTES * 2 },
|
|
1062
|
+
(err, stdout) => {
|
|
1063
|
+
if (err || !stdout.trim()) {
|
|
1064
|
+
resolve2(null);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const hex = stdout.trim().replace(/\s+/g, "");
|
|
1069
|
+
const buf = Buffer.from(hex, "hex");
|
|
1070
|
+
resolve2(buf.toString("base64"));
|
|
1071
|
+
} catch {
|
|
1072
|
+
resolve2(null);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async function imageFromPaste(data) {
|
|
1079
|
+
const bracketedContent = data.match(/^\x1b\[200~([\s\S]*?)\x1b\[201~$/);
|
|
1080
|
+
const trimmed = (bracketedContent ? bracketedContent[1] : data).trim();
|
|
1081
|
+
if (!trimmed) return null;
|
|
1082
|
+
const dataUrl = trimmed.match(/^data:(image\/(?:png|jpe?g|webp|gif));base64,([a-z0-9+/=\s]+)$/i);
|
|
1083
|
+
if (dataUrl) {
|
|
1084
|
+
const mime2 = dataUrl[1].toLowerCase();
|
|
1085
|
+
const base64 = dataUrl[2].replace(/\s+/g, "");
|
|
1086
|
+
const approxBytes = Math.floor(base64.length * 3 / 4);
|
|
1087
|
+
if (approxBytes > MAX_IMAGE_BYTES) {
|
|
1088
|
+
throw new Error("Image paste is too large. Max size is 5 MB.");
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
marker: `[image: pasted ${mime2.split("/")[1]}]`,
|
|
1092
|
+
data: base64,
|
|
1093
|
+
mimeType: mime2
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const pathText = extractSinglePath(trimmed);
|
|
1097
|
+
if (!pathText) return null;
|
|
1098
|
+
const ext = extname(pathText).toLowerCase();
|
|
1099
|
+
const mime = MIME_BY_EXT[ext];
|
|
1100
|
+
if (!mime) return null;
|
|
1101
|
+
const candidates = [pathText, tryUrlDecode(pathText)].filter(
|
|
1102
|
+
(p, i, arr) => p !== null && arr.indexOf(p) === i
|
|
1103
|
+
);
|
|
1104
|
+
for (const candidate of candidates) {
|
|
1105
|
+
const filePath = isAbsolute(candidate) ? candidate : resolve(process.cwd(), candidate);
|
|
1106
|
+
try {
|
|
1107
|
+
const info = await stat(filePath);
|
|
1108
|
+
if (!info.isFile()) continue;
|
|
1109
|
+
if (info.size > MAX_IMAGE_BYTES) {
|
|
1110
|
+
throw new Error(`Image is too large (${Math.ceil(info.size / 1024 / 1024)} MB). Max size is 5 MB.`);
|
|
1111
|
+
}
|
|
1112
|
+
const bytes = await readFile(filePath);
|
|
1113
|
+
const base64 = bytes.toString("base64");
|
|
1114
|
+
const name = basename(filePath);
|
|
1115
|
+
return {
|
|
1116
|
+
marker: `[image: ${name}]`,
|
|
1117
|
+
data: base64,
|
|
1118
|
+
mimeType: mime
|
|
1119
|
+
};
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
if (err.code !== "ENOENT") throw err;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (ext === ".png" || mime === "image/png") {
|
|
1125
|
+
const clipBase64 = await readClipboardImage();
|
|
1126
|
+
if (clipBase64) {
|
|
1127
|
+
const name = basename(pathText);
|
|
1128
|
+
return {
|
|
1129
|
+
marker: `[image: ${name}]`,
|
|
1130
|
+
data: clipBase64,
|
|
1131
|
+
mimeType: "image/png"
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
throw new Error(`Image not found: ${basename(pathText)}`);
|
|
1136
|
+
}
|
|
1137
|
+
function extractSinglePath(value) {
|
|
1138
|
+
const unquoted = value.replace(/^['"]|['"]$/g, "");
|
|
1139
|
+
if (unquoted.startsWith("file://")) {
|
|
1140
|
+
try {
|
|
1141
|
+
return fileURLToPath(unquoted);
|
|
1142
|
+
} catch {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (/[\r\n]/.test(unquoted)) return null;
|
|
1147
|
+
const unescaped = unquoted.replace(/\\(.)/g, "$1");
|
|
1148
|
+
if (isAbsolute(unescaped) || unescaped.startsWith("./") || unescaped.startsWith("../")) {
|
|
1149
|
+
return unescaped;
|
|
1150
|
+
}
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
function tryUrlDecode(p) {
|
|
1154
|
+
try {
|
|
1155
|
+
const decoded = decodeURIComponent(p);
|
|
1156
|
+
return decoded !== p ? decoded : null;
|
|
1157
|
+
} catch {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/cli-agent/chat/ui/pi-tui/editor-renderer.ts
|
|
1163
|
+
import { CURSOR_MARKER as CURSOR_MARKER2 } from "@earendil-works/pi-tui";
|
|
1164
|
+
var PROMPT_W = 2;
|
|
1165
|
+
function stripFirstVisibleChar(line) {
|
|
1166
|
+
let i = 0;
|
|
1167
|
+
while (i < line.length) {
|
|
1168
|
+
if (line.charCodeAt(i) === 27 && line[i + 1] === "[") {
|
|
1169
|
+
const end = line.indexOf("m", i + 2);
|
|
1170
|
+
if (end !== -1) {
|
|
1171
|
+
i = end + 1;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (line.charCodeAt(i) === 27 && line[i + 1] === "_") {
|
|
1176
|
+
const end = line.indexOf("\x07", i + 2);
|
|
1177
|
+
if (end !== -1) {
|
|
1178
|
+
i = end + 1;
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return line.slice(0, i) + line.slice(i + 1);
|
|
1183
|
+
}
|
|
1184
|
+
return line;
|
|
1185
|
+
}
|
|
1186
|
+
function renderEditor(editor, w, shellMode, debugMode) {
|
|
1187
|
+
const promptFirst = shellMode ? `${C_SUCCESS}!${RESET} ` : debugMode ? `${C_ERROR}>${RESET} ` : `${C_ACTION}>${RESET} `;
|
|
1188
|
+
const promptCont = " ".repeat(PROMPT_W);
|
|
1189
|
+
return editor.render(Math.max(8, w - PROMPT_W)).map((l) => {
|
|
1190
|
+
const hasCursor = l.indexOf(CURSOR_MARKER2) >= 0;
|
|
1191
|
+
if (hasCursor) {
|
|
1192
|
+
const raw = shellMode ? stripFirstVisibleChar(l) : l;
|
|
1193
|
+
return promptFirst + raw;
|
|
1194
|
+
}
|
|
1195
|
+
return promptCont + l;
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/cli-agent/chat/ui/pi-tui/panel-builder.ts
|
|
1200
|
+
function commitsDiffer(localCommit, cloudCommit) {
|
|
1201
|
+
if (!localCommit || !cloudCommit) return false;
|
|
1202
|
+
const local = localCommit.trim();
|
|
1203
|
+
const cloud = cloudCommit.trim();
|
|
1204
|
+
if (!local || !cloud) return false;
|
|
1205
|
+
return !(local.startsWith(cloud) || cloud.startsWith(local));
|
|
1206
|
+
}
|
|
1207
|
+
function isActiveTaskPhase(phase) {
|
|
1208
|
+
return phase === "thinking" || phase === "tool" || phase === "streaming" || phase === "classifying" || phase === "planning" || phase === "executing" || phase === "reviewing";
|
|
1209
|
+
}
|
|
1210
|
+
function buildStatusBar(state, w, debugMode, statusMessage = "") {
|
|
1211
|
+
const workspace = state.workspace?.repoName ?? state.config.model;
|
|
1212
|
+
const branch = state.workspace?.branch ? `/${state.workspace.branch}` : "";
|
|
1213
|
+
const gatewayTag = isGatewayOffline() ? ` ${C_MUTED}\xB7 \u2717 models.dev${RESET}` : "";
|
|
1214
|
+
const right = statusMessage ? `${C_WARNING}${statusMessage}${RESET}` : state.planMode ? `${C_AI}\u25C8 PLAN MODE${RESET}${C_MUTED} \xB7 /plan to exit${RESET}` : debugMode ? `${C_ERROR}\u23FA DEBUG${RESET}${C_MUTED} \xB7 /debug to stop${RESET}` : `${C_ACTION}MCP connected${RESET}${gatewayTag}`;
|
|
1215
|
+
const reimportHint = commitsDiffer(
|
|
1216
|
+
state.workspace?.localCommit,
|
|
1217
|
+
state.workspace?.cloudCommit
|
|
1218
|
+
) ? `${C_WARNING}\u26A0 reimport recommended${RESET}` : "";
|
|
1219
|
+
const left = `${C_WHITE}${workspace}${RESET}${C_MUTED}${branch} \xB7 ${RESET}${C_WHITE}${truncate(state.config.model, 28)}${RESET}${C_MUTED} \xB7 ${RESET}` + right;
|
|
1220
|
+
if (!reimportHint) return fitLine(left, w);
|
|
1221
|
+
const hintWidth = stripAnsi(reimportHint).length;
|
|
1222
|
+
const gap = " ";
|
|
1223
|
+
const maxLeftWidth = Math.max(0, w - hintWidth - gap.length);
|
|
1224
|
+
const clippedLeft = truncateVisible(left, maxLeftWidth);
|
|
1225
|
+
const paddingWidth = Math.max(
|
|
1226
|
+
0,
|
|
1227
|
+
w - stripAnsi(clippedLeft).length - hintWidth
|
|
1228
|
+
);
|
|
1229
|
+
return fitLine(clippedLeft + " ".repeat(paddingWidth) + reimportHint, w);
|
|
1230
|
+
}
|
|
1231
|
+
function formatTaskStatusIcon(status) {
|
|
1232
|
+
switch (status) {
|
|
1233
|
+
case "completed":
|
|
1234
|
+
return `${C_SUCCESS}\u2713${RESET}`;
|
|
1235
|
+
case "in_progress":
|
|
1236
|
+
return `${C_AI}\u25CF${RESET}`;
|
|
1237
|
+
default:
|
|
1238
|
+
return `${C_MUTED}\u25CB${RESET}`;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function renderTaskList(tasks, w) {
|
|
1242
|
+
const lines = [];
|
|
1243
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
1244
|
+
const total = tasks.length;
|
|
1245
|
+
const header = ` ${C_WHITE}${BOLD}Tasks${RESET} ${C_MUTED}(${completed}/${total})${RESET}`;
|
|
1246
|
+
lines.push(fitLine(header, w));
|
|
1247
|
+
for (const task of tasks) {
|
|
1248
|
+
const icon = formatTaskStatusIcon(task.status);
|
|
1249
|
+
const text = task.status === "completed" ? `${C_MUTED}${task.content}${RESET}` : task.status === "in_progress" ? `${C_WHITE}${BOLD}${task.content}${RESET}` : `${C_WHITE}${task.content}${RESET}`;
|
|
1250
|
+
const activeForm = task.status === "in_progress" && task.activeForm ? ` ${C_MUTED}${task.activeForm}${RESET}` : "";
|
|
1251
|
+
lines.push(fitLine(` ${icon} ${text}${activeForm}`, w));
|
|
1252
|
+
}
|
|
1253
|
+
return lines;
|
|
1254
|
+
}
|
|
1255
|
+
function buildPanel(state, w, ctx) {
|
|
1256
|
+
const { editor, frame, shellMode, debugMode, statusMessage, modelPickerActive } = ctx;
|
|
1257
|
+
const out = [];
|
|
1258
|
+
const sep = `${DIM}${C_MUTED}${"\u2500".repeat(Math.min(w - 4, 40))}${RESET}`;
|
|
1259
|
+
if (state.screen === "help") {
|
|
1260
|
+
const cmds = getCommandList();
|
|
1261
|
+
const half = Math.ceil(cmds.length / 2);
|
|
1262
|
+
const colW = Math.max(24, Math.floor((w - 4) / 2));
|
|
1263
|
+
const NAME_W = 13;
|
|
1264
|
+
const descW = Math.max(4, colW - NAME_W - 1);
|
|
1265
|
+
const fmtCol = (c) => {
|
|
1266
|
+
if (!c) return " ".repeat(colW);
|
|
1267
|
+
const namePart = `${C_ACTION}/${c.name.padEnd(NAME_W - 1)}${RESET} `;
|
|
1268
|
+
const descPart = `${C_GRAY}${truncate(c.description, descW)}${RESET}`;
|
|
1269
|
+
return padToWidth(namePart + descPart, colW);
|
|
1270
|
+
};
|
|
1271
|
+
out.push(fitLine(`${C_ACTION}${BOLD} Commands${RESET}`, w));
|
|
1272
|
+
for (let i = 0; i < half; i++) {
|
|
1273
|
+
out.push(fitLine(` ${fmtCol(cmds[i])} ${fmtCol(cmds[i + half])}`, w));
|
|
1274
|
+
}
|
|
1275
|
+
out.push(
|
|
1276
|
+
fitLine("", w),
|
|
1277
|
+
fitLine(
|
|
1278
|
+
` ${C_ACTION}@${RESET} ${C_GRAY}mention file${RESET} ${C_ACTION}!<cmd>${RESET} ${C_GRAY}shell${RESET} ${C_ACTION}PgUp/Dn${RESET} ${C_GRAY}scroll${RESET} ${C_ACTION}Ctrl+T${RESET} ${C_GRAY}tasks${RESET}`,
|
|
1279
|
+
w
|
|
1280
|
+
),
|
|
1281
|
+
fitLine(` ${C_ACTION}Esc${RESET} ${C_GRAY}to close${RESET}`, w),
|
|
1282
|
+
fitLine("", w)
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
if (state.subprocess.active) {
|
|
1286
|
+
out.push(
|
|
1287
|
+
fitLine(`${C_WARNING}Running:${RESET} ${state.subprocess.command}`, w)
|
|
1288
|
+
);
|
|
1289
|
+
for (const l of state.subprocess.logLines.slice(-4)) {
|
|
1290
|
+
out.push(fitLine(`${C_GRAY}${l}${RESET}`, w));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (state.task.phase !== "idle") {
|
|
1294
|
+
const active = isActiveTaskPhase(state.task.phase);
|
|
1295
|
+
const endTime = active ? Date.now() : state.task.endTime ?? Date.now();
|
|
1296
|
+
const elapsed = state.task.startTime ? formatElapsed(Math.max(0, endTime - state.task.startTime)) : "0s";
|
|
1297
|
+
const promptTok = state.task.usage?.promptTokens ?? 0;
|
|
1298
|
+
const completionTok = state.task.usage?.completionTokens ?? 0;
|
|
1299
|
+
const usage = promptTok > 0 || completionTok > 0 ? ` \xB7 ${[
|
|
1300
|
+
promptTok > 0 ? `\u2191${formatTokenCount(promptTok)}` : "",
|
|
1301
|
+
completionTok > 0 ? `\u2193${formatTokenCount(completionTok)}` : ""
|
|
1302
|
+
].filter(Boolean).join(" ")} tok` : "";
|
|
1303
|
+
const tool = state.task.phase === "tool" && state.task.toolName ? ` \xB7 ${state.task.toolName}` : "";
|
|
1304
|
+
const displayModel = state.task.model ?? "";
|
|
1305
|
+
const model = displayModel ? ` ${C_GRAY}${truncate(displayModel, 28)}${RESET}` : "";
|
|
1306
|
+
const effort = state.task.effort ? ` ${C_MUTED}\xB7 ${state.task.effort}${RESET}` : "";
|
|
1307
|
+
const phaseLabel = {
|
|
1308
|
+
classifying: "classifying...",
|
|
1309
|
+
planning: "planning...",
|
|
1310
|
+
executing: "executing...",
|
|
1311
|
+
reviewing: "reviewing...",
|
|
1312
|
+
thinking: "thinking",
|
|
1313
|
+
streaming: "streaming",
|
|
1314
|
+
tool: "tool",
|
|
1315
|
+
done: "done"
|
|
1316
|
+
};
|
|
1317
|
+
const label = phaseLabel[state.task.phase] ?? state.task.phase;
|
|
1318
|
+
const phaseColor = state.task.phase === "done" ? C_SUCCESS : state.task.phase === "classifying" || state.task.phase === "planning" || state.task.phase === "reviewing" || state.task.phase === "thinking" ? C_AI : C_ACTION;
|
|
1319
|
+
const marker = state.task.phase === "done" ? `${C_SUCCESS}\u2713${RESET}` : `${phaseColor}${SPINNER[frame]}${RESET}`;
|
|
1320
|
+
out.push(
|
|
1321
|
+
fitLine(
|
|
1322
|
+
` ${marker} ${phaseColor}${label}${RESET}${model}${effort}${tool} ${C_MUTED}\xB7 ${elapsed}${usage}${RESET}`,
|
|
1323
|
+
w
|
|
1324
|
+
)
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (state.taskListVisible && state.taskList.length > 0) {
|
|
1328
|
+
out.push(...renderTaskList(state.taskList, w));
|
|
1329
|
+
}
|
|
1330
|
+
if (state.synthRunning) {
|
|
1331
|
+
out.push(
|
|
1332
|
+
fitLine(
|
|
1333
|
+
` ${C_AI}${SPINNER[frame]}${RESET} ${C_GRAY}synthesizing context ${C_MUTED}(conventions \xB7 rules \xB7 skills)\u2026${RESET}`,
|
|
1334
|
+
w
|
|
1335
|
+
)
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (state.askQuestion?.active) {
|
|
1339
|
+
const { question, options, selection, selectionMode, selected = [] } = state.askQuestion;
|
|
1340
|
+
const isMultiSelect = selectionMode === "multi_select";
|
|
1341
|
+
const questionLines = question.split("\n");
|
|
1342
|
+
out.push(fitLine(` ${sep}`, w));
|
|
1343
|
+
for (let i = 0; i < questionLines.length; i++) {
|
|
1344
|
+
const ql = questionLines[i];
|
|
1345
|
+
if (i === 0) {
|
|
1346
|
+
out.push(fitLine(` ${C_AI}? ${RESET}${C_WHITE}${BOLD}${ql}${RESET}`, w));
|
|
1347
|
+
} else {
|
|
1348
|
+
out.push(fitLine(` ${C_WHITE}${ql}${RESET}`, w));
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
out.push(fitLine(` ${DIM}${C_MUTED}${"\u2500".repeat(Math.min(w - 8, 36))}${RESET}`, w));
|
|
1352
|
+
if (options && options.length > 0) {
|
|
1353
|
+
for (const [idx, opt] of options.entries()) {
|
|
1354
|
+
const focused = idx === (selection ?? 0);
|
|
1355
|
+
const checked = selected.includes(idx);
|
|
1356
|
+
const num = `${C_MUTED}${idx + 1}.${RESET}`;
|
|
1357
|
+
const checkbox = isMultiSelect ? `${C_MUTED}[${checked ? "x" : " "}]${RESET}` : "";
|
|
1358
|
+
const prefix = focused ? `${C_ACTION}>${RESET}` : " ";
|
|
1359
|
+
const label = focused ? `${C_WHITE}${BOLD}${opt.label}${RESET}` : `${C_WHITE}${opt.label}${RESET}`;
|
|
1360
|
+
const desc = opt.description ? ` ${C_GRAY}${opt.description}${RESET}` : "";
|
|
1361
|
+
out.push(fitLine(` ${prefix} ${num} ${checkbox}${checkbox ? " " : ""}${label}${desc}`, w));
|
|
1362
|
+
}
|
|
1363
|
+
out.push(fitLine(``, w));
|
|
1364
|
+
out.push(
|
|
1365
|
+
fitLine(
|
|
1366
|
+
isMultiSelect ? ` ${C_ACTION}\u2191\u2193${RESET} ${C_GRAY}move \xB7 ${RESET}${C_ACTION}Space${RESET} ${C_GRAY}toggle \xB7 ${RESET}${C_ACTION}Enter${RESET} ${C_GRAY}confirm \xB7 ${RESET}${C_ACTION}Esc${RESET} ${C_GRAY}skip${RESET}` : ` ${C_ACTION}\u2191\u2193${RESET} ${C_GRAY}select \xB7 ${RESET}${C_ACTION}Enter${RESET} ${C_GRAY}confirm \xB7 ${RESET}${C_ACTION}Esc${RESET} ${C_GRAY}skip${RESET}`,
|
|
1367
|
+
w
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
} else {
|
|
1371
|
+
out.push(fitLine(``, w));
|
|
1372
|
+
out.push(
|
|
1373
|
+
fitLine(
|
|
1374
|
+
` ${C_GRAY}Type answer + ${RESET}${C_ACTION}Enter${RESET}${C_GRAY} to reply \xB7 ${RESET}${C_ACTION}Esc${RESET} ${C_GRAY}skip${RESET}`,
|
|
1375
|
+
w
|
|
1376
|
+
)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
out.push(fitLine(` ${sep}`, w));
|
|
1380
|
+
} else if (state.planReview?.active) {
|
|
1381
|
+
const sel = state.planReview.selection ?? 0;
|
|
1382
|
+
const reviseMode = state.planReview.reviseMode ?? false;
|
|
1383
|
+
const PLAN_OPTIONS = [
|
|
1384
|
+
{
|
|
1385
|
+
label: "apply",
|
|
1386
|
+
desc: "Proceed with implementation (planner \u2192 coder \u2192 reviewer)"
|
|
1387
|
+
},
|
|
1388
|
+
{ label: "no", desc: "Cancel \u2014 don't implement this plan" },
|
|
1389
|
+
{ label: "revise", desc: "Request changes to the plan" }
|
|
1390
|
+
];
|
|
1391
|
+
out.push(fitLine(` ${sep}`, w));
|
|
1392
|
+
if (reviseMode) {
|
|
1393
|
+
out.push(
|
|
1394
|
+
fitLine(` ${C_WARNING}\u25C8${RESET} ${C_WHITE}${BOLD}Revise plan${RESET}`, w)
|
|
1395
|
+
);
|
|
1396
|
+
out.push(fitLine(` ${DIM}${C_MUTED}${"\u2500".repeat(Math.min(w - 8, 36))}${RESET}`, w));
|
|
1397
|
+
out.push(
|
|
1398
|
+
fitLine(` ${C_GRAY}Describe what to change, then press ${RESET}${C_ACTION}Enter${RESET}${C_GRAY} to submit${RESET}`, w)
|
|
1399
|
+
);
|
|
1400
|
+
out.push(fitLine(``, w));
|
|
1401
|
+
out.push(
|
|
1402
|
+
fitLine(
|
|
1403
|
+
` ${C_ACTION}Esc${RESET} ${C_GRAY}back to options${RESET}`,
|
|
1404
|
+
w
|
|
1405
|
+
)
|
|
1406
|
+
);
|
|
1407
|
+
} else {
|
|
1408
|
+
out.push(
|
|
1409
|
+
fitLine(` ${C_AI}\u25C8${RESET} ${C_WHITE}${BOLD}Plan ready${RESET}`, w)
|
|
1410
|
+
);
|
|
1411
|
+
out.push(fitLine(` ${DIM}${C_MUTED}${"\u2500".repeat(Math.min(w - 8, 36))}${RESET}`, w));
|
|
1412
|
+
for (const [idx, opt] of PLAN_OPTIONS.entries()) {
|
|
1413
|
+
const selected = idx === sel;
|
|
1414
|
+
const num = `${C_MUTED}${idx + 1}.${RESET}`;
|
|
1415
|
+
const prefix = selected ? `${C_ACTION}>${RESET}` : " ";
|
|
1416
|
+
const isNo = opt.label === "no";
|
|
1417
|
+
const isRevise = opt.label === "revise";
|
|
1418
|
+
const labelColor = isNo ? C_ERROR : isRevise ? C_WARNING : C_WHITE;
|
|
1419
|
+
const label = selected ? `${labelColor}${BOLD}${opt.label}${RESET}` : `${labelColor}${opt.label}${RESET}`;
|
|
1420
|
+
out.push(
|
|
1421
|
+
fitLine(` ${prefix} ${num} ${label} ${C_GRAY}${opt.desc}${RESET}`, w)
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
out.push(fitLine(``, w));
|
|
1425
|
+
out.push(
|
|
1426
|
+
fitLine(
|
|
1427
|
+
` ${C_ACTION}\u2191\u2193${RESET} ${C_GRAY}select \xB7 ${RESET}${C_ACTION}Enter${RESET} ${C_GRAY}confirm${RESET}`,
|
|
1428
|
+
w
|
|
1429
|
+
)
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
out.push(fitLine(` ${sep}`, w));
|
|
1433
|
+
} else {
|
|
1434
|
+
out.push(
|
|
1435
|
+
fitLine(
|
|
1436
|
+
` ${C_ACTION}Tab${RESET} ${C_GRAY}complete${RESET} ${C_ACTION}\u2191\u2193${RESET} ${C_GRAY}history${RESET} ${C_ACTION}@${RESET} ${C_GRAY}files${RESET} ${C_ACTION}!${RESET} ${C_GRAY}shell${RESET} ${C_ACTION}/help${RESET} ${C_GRAY}commands${RESET} ${C_ACTION}Ctrl+T${RESET} ${C_GRAY}tasks${RESET} ${C_ACTION}Ctrl+C${RESET} ${C_GRAY}exit${RESET}`,
|
|
1437
|
+
w
|
|
1438
|
+
)
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
if (modelPickerActive) {
|
|
1442
|
+
out.push(
|
|
1443
|
+
fitLine(
|
|
1444
|
+
` ${C_ACTION}\u2191\u2193${RESET} ${C_GRAY}navigate ${RESET}${C_ACTION}Enter${RESET}${C_GRAY}/${RESET}${C_ACTION}Tab${RESET} ${C_GRAY}select model ${RESET}${C_ACTION}Esc${RESET} ${C_GRAY}cancel${RESET}`,
|
|
1445
|
+
w
|
|
1446
|
+
)
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
const editorLines = renderEditor(editor, w, shellMode, debugMode);
|
|
1450
|
+
out.push(...editorLines);
|
|
1451
|
+
out.push(buildStatusBar(state, w, debugMode, statusMessage));
|
|
1452
|
+
return { lines: out };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/cli-agent/chat/ui/pi-tui-app.ts
|
|
1456
|
+
var EXIT_CONFIRM_WINDOW_MS = 2e3;
|
|
1457
|
+
function extractToolResultText(result) {
|
|
1458
|
+
if (typeof result === "string") return result;
|
|
1459
|
+
if (Array.isArray(result)) {
|
|
1460
|
+
return result.map((r) => {
|
|
1461
|
+
if (typeof r === "string") return r;
|
|
1462
|
+
if (typeof r === "object" && r !== null && "text" in r)
|
|
1463
|
+
return String(r.text);
|
|
1464
|
+
return JSON.stringify(r, null, 2);
|
|
1465
|
+
}).join("\n");
|
|
1466
|
+
}
|
|
1467
|
+
return JSON.stringify(result, null, 2);
|
|
1468
|
+
}
|
|
1469
|
+
async function toggleAllToolCallsExpanded(chatTerminal) {
|
|
1470
|
+
const state = chatTerminal.store.getState();
|
|
1471
|
+
const messages = state.messages;
|
|
1472
|
+
const toolCallIdxs = [];
|
|
1473
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1474
|
+
if (messages[i].role === "tool_call") toolCallIdxs.push(i);
|
|
1475
|
+
}
|
|
1476
|
+
if (toolCallIdxs.length === 0) {
|
|
1477
|
+
chatTerminal.store.dispatch((prev) => ({
|
|
1478
|
+
messages: [
|
|
1479
|
+
...prev.messages,
|
|
1480
|
+
{
|
|
1481
|
+
role: "system",
|
|
1482
|
+
content: "No tool call to expand yet.",
|
|
1483
|
+
timestamp: Date.now()
|
|
1484
|
+
}
|
|
1485
|
+
]
|
|
1486
|
+
}));
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const anyExpanded = toolCallIdxs.some((i) => messages[i].expanded);
|
|
1490
|
+
if (anyExpanded) {
|
|
1491
|
+
chatTerminal.store.dispatch((prev) => {
|
|
1492
|
+
const msgs = [...prev.messages];
|
|
1493
|
+
for (const idx of toolCallIdxs) {
|
|
1494
|
+
msgs[idx] = { ...msgs[idx], expanded: false };
|
|
1495
|
+
}
|
|
1496
|
+
return { messages: msgs };
|
|
1497
|
+
});
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const needsFetch = toolCallIdxs.filter((i) => !messages[i].expandedContent);
|
|
1501
|
+
let fetchedResults = /* @__PURE__ */ new Map();
|
|
1502
|
+
if (needsFetch.length > 0) {
|
|
1503
|
+
try {
|
|
1504
|
+
const threadMessages = await getMastraMessages(300);
|
|
1505
|
+
const allResults = [];
|
|
1506
|
+
for (const m of threadMessages) {
|
|
1507
|
+
if (!m) continue;
|
|
1508
|
+
for (const block of m.content) {
|
|
1509
|
+
if (block.type === "tool_result") {
|
|
1510
|
+
allResults.push(
|
|
1511
|
+
extractToolResultText(block.result)
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
for (let j = 0; j < needsFetch.length && j < allResults.length; j++) {
|
|
1517
|
+
const result = allResults[allResults.length - 1 - j];
|
|
1518
|
+
const msgIdx = needsFetch[needsFetch.length - 1 - j];
|
|
1519
|
+
if (result?.trim()) fetchedResults.set(msgIdx, result);
|
|
1520
|
+
}
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
chatTerminal.store.dispatch((prev) => {
|
|
1525
|
+
const msgs = [...prev.messages];
|
|
1526
|
+
for (const idx of toolCallIdxs) {
|
|
1527
|
+
const existing = msgs[idx];
|
|
1528
|
+
const latestResult = existing.toolResults?.at(-1);
|
|
1529
|
+
const fetched = fetchedResults.get(idx);
|
|
1530
|
+
if (existing.expandedContent) {
|
|
1531
|
+
msgs[idx] = { ...existing, expanded: true };
|
|
1532
|
+
} else if (latestResult?.fullContent || latestResult?.content) {
|
|
1533
|
+
msgs[idx] = {
|
|
1534
|
+
...existing,
|
|
1535
|
+
expanded: true,
|
|
1536
|
+
expandedContent: latestResult.fullContent ?? latestResult.content
|
|
1537
|
+
};
|
|
1538
|
+
} else if (fetched) {
|
|
1539
|
+
msgs[idx] = { ...existing, expanded: true, expandedContent: fetched };
|
|
1540
|
+
} else {
|
|
1541
|
+
msgs[idx] = { ...existing, expanded: true };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return { messages: msgs };
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
async function startPiTuiApp(chatTerminal) {
|
|
1548
|
+
const terminal = new ProcessTerminal();
|
|
1549
|
+
const tui = new TUI(terminal, true);
|
|
1550
|
+
let stopped = false;
|
|
1551
|
+
let shellMode = false;
|
|
1552
|
+
let debugMode = false;
|
|
1553
|
+
let planMode = false;
|
|
1554
|
+
let frame = 0;
|
|
1555
|
+
let lastExitConfirmAt = 0;
|
|
1556
|
+
let exitConfirmTimer;
|
|
1557
|
+
let statusMessage = "";
|
|
1558
|
+
let tick;
|
|
1559
|
+
let unsubscribe;
|
|
1560
|
+
const pendingImages = [];
|
|
1561
|
+
let _cachedBlocks = [];
|
|
1562
|
+
let _cachedChromeLines = [];
|
|
1563
|
+
let _cachedWidth = -1;
|
|
1564
|
+
let _cachedChromeSignature = "";
|
|
1565
|
+
let _cachedMessageCount = 0;
|
|
1566
|
+
function messageRenderSignature(msg, idx) {
|
|
1567
|
+
return [
|
|
1568
|
+
idx,
|
|
1569
|
+
msg.role,
|
|
1570
|
+
msg.name ?? "",
|
|
1571
|
+
msg.expanded ? "1" : "0",
|
|
1572
|
+
msg.expandedResultIndex ?? "",
|
|
1573
|
+
chatTerminal.store.getState().config.debug ? "debug" : "",
|
|
1574
|
+
msg.toolCalls?.length ?? 0,
|
|
1575
|
+
msg.toolResults?.length ?? 0
|
|
1576
|
+
].join(":");
|
|
1577
|
+
}
|
|
1578
|
+
function chromeRenderSignature(state) {
|
|
1579
|
+
return JSON.stringify([state.chatMode, state.workspaceState]);
|
|
1580
|
+
}
|
|
1581
|
+
function renderMessageLines2(state, w) {
|
|
1582
|
+
const contentWidth = w - 2;
|
|
1583
|
+
const widthChanged = w !== _cachedWidth;
|
|
1584
|
+
const chromeSignature = chromeRenderSignature(state);
|
|
1585
|
+
const chromeChanged = chromeSignature !== _cachedChromeSignature;
|
|
1586
|
+
if (widthChanged || chromeChanged) {
|
|
1587
|
+
_cachedChromeLines = [
|
|
1588
|
+
...headerLines(state),
|
|
1589
|
+
...workspaceStateCardLines(
|
|
1590
|
+
state.workspaceState,
|
|
1591
|
+
state.chatMode,
|
|
1592
|
+
contentWidth
|
|
1593
|
+
)
|
|
1594
|
+
];
|
|
1595
|
+
_cachedChromeSignature = chromeSignature;
|
|
1596
|
+
}
|
|
1597
|
+
let firstDirty = 0;
|
|
1598
|
+
if (!widthChanged && !chromeChanged && _cachedMessageCount === state.messages.length) {
|
|
1599
|
+
firstDirty = state.messages.length;
|
|
1600
|
+
for (let idx = 0; idx < state.messages.length; idx += 1) {
|
|
1601
|
+
const msg = state.messages[idx];
|
|
1602
|
+
const prev = _cachedBlocks[idx];
|
|
1603
|
+
if (!msg || !prev) {
|
|
1604
|
+
firstDirty = idx;
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
const signature = messageRenderSignature(msg, idx);
|
|
1608
|
+
if (prev.signature !== signature || prev.contentRef !== msg.content) {
|
|
1609
|
+
firstDirty = idx;
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
const nextBlocks = _cachedBlocks.slice(0, firstDirty);
|
|
1615
|
+
for (let idx = firstDirty; idx < state.messages.length; idx += 1) {
|
|
1616
|
+
const msg = state.messages[idx];
|
|
1617
|
+
if (!msg) continue;
|
|
1618
|
+
const signature = messageRenderSignature(msg, idx);
|
|
1619
|
+
const prev = !widthChanged ? _cachedBlocks[idx] : void 0;
|
|
1620
|
+
const lines = prev?.signature === signature && prev.contentRef === msg.content ? prev.lines : messageLines([msg], contentWidth, frame, {
|
|
1621
|
+
showRawToolData: state.config.debug
|
|
1622
|
+
});
|
|
1623
|
+
nextBlocks[idx] = { signature, contentRef: msg.content, lines };
|
|
1624
|
+
}
|
|
1625
|
+
_cachedBlocks = nextBlocks;
|
|
1626
|
+
_cachedWidth = w;
|
|
1627
|
+
_cachedMessageCount = state.messages.length;
|
|
1628
|
+
const out = [];
|
|
1629
|
+
for (const line of _cachedChromeLines) {
|
|
1630
|
+
out.push(line);
|
|
1631
|
+
}
|
|
1632
|
+
for (const block of _cachedBlocks) {
|
|
1633
|
+
if (block) {
|
|
1634
|
+
out.push(...block.lines);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return out;
|
|
1638
|
+
}
|
|
1639
|
+
const editorTheme = {
|
|
1640
|
+
borderColor: (str) => {
|
|
1641
|
+
if (shellMode) return `${C_GREEN}${str}${RESET}`;
|
|
1642
|
+
if (planMode) return `${C_PURPLE}${str}${RESET}`;
|
|
1643
|
+
if (debugMode) return `${C_RED}${str}${RESET}`;
|
|
1644
|
+
return `${C_GRAY}${str}${RESET}`;
|
|
1645
|
+
},
|
|
1646
|
+
selectList: {
|
|
1647
|
+
selectedPrefix: (s) => `${C_CYAN}> ${RESET}${s}`,
|
|
1648
|
+
selectedText: (s) => `\x1B[1m${s}${RESET}`,
|
|
1649
|
+
description: (s) => `${C_GRAY}${s}${RESET}`,
|
|
1650
|
+
scrollInfo: (s) => `${C_GRAY}${s}${RESET}`,
|
|
1651
|
+
noMatch: (s) => `${C_GRAY}${s}${RESET}`
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
const editor = new Editor(tui, editorTheme);
|
|
1655
|
+
editor.focused = true;
|
|
1656
|
+
const slashCommands = [
|
|
1657
|
+
{
|
|
1658
|
+
value: "/plan",
|
|
1659
|
+
description: "Plan then implement: planner \u2192 coder \u2192 reviewer"
|
|
1660
|
+
},
|
|
1661
|
+
...getCommandList().map((c) => ({
|
|
1662
|
+
value: `/${c.name}`,
|
|
1663
|
+
description: c.description
|
|
1664
|
+
}))
|
|
1665
|
+
].sort((a, b) => a.value.localeCompare(b.value));
|
|
1666
|
+
const defaultAutocompleteProvider = new MentionAutocompleteProvider({
|
|
1667
|
+
commands: slashCommands
|
|
1668
|
+
});
|
|
1669
|
+
let modelPickerActive = false;
|
|
1670
|
+
const switchModel = (model) => {
|
|
1671
|
+
const prev = chatTerminal.store.getState();
|
|
1672
|
+
chatTerminal.store.dispatch((s) => ({ config: { ...s.config, model } }));
|
|
1673
|
+
chatTerminal.store.dispatch((s) => ({
|
|
1674
|
+
messages: [
|
|
1675
|
+
...s.messages,
|
|
1676
|
+
{
|
|
1677
|
+
role: "system",
|
|
1678
|
+
content: `Switched model: ${prev.config.model} \u2192 ${model}`
|
|
1679
|
+
}
|
|
1680
|
+
]
|
|
1681
|
+
}));
|
|
1682
|
+
};
|
|
1683
|
+
const closeModelPicker = () => {
|
|
1684
|
+
if (!modelPickerActive) return;
|
|
1685
|
+
modelPickerActive = false;
|
|
1686
|
+
editor.setAutocompleteProvider(defaultAutocompleteProvider);
|
|
1687
|
+
editor.setText("");
|
|
1688
|
+
};
|
|
1689
|
+
const openModelPicker = () => {
|
|
1690
|
+
const state = chatTerminal.store.getState();
|
|
1691
|
+
if (state.config.availableModels.length === 0) return;
|
|
1692
|
+
modelPickerActive = true;
|
|
1693
|
+
editor.setText("");
|
|
1694
|
+
editor.setAutocompleteProvider(
|
|
1695
|
+
new ModelPickerProvider(
|
|
1696
|
+
() => chatTerminal.store.getState().config.availableModels,
|
|
1697
|
+
() => chatTerminal.store.getState().config.model,
|
|
1698
|
+
switchModel,
|
|
1699
|
+
closeModelPicker
|
|
1700
|
+
)
|
|
1701
|
+
);
|
|
1702
|
+
editor.handleInput(" ");
|
|
1703
|
+
};
|
|
1704
|
+
editor.setAutocompleteProvider(defaultAutocompleteProvider);
|
|
1705
|
+
editor.onChange = (text) => {
|
|
1706
|
+
if (!modelPickerActive && /^\/models?\s$/i.test(text)) {
|
|
1707
|
+
openModelPicker();
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
editor.onSubmit = (value) => {
|
|
1711
|
+
const trimmed = value.trim();
|
|
1712
|
+
const state = chatTerminal.store.getState();
|
|
1713
|
+
if (state.planReview?.active && trimmed) {
|
|
1714
|
+
if (state.planReview.reviseMode) {
|
|
1715
|
+
editor.setText("");
|
|
1716
|
+
chatTerminal.resolvePlanReview(`revise: ${trimmed}`);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (state.askQuestion?.active && trimmed && !state.askQuestion.options?.length) {
|
|
1722
|
+
editor.setText("");
|
|
1723
|
+
chatTerminal.resolveAskQuestion(trimmed);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (!trimmed || state.input.busy) return;
|
|
1727
|
+
if (/^\/models?$/i.test(trimmed)) {
|
|
1728
|
+
openModelPicker();
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const imageFiles = pendingImages.map((img) => ({
|
|
1732
|
+
data: img.data,
|
|
1733
|
+
mimeType: img.mimeType
|
|
1734
|
+
}));
|
|
1735
|
+
pendingImages.length = 0;
|
|
1736
|
+
chatTerminal.store.dispatch((prev) => ({
|
|
1737
|
+
input: { ...prev.input, history: [...prev.input.history, trimmed] }
|
|
1738
|
+
}));
|
|
1739
|
+
editor.addToHistory(trimmed);
|
|
1740
|
+
editor.setText("");
|
|
1741
|
+
shellMode = false;
|
|
1742
|
+
void chatTerminal.handleSubmitWithContent(
|
|
1743
|
+
trimmed,
|
|
1744
|
+
false,
|
|
1745
|
+
imageFiles.length > 0 ? imageFiles : void 0
|
|
1746
|
+
);
|
|
1747
|
+
};
|
|
1748
|
+
class MessagesComponent {
|
|
1749
|
+
invalidate() {
|
|
1750
|
+
_cachedWidth = -1;
|
|
1751
|
+
}
|
|
1752
|
+
render(width) {
|
|
1753
|
+
return renderMessageLines2(chatTerminal.store.getState(), width);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
class PanelComponent {
|
|
1757
|
+
invalidate() {
|
|
1758
|
+
}
|
|
1759
|
+
render(width) {
|
|
1760
|
+
const state = chatTerminal.store.getState();
|
|
1761
|
+
debugMode = state.debug;
|
|
1762
|
+
planMode = state.planMode;
|
|
1763
|
+
const result = buildPanel(state, width, {
|
|
1764
|
+
editor,
|
|
1765
|
+
frame,
|
|
1766
|
+
shellMode,
|
|
1767
|
+
debugMode,
|
|
1768
|
+
statusMessage,
|
|
1769
|
+
modelPickerActive
|
|
1770
|
+
});
|
|
1771
|
+
return result.lines;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
tui.addChild(new MessagesComponent());
|
|
1775
|
+
tui.addChild(new PanelComponent());
|
|
1776
|
+
tui.setFocus(editor);
|
|
1777
|
+
function clearExitConfirm() {
|
|
1778
|
+
lastExitConfirmAt = 0;
|
|
1779
|
+
statusMessage = "";
|
|
1780
|
+
if (exitConfirmTimer) {
|
|
1781
|
+
clearTimeout(exitConfirmTimer);
|
|
1782
|
+
exitConfirmTimer = void 0;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function handleInterrupt() {
|
|
1786
|
+
const state = chatTerminal.store.getState();
|
|
1787
|
+
if (editor.getText().length > 0) {
|
|
1788
|
+
editor.setText("");
|
|
1789
|
+
shellMode = false;
|
|
1790
|
+
clearExitConfirm();
|
|
1791
|
+
tui.requestRender();
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (state.input.busy) {
|
|
1795
|
+
const canceledPrompt = chatTerminal.cancelTask();
|
|
1796
|
+
if (canceledPrompt) {
|
|
1797
|
+
editor.setText(canceledPrompt);
|
|
1798
|
+
shellMode = canceledPrompt.startsWith("!");
|
|
1799
|
+
}
|
|
1800
|
+
clearExitConfirm();
|
|
1801
|
+
tui.requestRender();
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (state.planMode) {
|
|
1805
|
+
chatTerminal.store.dispatch({ planMode: false });
|
|
1806
|
+
clearExitConfirm();
|
|
1807
|
+
tui.requestRender();
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
requestExit();
|
|
1811
|
+
}
|
|
1812
|
+
function requestExit() {
|
|
1813
|
+
const now = Date.now();
|
|
1814
|
+
if (now - lastExitConfirmAt <= EXIT_CONFIRM_WINDOW_MS) {
|
|
1815
|
+
cleanup();
|
|
1816
|
+
process.exit(0);
|
|
1817
|
+
}
|
|
1818
|
+
lastExitConfirmAt = now;
|
|
1819
|
+
statusMessage = "Press Ctrl+C again within 2s to exit";
|
|
1820
|
+
if (exitConfirmTimer) clearTimeout(exitConfirmTimer);
|
|
1821
|
+
exitConfirmTimer = setTimeout(() => {
|
|
1822
|
+
clearExitConfirm();
|
|
1823
|
+
tui.requestRender();
|
|
1824
|
+
}, EXIT_CONFIRM_WINDOW_MS);
|
|
1825
|
+
tui.requestRender();
|
|
1826
|
+
}
|
|
1827
|
+
async function handleEditorInput(data) {
|
|
1828
|
+
try {
|
|
1829
|
+
const image = await imageFromPaste(data);
|
|
1830
|
+
if (image) {
|
|
1831
|
+
pendingImages.push(image);
|
|
1832
|
+
const current = editor.getText();
|
|
1833
|
+
editor.setText(current ? `${current} ${image.marker}` : image.marker);
|
|
1834
|
+
tui.requestRender();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1839
|
+
chatTerminal.store.dispatch((prev) => ({
|
|
1840
|
+
messages: [
|
|
1841
|
+
...prev.messages,
|
|
1842
|
+
{
|
|
1843
|
+
role: "system",
|
|
1844
|
+
content: `Image paste skipped: ${message}`,
|
|
1845
|
+
timestamp: Date.now()
|
|
1846
|
+
}
|
|
1847
|
+
]
|
|
1848
|
+
}));
|
|
1849
|
+
tui.requestRender();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (matchesKey(data, Key.backspace)) {
|
|
1853
|
+
const lines = editor.getLines();
|
|
1854
|
+
const { line, col } = editor.getCursor();
|
|
1855
|
+
const textBeforeCursor = (lines[line] ?? "").slice(0, col);
|
|
1856
|
+
const markerMatch = textBeforeCursor.match(/\[image:[^\]]+\]$/);
|
|
1857
|
+
if (markerMatch) {
|
|
1858
|
+
const marker = markerMatch[0];
|
|
1859
|
+
const fullText = lines.join("\n");
|
|
1860
|
+
editor.setText(
|
|
1861
|
+
fullText.slice(0, fullText.lastIndexOf(marker)) + fullText.slice(fullText.lastIndexOf(marker) + marker.length)
|
|
1862
|
+
);
|
|
1863
|
+
for (let i = pendingImages.length - 1; i >= 0; i--) {
|
|
1864
|
+
if (pendingImages[i].marker === marker) {
|
|
1865
|
+
pendingImages.splice(i, 1);
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
tui.requestRender();
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
editor.handleInput(data);
|
|
1874
|
+
if (modelPickerActive && matchesKey(data, Key.escape)) {
|
|
1875
|
+
closeModelPicker();
|
|
1876
|
+
}
|
|
1877
|
+
const currentText = editor.getText();
|
|
1878
|
+
for (let i = pendingImages.length - 1; i >= 0; i--) {
|
|
1879
|
+
if (!currentText.includes(pendingImages[i].marker))
|
|
1880
|
+
pendingImages.splice(i, 1);
|
|
1881
|
+
}
|
|
1882
|
+
shellMode = currentText.startsWith("!");
|
|
1883
|
+
tui.requestRender();
|
|
1884
|
+
}
|
|
1885
|
+
tui.addInputListener((data) => {
|
|
1886
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
1887
|
+
handleInterrupt();
|
|
1888
|
+
return { consume: true };
|
|
1889
|
+
}
|
|
1890
|
+
const state = chatTerminal.store.getState();
|
|
1891
|
+
if (matchesKey(data, Key.escape) && state.screen === "help") {
|
|
1892
|
+
chatTerminal.store.dispatch({ screen: "main" });
|
|
1893
|
+
tui.requestRender();
|
|
1894
|
+
return { consume: true };
|
|
1895
|
+
}
|
|
1896
|
+
if (matchesKey(data, Key.ctrl("o"))) {
|
|
1897
|
+
void toggleAllToolCallsExpanded(chatTerminal);
|
|
1898
|
+
return { consume: true };
|
|
1899
|
+
}
|
|
1900
|
+
if (matchesKey(data, Key.ctrl("t"))) {
|
|
1901
|
+
chatTerminal.store.dispatch((prev) => ({
|
|
1902
|
+
taskListVisible: !prev.taskListVisible
|
|
1903
|
+
}));
|
|
1904
|
+
tui.requestRender();
|
|
1905
|
+
return { consume: true };
|
|
1906
|
+
}
|
|
1907
|
+
if (state.askQuestion?.active) {
|
|
1908
|
+
const aq = state.askQuestion;
|
|
1909
|
+
const options = aq.options ?? [];
|
|
1910
|
+
if (options.length > 0) {
|
|
1911
|
+
const sel = aq.selection ?? 0;
|
|
1912
|
+
const isMultiSelect = aq.selectionMode === "multi_select";
|
|
1913
|
+
if (editor.getText().trim() === "") {
|
|
1914
|
+
if (matchesKey(data, Key.up)) {
|
|
1915
|
+
chatTerminal.store.dispatch({
|
|
1916
|
+
askQuestion: {
|
|
1917
|
+
...aq,
|
|
1918
|
+
selection: (sel + options.length - 1) % options.length
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
tui.requestRender();
|
|
1922
|
+
return { consume: true };
|
|
1923
|
+
}
|
|
1924
|
+
if (matchesKey(data, Key.down)) {
|
|
1925
|
+
chatTerminal.store.dispatch({
|
|
1926
|
+
askQuestion: { ...aq, selection: (sel + 1) % options.length }
|
|
1927
|
+
});
|
|
1928
|
+
tui.requestRender();
|
|
1929
|
+
return { consume: true };
|
|
1930
|
+
}
|
|
1931
|
+
if (isMultiSelect && data === " ") {
|
|
1932
|
+
const selected = aq.selected.includes(sel) ? aq.selected.filter((idx) => idx !== sel) : [...aq.selected, sel];
|
|
1933
|
+
chatTerminal.store.dispatch({ askQuestion: { ...aq, selected } });
|
|
1934
|
+
tui.requestRender();
|
|
1935
|
+
return { consume: true };
|
|
1936
|
+
}
|
|
1937
|
+
if (matchesKey(data, Key.enter)) {
|
|
1938
|
+
if (isMultiSelect) {
|
|
1939
|
+
chatTerminal.resolveAskQuestion(
|
|
1940
|
+
aq.selected.map((idx) => options[idx]?.label).filter((label) => Boolean(label))
|
|
1941
|
+
);
|
|
1942
|
+
} else {
|
|
1943
|
+
const selected = options[sel];
|
|
1944
|
+
if (selected) chatTerminal.resolveAskQuestion(selected.label);
|
|
1945
|
+
}
|
|
1946
|
+
return { consume: true };
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (matchesKey(data, Key.escape)) {
|
|
1950
|
+
chatTerminal.resolveAskQuestion(isMultiSelect ? [] : "(skipped)");
|
|
1951
|
+
return { consume: true };
|
|
1952
|
+
}
|
|
1953
|
+
return { consume: true };
|
|
1954
|
+
}
|
|
1955
|
+
if (matchesKey(data, Key.escape)) {
|
|
1956
|
+
chatTerminal.resolveAskQuestion("(skipped)");
|
|
1957
|
+
return { consume: true };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (state.planReview?.active && editor.getText().trim() === "") {
|
|
1961
|
+
const pr = state.planReview;
|
|
1962
|
+
if (pr.reviseMode) {
|
|
1963
|
+
if (matchesKey(data, Key.escape)) {
|
|
1964
|
+
chatTerminal.store.dispatch({
|
|
1965
|
+
planReview: { active: true, selection: 2, reviseMode: false }
|
|
1966
|
+
});
|
|
1967
|
+
tui.requestRender();
|
|
1968
|
+
return { consume: true };
|
|
1969
|
+
}
|
|
1970
|
+
void handleEditorInput(data);
|
|
1971
|
+
return { consume: true };
|
|
1972
|
+
}
|
|
1973
|
+
if (!pr.reviseMode) {
|
|
1974
|
+
if (!matchesKey(data, Key.up) && !matchesKey(data, Key.down) && !matchesKey(data, Key.enter) && !matchesKey(data, Key.escape)) {
|
|
1975
|
+
return { consume: true };
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const PLAN_OPTIONS = ["apply", "no", "revise"];
|
|
1979
|
+
const sel = pr.selection ?? 0;
|
|
1980
|
+
if (matchesKey(data, Key.up)) {
|
|
1981
|
+
chatTerminal.store.dispatch({
|
|
1982
|
+
planReview: {
|
|
1983
|
+
active: true,
|
|
1984
|
+
selection: (sel + PLAN_OPTIONS.length - 1) % PLAN_OPTIONS.length
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
tui.requestRender();
|
|
1988
|
+
return { consume: true };
|
|
1989
|
+
}
|
|
1990
|
+
if (matchesKey(data, Key.down)) {
|
|
1991
|
+
chatTerminal.store.dispatch({
|
|
1992
|
+
planReview: {
|
|
1993
|
+
active: true,
|
|
1994
|
+
selection: (sel + 1) % PLAN_OPTIONS.length
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
tui.requestRender();
|
|
1998
|
+
return { consume: true };
|
|
1999
|
+
}
|
|
2000
|
+
if (matchesKey(data, Key.enter)) {
|
|
2001
|
+
const chosen = PLAN_OPTIONS[sel] ?? "apply";
|
|
2002
|
+
if (chosen === "revise") {
|
|
2003
|
+
chatTerminal.store.dispatch({
|
|
2004
|
+
planReview: { active: true, selection: sel, reviseMode: true }
|
|
2005
|
+
});
|
|
2006
|
+
tui.requestRender();
|
|
2007
|
+
return { consume: true };
|
|
2008
|
+
}
|
|
2009
|
+
chatTerminal.resolvePlanReview(chosen);
|
|
2010
|
+
return { consume: true };
|
|
2011
|
+
}
|
|
2012
|
+
if (matchesKey(data, Key.escape)) {
|
|
2013
|
+
chatTerminal.resolvePlanReview("cancel");
|
|
2014
|
+
return { consume: true };
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (state.planReview?.active && !state.planReview.reviseMode) {
|
|
2018
|
+
if (matchesKey(data, Key.up) || matchesKey(data, Key.down) || matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) {
|
|
2019
|
+
return { consume: true };
|
|
2020
|
+
}
|
|
2021
|
+
return { consume: true };
|
|
2022
|
+
}
|
|
2023
|
+
void handleEditorInput(data);
|
|
2024
|
+
return { consume: true };
|
|
2025
|
+
});
|
|
2026
|
+
function cleanup() {
|
|
2027
|
+
if (stopped) return;
|
|
2028
|
+
stopped = true;
|
|
2029
|
+
if (exitConfirmTimer) {
|
|
2030
|
+
clearTimeout(exitConfirmTimer);
|
|
2031
|
+
exitConfirmTimer = void 0;
|
|
2032
|
+
}
|
|
2033
|
+
if (tick) {
|
|
2034
|
+
clearInterval(tick);
|
|
2035
|
+
tick = void 0;
|
|
2036
|
+
}
|
|
2037
|
+
unsubscribe?.();
|
|
2038
|
+
process.off("SIGINT", handleInterrupt);
|
|
2039
|
+
tui.stop();
|
|
2040
|
+
}
|
|
2041
|
+
process.once("exit", cleanup);
|
|
2042
|
+
process.once("SIGTERM", () => {
|
|
2043
|
+
cleanup();
|
|
2044
|
+
process.exit(0);
|
|
2045
|
+
});
|
|
2046
|
+
process.on("SIGINT", handleInterrupt);
|
|
2047
|
+
await initShiki().catch(() => {
|
|
2048
|
+
});
|
|
2049
|
+
tui.start();
|
|
2050
|
+
tick = setInterval(() => {
|
|
2051
|
+
const state = chatTerminal.store.getState();
|
|
2052
|
+
if (state.planReview?.active) return;
|
|
2053
|
+
if (isActiveTaskPhase(state.task.phase) || state.synthRunning) {
|
|
2054
|
+
frame = (frame + 1) % SPINNER.length;
|
|
2055
|
+
tui.requestRender();
|
|
2056
|
+
}
|
|
2057
|
+
}, 120);
|
|
2058
|
+
unsubscribe = chatTerminal.bus.on("screen:refresh", () => {
|
|
2059
|
+
tui.requestRender();
|
|
2060
|
+
});
|
|
2061
|
+
await new Promise((resolve2) => {
|
|
2062
|
+
const interval = setInterval(() => {
|
|
2063
|
+
if (stopped) {
|
|
2064
|
+
clearInterval(interval);
|
|
2065
|
+
resolve2();
|
|
2066
|
+
}
|
|
2067
|
+
}, 50);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
export {
|
|
2071
|
+
isActiveTaskPhase,
|
|
2072
|
+
startPiTuiApp
|
|
2073
|
+
};
|