u-foo 2.5.13 → 2.5.15
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/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +339 -24
- package/src/code/commands.js +61 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +698 -0
- package/src/code/context/executionSegment.js +314 -0
- package/src/code/context/featureFlag.js +13 -0
- package/src/code/context/index.js +18 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +159 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +412 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +4 -1
- package/src/code/index.js +6 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +187 -31
- package/src/code/repl.js +36 -32
- package/src/code/sessionStore.js +227 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +65 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +32 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +260 -44
- package/src/ui/format/markdownRenderer.js +215 -72
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +408 -55
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
2
|
+
* Shared markdown renderer for TUI log output.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Produces either blessed tags (chat / legacy) or chalk ANSI (Ink ucode).
|
|
5
|
+
* Terminals cannot change font size, so headings/emphasis use color + weight
|
|
6
|
+
* instead of literal `#` / `**` markers.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
const chalk = require("chalk");
|
|
10
|
+
|
|
8
11
|
function stripLeakedEscapeTags(text = "") {
|
|
9
12
|
const source = String(text == null ? "" : text);
|
|
10
13
|
const withoutClosedTags = source.replace(/\{[^{}\n]*escape[^{}\n]*\}/gi, "");
|
|
@@ -12,35 +15,157 @@ function stripLeakedEscapeTags(text = "") {
|
|
|
12
15
|
return withoutDanglingEscape.replace(/\{\s*\/?\s*e?s?c?a?p?e?[^{}\n]*$/gi, "");
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
function createBlessedAdapters(escapeFn = (value) => String(value || "")) {
|
|
19
|
+
const escape = (value) => escapeFn(value);
|
|
20
|
+
return {
|
|
21
|
+
escape,
|
|
22
|
+
bold: (value) => `{bold}{white-fg}${escape(value)}{/white-fg}{/bold}`,
|
|
23
|
+
italic: (value) => `{italic}{gray-fg}${escape(value)}{/gray-fg}{/italic}`,
|
|
24
|
+
code: (value) => `{yellow-fg}${escape(value)}{/yellow-fg}`,
|
|
25
|
+
heading: (level, value) => {
|
|
26
|
+
const depth = Math.max(1, Math.min(6, Number(level) || 1));
|
|
27
|
+
if (depth <= 2) return `{cyan-fg}{bold}${value}{/bold}{/cyan-fg}`;
|
|
28
|
+
if (depth === 3) return `{blue-fg}{bold}${value}{/bold}{/blue-fg}`;
|
|
29
|
+
return `{bold}${value}{/bold}`;
|
|
30
|
+
},
|
|
31
|
+
quoteMarker: () => "{gray-fg}│{/gray-fg}",
|
|
32
|
+
bulletMarker: () => "{gray-fg}•{/gray-fg}",
|
|
33
|
+
orderedMarker: (value) => `{gray-fg}${escape(value)}.{/gray-fg}`,
|
|
34
|
+
rule: () => "{gray-fg}────────────────────────{/gray-fg}",
|
|
35
|
+
fenceOpen: (language) => (
|
|
36
|
+
language
|
|
37
|
+
? `{gray-fg}┌ code:${escape(language)}{/gray-fg}`
|
|
38
|
+
: "{gray-fg}┌ code{/gray-fg}"
|
|
39
|
+
),
|
|
40
|
+
fenceClose: () => "{gray-fg}└{/gray-fg}",
|
|
41
|
+
fenceBody: (value) => `{gray-fg}│{/gray-fg} {white-fg}${escape(value)}{/white-fg}`,
|
|
42
|
+
error: (value) => `{red-fg}${value}{/red-fg}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createAnsiAdapters() {
|
|
47
|
+
// Ink always paints into a TTY-capable stdout; force color so bold/heading
|
|
48
|
+
// styles survive even when chalk's autodetection thinks we're non-TTY
|
|
49
|
+
// (e.g. piped test harnesses that still render Ink).
|
|
50
|
+
const paint = typeof chalk.Instance === "function"
|
|
51
|
+
? new chalk.Instance({ level: Math.max(Number(chalk.level) || 0, 2) })
|
|
52
|
+
: chalk;
|
|
53
|
+
return {
|
|
54
|
+
escape: (value) => String(value || ""),
|
|
55
|
+
// Bold gets weight + brighter foreground so it reads even when the
|
|
56
|
+
// terminal theme barely differentiates ANSI bold.
|
|
57
|
+
bold: (value) => paint.bold.whiteBright(String(value || "")),
|
|
58
|
+
italic: (value) => paint.italic.dim(String(value || "")),
|
|
59
|
+
code: (value) => paint.yellow(String(value || "")),
|
|
60
|
+
heading: (level, value) => {
|
|
61
|
+
const depth = Math.max(1, Math.min(6, Number(level) || 1));
|
|
62
|
+
const text = String(value || "");
|
|
63
|
+
if (depth <= 2) return paint.bold.cyan(text);
|
|
64
|
+
if (depth === 3) return paint.bold.blue(text);
|
|
65
|
+
return paint.bold(text);
|
|
66
|
+
},
|
|
67
|
+
quoteMarker: () => paint.gray("│"),
|
|
68
|
+
bulletMarker: () => paint.gray("•"),
|
|
69
|
+
orderedMarker: (value) => paint.gray(`${value}.`),
|
|
70
|
+
rule: () => paint.gray("────────────────────────"),
|
|
71
|
+
fenceOpen: (language) => (
|
|
72
|
+
language
|
|
73
|
+
? paint.gray(`┌ code:${language}`)
|
|
74
|
+
: paint.gray("┌ code")
|
|
75
|
+
),
|
|
76
|
+
fenceClose: () => paint.gray("└"),
|
|
77
|
+
fenceBody: (value) => `${paint.gray("│")} ${paint.white(String(value || ""))}`,
|
|
78
|
+
error: (value) => paint.red(String(value || "")),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply inline markdown to a single line.
|
|
84
|
+
* Scans left-to-right so **bold** wins over nested `code`/`*` patterns
|
|
85
|
+
* (LLMs often emit **`name`** which previously left literal asterisks).
|
|
86
|
+
*/
|
|
87
|
+
function renderInlineMarkdown(input = "", adapters = createBlessedAdapters()) {
|
|
88
|
+
const source = String(input || "");
|
|
89
|
+
if (!source) return "";
|
|
90
|
+
|
|
91
|
+
const escape = adapters.escape || ((value) => String(value || ""));
|
|
92
|
+
const styleBold = adapters.bold || escape;
|
|
93
|
+
const styleItalic = adapters.italic || escape;
|
|
94
|
+
const styleCode = adapters.code || escape;
|
|
95
|
+
|
|
96
|
+
if (!source.includes("`") && !source.includes("*") && !source.includes("_")) {
|
|
97
|
+
return escape(source);
|
|
19
98
|
}
|
|
20
99
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
100
|
+
const renderInner = (chunk) => renderInlineMarkdown(chunk, adapters);
|
|
101
|
+
|
|
102
|
+
let out = "";
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < source.length) {
|
|
105
|
+
// **bold** / __bold__
|
|
106
|
+
if (source.startsWith("**", i) || source.startsWith("__", i)) {
|
|
107
|
+
const mark = source.slice(i, i + 2);
|
|
108
|
+
const close = source.indexOf(mark, i + 2);
|
|
109
|
+
if (close !== -1) {
|
|
110
|
+
const inner = source.slice(i + 2, close);
|
|
111
|
+
out += styleBold(renderInner(inner));
|
|
112
|
+
i = close + 2;
|
|
113
|
+
continue;
|
|
34
114
|
}
|
|
35
|
-
out += `{yellow-fg}${escapeFn(match[1])}{/yellow-fg}`;
|
|
36
|
-
cursor = index + match[0].length;
|
|
37
|
-
match = pattern.exec(source);
|
|
38
115
|
}
|
|
39
|
-
|
|
40
|
-
|
|
116
|
+
|
|
117
|
+
// `code`
|
|
118
|
+
if (source[i] === "`") {
|
|
119
|
+
const close = source.indexOf("`", i + 1);
|
|
120
|
+
if (close !== -1) {
|
|
121
|
+
let inner = source.slice(i + 1, close);
|
|
122
|
+
// Code that is only a bold/italic wrapper → treat as emphasis.
|
|
123
|
+
const boldOnly = inner.match(/^\*\*(.+)\*\*$/) || inner.match(/^__(.+)__$/);
|
|
124
|
+
const italicOnly = !boldOnly && (inner.match(/^\*(.+)\*$/) || inner.match(/^_(.+)_$/));
|
|
125
|
+
if (boldOnly) out += styleBold(renderInner(boldOnly[1]));
|
|
126
|
+
else if (italicOnly) out += styleItalic(renderInner(italicOnly[1]));
|
|
127
|
+
else out += styleCode(inner);
|
|
128
|
+
i = close + 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
41
131
|
}
|
|
42
|
-
|
|
43
|
-
|
|
132
|
+
|
|
133
|
+
// *italic* / _italic_ (single delimiter; avoid ** / __)
|
|
134
|
+
if (
|
|
135
|
+
(source[i] === "*" && source[i + 1] !== "*")
|
|
136
|
+
|| (source[i] === "_" && source[i + 1] !== "_")
|
|
137
|
+
) {
|
|
138
|
+
const mark = source[i];
|
|
139
|
+
const close = source.indexOf(mark, i + 1);
|
|
140
|
+
if (close !== -1 && source[close + 1] !== mark) {
|
|
141
|
+
const inner = source.slice(i + 1, close);
|
|
142
|
+
if (inner && !inner.includes("\n")) {
|
|
143
|
+
out += styleItalic(renderInner(inner));
|
|
144
|
+
i = close + 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Accumulate plain run until the next markup candidate.
|
|
151
|
+
let next = source.length;
|
|
152
|
+
for (const ch of ["*", "_", "`"]) {
|
|
153
|
+
const at = source.indexOf(ch, i + 1);
|
|
154
|
+
if (at !== -1 && at < next) next = at;
|
|
155
|
+
}
|
|
156
|
+
// Also stop at `**` start from current if we failed to parse above.
|
|
157
|
+
out += escape(source.slice(i, next));
|
|
158
|
+
i = next === i ? i + 1 : next;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function renderMarkdownLinesWithAdapters(text = "", state = {}, adapters = createBlessedAdapters()) {
|
|
165
|
+
const renderState = state && typeof state === "object" ? state : {};
|
|
166
|
+
if (typeof renderState.inCodeBlock !== "boolean") {
|
|
167
|
+
renderState.inCodeBlock = false;
|
|
168
|
+
}
|
|
44
169
|
|
|
45
170
|
const lines = String(text || "").split(/\r?\n/);
|
|
46
171
|
const out = [];
|
|
@@ -51,71 +176,89 @@ function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String
|
|
|
51
176
|
if (fenceMatch) {
|
|
52
177
|
if (!renderState.inCodeBlock) {
|
|
53
178
|
const language = String(fenceMatch[3] || "").trim();
|
|
54
|
-
|
|
55
|
-
? `┌ code:${escapeFn(language)}`
|
|
56
|
-
: "┌ code";
|
|
57
|
-
out.push(`{gray-fg}${label}{/gray-fg}`);
|
|
179
|
+
out.push(adapters.fenceOpen(language));
|
|
58
180
|
renderState.inCodeBlock = true;
|
|
59
181
|
} else {
|
|
60
|
-
out.push(
|
|
182
|
+
out.push(adapters.fenceClose());
|
|
61
183
|
renderState.inCodeBlock = false;
|
|
62
184
|
}
|
|
63
185
|
continue;
|
|
64
186
|
}
|
|
65
187
|
|
|
66
188
|
if (renderState.inCodeBlock) {
|
|
67
|
-
out.push(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (headingMatch) {
|
|
71
|
-
const indent = escapeFn(headingMatch[1] || "");
|
|
72
|
-
const marks = escapeFn(headingMatch[2] || "");
|
|
73
|
-
const content = renderInlineCode(headingMatch[3] || "");
|
|
74
|
-
out.push(`${indent}{cyan-fg}${marks}{/cyan-fg} {bold}${content}{/bold}`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
189
|
+
out.push(adapters.fenceBody(raw));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
77
192
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
out.push(`${indent}{gray-fg}▍{/gray-fg} ${content}`);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
193
|
+
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(raw)) {
|
|
194
|
+
out.push(adapters.rule());
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
85
197
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
198
|
+
const headingMatch = raw.match(/^(\s*)(#{1,6})\s+(.*)$/);
|
|
199
|
+
if (headingMatch) {
|
|
200
|
+
const indent = adapters.escape(headingMatch[1] || "");
|
|
201
|
+
const level = String(headingMatch[2] || "#").length;
|
|
202
|
+
const content = adapters.heading(
|
|
203
|
+
level,
|
|
204
|
+
renderInlineMarkdown(headingMatch[3] || "", adapters),
|
|
205
|
+
);
|
|
206
|
+
out.push(`${indent}${content}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
93
209
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
210
|
+
const quoteMatch = raw.match(/^(\s*)>\s?(.*)$/);
|
|
211
|
+
if (quoteMatch) {
|
|
212
|
+
const indent = adapters.escape(quoteMatch[1] || "");
|
|
213
|
+
const content = renderInlineMarkdown(quoteMatch[2] || "", adapters);
|
|
214
|
+
out.push(`${indent}${adapters.quoteMarker()} ${content}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
102
217
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
218
|
+
const bulletMatch = raw.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
219
|
+
if (bulletMatch) {
|
|
220
|
+
const indent = adapters.escape(bulletMatch[1] || "");
|
|
221
|
+
const content = renderInlineMarkdown(bulletMatch[3] || "", adapters);
|
|
222
|
+
out.push(`${indent}${adapters.bulletMarker()} ${content}`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const orderedMatch = raw.match(/^(\s*)(\d+)\.\s+(.*)$/);
|
|
227
|
+
if (orderedMatch) {
|
|
228
|
+
const indent = adapters.escape(orderedMatch[1] || "");
|
|
229
|
+
const content = renderInlineMarkdown(orderedMatch[3] || "", adapters);
|
|
230
|
+
out.push(`${indent}${adapters.orderedMarker(orderedMatch[2] || "")} ${content}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
110
233
|
|
|
111
|
-
|
|
234
|
+
const errorMatch = raw.match(/^(\s*)(Error:\s+.*)$/i);
|
|
235
|
+
if (errorMatch) {
|
|
236
|
+
const indent = adapters.escape(errorMatch[1] || "");
|
|
237
|
+
const content = renderInlineMarkdown(errorMatch[2] || "", adapters);
|
|
238
|
+
out.push(`${indent}${adapters.error(content)}`);
|
|
239
|
+
continue;
|
|
112
240
|
}
|
|
241
|
+
|
|
242
|
+
out.push(renderInlineMarkdown(raw, adapters));
|
|
113
243
|
}
|
|
114
244
|
|
|
115
245
|
return out;
|
|
116
246
|
}
|
|
117
247
|
|
|
248
|
+
function renderMarkdownLines(text = "", state = {}, escapeFn = (value) => String(value || "")) {
|
|
249
|
+
return renderMarkdownLinesWithAdapters(text, state, createBlessedAdapters(escapeFn));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderMarkdownLinesAnsi(text = "", state = {}) {
|
|
253
|
+
return renderMarkdownLinesWithAdapters(text, state, createAnsiAdapters());
|
|
254
|
+
}
|
|
255
|
+
|
|
118
256
|
module.exports = {
|
|
119
257
|
stripLeakedEscapeTags,
|
|
258
|
+
renderInlineMarkdown,
|
|
120
259
|
renderMarkdownLines,
|
|
260
|
+
renderMarkdownLinesAnsi,
|
|
261
|
+
renderMarkdownLinesWithAdapters,
|
|
262
|
+
createBlessedAdapters,
|
|
263
|
+
createAnsiAdapters,
|
|
121
264
|
};
|
package/src/ui/ink/ChatApp.js
CHANGED
|
@@ -457,7 +457,13 @@ const CHAT_LOG_ROW_PALETTE = {
|
|
|
457
457
|
// margin-top on the next entry, because per-item rendering can't know a
|
|
458
458
|
// group's end until the following entry arrives.
|
|
459
459
|
function decorateStaticLogEntry(prev, entry) {
|
|
460
|
-
const
|
|
460
|
+
const markdownState = prev && prev.markdownState && typeof prev.markdownState === "object"
|
|
461
|
+
? { inCodeBlock: Boolean(prev.markdownState.inCodeBlock) }
|
|
462
|
+
: { inCodeBlock: false };
|
|
463
|
+
const sourceText = entry && typeof entry === "object" && entry.text != null
|
|
464
|
+
? String(entry.text)
|
|
465
|
+
: entry;
|
|
466
|
+
const row = buildChatLogLineModel(sourceText, { markdownState });
|
|
461
467
|
const continuation = Boolean(
|
|
462
468
|
prev
|
|
463
469
|
&& (row.kind === "plain" || row.kind === "spacer")
|
|
@@ -468,7 +474,7 @@ function decorateStaticLogEntry(prev, entry) {
|
|
|
468
474
|
// block, and only when the previous block was a transcript group (whose
|
|
469
475
|
// old dynamic renderer contributed a trailing marginBottom).
|
|
470
476
|
const marginBefore = Boolean(!continuation && prev && STATIC_GROUPABLE_KINDS.has(prev.groupKind));
|
|
471
|
-
return { entry, row, groupKind, continuation, marginBefore };
|
|
477
|
+
return { entry, row, groupKind, continuation, marginBefore, markdownState };
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
function createInkStreamState({
|
|
@@ -713,8 +719,9 @@ function buildInternalLogRows(lines = [], width = 80, maxRows = 20) {
|
|
|
713
719
|
let rendered = [classified.text];
|
|
714
720
|
if (classified.markdown) {
|
|
715
721
|
try {
|
|
716
|
-
|
|
717
|
-
|
|
722
|
+
// Share ucode's ANSI markdown renderer so Ink can show bold/code
|
|
723
|
+
// without blessed tags (which would otherwise be stripped).
|
|
724
|
+
rendered = fmt.renderLogLinesWithMarkdownAnsi(classified.text, markdownState);
|
|
718
725
|
} catch {
|
|
719
726
|
rendered = [classified.text];
|
|
720
727
|
}
|
|
@@ -2484,7 +2491,17 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
2484
2491
|
exit,
|
|
2485
2492
|
]);
|
|
2486
2493
|
|
|
2487
|
-
const onArrowUpAtTop = useCallback(() => {
|
|
2494
|
+
const onArrowUpAtTop = useCallback((currentValue) => {
|
|
2495
|
+
// Clear @-target before history so Up from an empty ›@agent prompt
|
|
2496
|
+
// restores the bare › prompt instead of recalling a prior draft.
|
|
2497
|
+
const inputValue = currentValue != null ? currentValue : state.draft;
|
|
2498
|
+
if (fmt.shouldClearAgentSelectionOnUp({
|
|
2499
|
+
agentSelectionMode: state.agentSelectionMode,
|
|
2500
|
+
inputValue,
|
|
2501
|
+
})) {
|
|
2502
|
+
dispatch({ type: "agents/clearTarget" });
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2488
2505
|
if (state.inputHistory.length > 0) {
|
|
2489
2506
|
const next = Math.max(0, state.historyIndex - 1);
|
|
2490
2507
|
if (next !== state.historyIndex || state.draft !== state.inputHistory[next]) {
|
|
@@ -2492,10 +2509,8 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
2492
2509
|
dispatch({ type: "draft/set", value: state.inputHistory[next] || "" });
|
|
2493
2510
|
setCompletionSuppressedDraft(state.inputHistory[next] || "");
|
|
2494
2511
|
setDraftVersion((v) => v + 1);
|
|
2495
|
-
return;
|
|
2496
2512
|
}
|
|
2497
2513
|
}
|
|
2498
|
-
if (state.agentSelectionMode) dispatch({ type: "agents/clearTarget" });
|
|
2499
2514
|
}, [state.inputHistory, state.historyIndex, state.draft, state.agentSelectionMode]);
|
|
2500
2515
|
|
|
2501
2516
|
const onArrowDownAtBottom = useCallback((currentValue) => {
|
|
@@ -2985,7 +3000,23 @@ function createChatApp({ React, ink, props, interactive = true }) {
|
|
|
2985
3000
|
});
|
|
2986
3001
|
return;
|
|
2987
3002
|
}
|
|
2988
|
-
if (key.return
|
|
3003
|
+
if (key.return) {
|
|
3004
|
+
// Final/leaf completions submit immediately; parents only fill draft.
|
|
3005
|
+
const item = completions[Math.max(0, Math.min(completions.length - 1, completionIndex))];
|
|
3006
|
+
if (item && !item.hasChildren) {
|
|
3007
|
+
const cmd = String(item.replace || "").trim();
|
|
3008
|
+
setCompletionIndex(0);
|
|
3009
|
+
setCompletionSuppressedDraft(null);
|
|
3010
|
+
if (cmd) void submit(cmd);
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
acceptCompletion();
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (key.tab) {
|
|
3017
|
+
acceptCompletion();
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
2989
3020
|
if (key.escape) {
|
|
2990
3021
|
setCompletionSuppressedDraft(null);
|
|
2991
3022
|
dispatch({ type: "draft/clear" });
|