@bubblebrain-ai/bubble 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +6 -2
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +31 -3
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +46 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +302 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +217 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +286 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +288 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/main.js +98 -32
- package/dist/model-catalog.js +3 -0
- package/dist/prompt/compose.js +3 -3
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-openai-codex.d.ts +8 -1
- package/dist/provider-openai-codex.js +33 -9
- package/dist/provider.d.ts +2 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +134 -0
- package/dist/session-types.d.ts +5 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +154 -2
- package/dist/skills/invocation.js +0 -18
- package/dist/skills/registry.d.ts +1 -0
- package/dist/skills/registry.js +2 -0
- package/dist/slash-commands/commands.js +15 -22
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.js +1 -4
- package/dist/tui-ink/app.js +265 -118
- package/dist/tui-ink/code-highlight.js +2 -3
- package/dist/tui-ink/detect-theme.d.ts +1 -18
- package/dist/tui-ink/detect-theme.js +1 -37
- package/dist/tui-ink/display-history.d.ts +20 -3
- package/dist/tui-ink/display-history.js +26 -27
- package/dist/tui-ink/feedback-dialog.d.ts +19 -0
- package/dist/tui-ink/feedback-dialog.js +123 -0
- package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
- package/dist/tui-ink/feishu-setup-picker.js +261 -0
- package/dist/tui-ink/input-box.d.ts +25 -1
- package/dist/tui-ink/input-box.js +132 -11
- package/dist/tui-ink/input-history.js +3 -5
- package/dist/tui-ink/markdown.d.ts +32 -0
- package/dist/tui-ink/markdown.js +111 -4
- package/dist/tui-ink/message-list.d.ts +1 -6
- package/dist/tui-ink/message-list.js +86 -34
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +81 -27
- package/dist/tui-ink/run-session-picker.d.ts +10 -0
- package/dist/tui-ink/run-session-picker.js +22 -0
- package/dist/tui-ink/run.js +7 -2
- package/dist/tui-ink/session-picker.d.ts +10 -0
- package/dist/tui-ink/session-picker.js +110 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- package/dist/tui-ink/theme.js +2 -2
- package/dist/tui-ink/trace-groups.js +25 -2
- package/dist/tui-ink/welcome.js +2 -4
- package/package.json +4 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/display-history.d.ts +0 -44
- package/dist/tui/display-history.js +0 -243
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/file-mentions.d.ts +0 -29
- package/dist/tui/file-mentions.js +0 -174
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/image-paste.d.ts +0 -95
- package/dist/tui/image-paste.js +0 -505
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -21
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/recent-activity.d.ts +0 -8
- package/dist/tui/recent-activity.js +0 -71
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -38
- package/dist/tui/run.js +0 -6996
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -114
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -30
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import type { SkillRegistry } from "../skills/registry.js";
|
|
2
2
|
import { type ImageAttachment } from "./image-paste.js";
|
|
3
3
|
export interface SubmitPayload {
|
|
4
|
+
/** Fully-expanded text sent to the agent. */
|
|
4
5
|
text: string;
|
|
6
|
+
/** Text shown in the composer/transcript when it differs from the real text. */
|
|
7
|
+
displayText?: string;
|
|
5
8
|
images: ImageAttachment[];
|
|
6
9
|
}
|
|
7
10
|
interface InputBoxProps {
|
|
8
11
|
onSubmit: (payload: SubmitPayload) => void;
|
|
9
12
|
onPasteNotice?: (notice: string) => void;
|
|
10
13
|
disabled?: boolean;
|
|
14
|
+
cursorResetEpoch?: number;
|
|
15
|
+
draftText?: string;
|
|
16
|
+
draftEpoch?: number;
|
|
17
|
+
onDraftApplied?: () => void;
|
|
11
18
|
skillRegistry?: SkillRegistry;
|
|
12
19
|
terminalColumns: number;
|
|
13
20
|
cwd: string;
|
|
14
21
|
}
|
|
22
|
+
export interface PastedContentReference {
|
|
23
|
+
marker: string;
|
|
24
|
+
content: string;
|
|
25
|
+
}
|
|
15
26
|
export declare function needsCursorRowCompensation(nextOutputHeight: number, viewportRows: number, previousOutputHeight: number | null): boolean;
|
|
27
|
+
export declare function resolveCursorRowCompensation(input: {
|
|
28
|
+
sameRenderedFrame: boolean;
|
|
29
|
+
previousRowCompensation: number;
|
|
30
|
+
nextOutputHeight: number;
|
|
31
|
+
viewportRows: number;
|
|
32
|
+
previousOutputHeight: number | null;
|
|
33
|
+
}): number;
|
|
34
|
+
export declare function isCtrlCInput(input: string, key: {
|
|
35
|
+
ctrl?: boolean;
|
|
36
|
+
}): boolean;
|
|
16
37
|
export declare function shouldSubmitExactSlashSuggestion(input: string, suggestionName?: string): boolean;
|
|
17
38
|
export declare function resolveSlashEnterAction(input: string, suggestions: Array<{
|
|
18
39
|
name: string;
|
|
@@ -37,5 +58,8 @@ export declare function insertNewlineAtCursor(text: string, cursor: number): {
|
|
|
37
58
|
text: string;
|
|
38
59
|
cursor: number;
|
|
39
60
|
};
|
|
40
|
-
export declare function
|
|
61
|
+
export declare function shouldCollapsePastedContent(text: string): boolean;
|
|
62
|
+
export declare function createPastedContentMarker(content: string): string;
|
|
63
|
+
export declare function expandPastedContentMarkers(displayText: string, references: PastedContentReference[]): string;
|
|
64
|
+
export declare function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch, draftText, draftEpoch, onDraftApplied, skillRegistry, terminalColumns, cwd, }: InputBoxProps): import("react/jsx-runtime").JSX.Element;
|
|
41
65
|
export {};
|
|
@@ -8,11 +8,14 @@ import { useTheme } from "./theme.js";
|
|
|
8
8
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
9
9
|
import { ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
10
|
import { appendHistoryEntry, loadHistorySync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
|
+
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
11
12
|
const MIN_VISIBLE_LINES = 3;
|
|
12
13
|
const MAX_VISIBLE_LINES = 6;
|
|
13
14
|
const PADDING_X = 1;
|
|
14
15
|
const PROMPT = " > ";
|
|
15
16
|
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
17
|
+
const LONG_PASTE_CHAR_THRESHOLD = 1000;
|
|
18
|
+
const LONG_PASTE_LINE_THRESHOLD = 20;
|
|
16
19
|
export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previousOutputHeight) {
|
|
17
20
|
const hadPreviousFrame = previousOutputHeight !== null && previousOutputHeight > 0;
|
|
18
21
|
const isFullscreen = nextOutputHeight >= viewportRows;
|
|
@@ -26,6 +29,14 @@ export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previ
|
|
|
26
29
|
// line below the output, so pass y+1 in those cases.
|
|
27
30
|
return isFullscreen || wasOverflowing || (isOverflowing && hadPreviousFrame) || isLeavingFullscreen;
|
|
28
31
|
}
|
|
32
|
+
export function resolveCursorRowCompensation(input) {
|
|
33
|
+
if (input.sameRenderedFrame)
|
|
34
|
+
return input.previousRowCompensation;
|
|
35
|
+
return needsCursorRowCompensation(input.nextOutputHeight, input.viewportRows, input.previousOutputHeight) ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
export function isCtrlCInput(input, key) {
|
|
38
|
+
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
39
|
+
}
|
|
29
40
|
// Break a logical line into segments that each fit within `maxWidth` display
|
|
30
41
|
// columns. Uses string-width so CJK and emoji wrap correctly; empty lines
|
|
31
42
|
// still produce one empty segment so cursors on blank lines render.
|
|
@@ -137,7 +148,42 @@ export function insertNewlineAtCursor(text, cursor) {
|
|
|
137
148
|
cursor: clampedCursor + 1,
|
|
138
149
|
};
|
|
139
150
|
}
|
|
140
|
-
export function
|
|
151
|
+
export function shouldCollapsePastedContent(text) {
|
|
152
|
+
if (text.length >= LONG_PASTE_CHAR_THRESHOLD)
|
|
153
|
+
return true;
|
|
154
|
+
return text.split("\n").length >= LONG_PASTE_LINE_THRESHOLD;
|
|
155
|
+
}
|
|
156
|
+
export function createPastedContentMarker(content) {
|
|
157
|
+
return `[Pasted Content ${content.length} chars]`;
|
|
158
|
+
}
|
|
159
|
+
export function expandPastedContentMarkers(displayText, references) {
|
|
160
|
+
if (references.length === 0 || displayText.length === 0)
|
|
161
|
+
return displayText;
|
|
162
|
+
let expanded = "";
|
|
163
|
+
let index = 0;
|
|
164
|
+
const used = new Set();
|
|
165
|
+
while (index < displayText.length) {
|
|
166
|
+
let matched = -1;
|
|
167
|
+
for (let i = 0; i < references.length; i++) {
|
|
168
|
+
const ref = references[i];
|
|
169
|
+
if (!used.has(i) && displayText.startsWith(ref.marker, index)) {
|
|
170
|
+
matched = i;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (matched >= 0) {
|
|
175
|
+
const ref = references[matched];
|
|
176
|
+
expanded += ref.content;
|
|
177
|
+
index += ref.marker.length;
|
|
178
|
+
used.add(matched);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
expanded += displayText[index];
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
return expanded;
|
|
185
|
+
}
|
|
186
|
+
export function InputBox({ onSubmit, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, terminalColumns, cwd, }) {
|
|
141
187
|
const theme = useTheme();
|
|
142
188
|
const width = terminalColumns;
|
|
143
189
|
const [text, setText] = useState("");
|
|
@@ -145,6 +191,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
145
191
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
146
192
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
147
193
|
const [attachments, setAttachments] = useState([]);
|
|
194
|
+
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
148
195
|
const [history, setHistory] = useState(() => loadHistorySync());
|
|
149
196
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
150
197
|
const historyDraftRef = useRef("");
|
|
@@ -279,7 +326,14 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
279
326
|
const imageTokens = tokens.filter(isImageFilePath);
|
|
280
327
|
if (imageTokens.length === 0) {
|
|
281
328
|
// Plain text paste — insert into the input at the cursor.
|
|
282
|
-
|
|
329
|
+
if (shouldCollapsePastedContent(clean)) {
|
|
330
|
+
const marker = createPastedContentMarker(clean);
|
|
331
|
+
setPastedContentRefs((prev) => [...prev, { marker, content: clean }]);
|
|
332
|
+
insertTextAtCursor(marker);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
insertTextAtCursor(clean);
|
|
336
|
+
}
|
|
283
337
|
clearPending();
|
|
284
338
|
return;
|
|
285
339
|
}
|
|
@@ -333,20 +387,28 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
333
387
|
setSelectedIndex(0);
|
|
334
388
|
};
|
|
335
389
|
const submitInput = (submittedText) => {
|
|
336
|
-
|
|
390
|
+
const expandedText = expandPastedContentMarkers(submittedText, pastedContentRefs);
|
|
391
|
+
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
337
392
|
return;
|
|
338
|
-
onSubmit({
|
|
339
|
-
|
|
340
|
-
|
|
393
|
+
onSubmit({
|
|
394
|
+
text: expandedText,
|
|
395
|
+
displayText: expandedText === submittedText ? undefined : submittedText,
|
|
396
|
+
images: attachments,
|
|
397
|
+
});
|
|
398
|
+
// A collapsed marker cannot be safely replayed from history once its
|
|
399
|
+
// in-memory paste reference is gone; skip those entries instead.
|
|
400
|
+
if (expandedText.trim().length > 0 && expandedText === submittedText) {
|
|
401
|
+
const nextHistory = pushHistoryEntry(history, expandedText);
|
|
341
402
|
if (nextHistory !== history) {
|
|
342
403
|
setHistory(nextHistory);
|
|
343
|
-
appendHistoryEntry(
|
|
404
|
+
appendHistoryEntry(expandedText);
|
|
344
405
|
}
|
|
345
406
|
}
|
|
346
407
|
setText("");
|
|
347
408
|
setCursor(0);
|
|
348
409
|
setSelectedIndex(0);
|
|
349
410
|
setAttachments([]);
|
|
411
|
+
setPastedContentRefs([]);
|
|
350
412
|
setHistoryIndex(null);
|
|
351
413
|
historyDraftRef.current = "";
|
|
352
414
|
};
|
|
@@ -365,8 +427,15 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
365
427
|
return false;
|
|
366
428
|
};
|
|
367
429
|
useInput((input, key) => {
|
|
430
|
+
const strippedInput = stripTerminalMouseSequences(input);
|
|
431
|
+
if (strippedInput !== input && !strippedInput) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
input = strippedInput;
|
|
368
435
|
if (disabled)
|
|
369
436
|
return;
|
|
437
|
+
if (isCtrlCInput(input, key))
|
|
438
|
+
return;
|
|
370
439
|
if (process.env.BUBBLE_KEY_DEBUG) {
|
|
371
440
|
try {
|
|
372
441
|
appendFileSync("/tmp/bubble-key.log", JSON.stringify({
|
|
@@ -490,6 +559,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
490
559
|
setHistoryIndex(result.index);
|
|
491
560
|
historyDraftRef.current = result.draft;
|
|
492
561
|
setSelectedIndex(0);
|
|
562
|
+
setPastedContentRefs([]);
|
|
493
563
|
}
|
|
494
564
|
return;
|
|
495
565
|
}
|
|
@@ -505,6 +575,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
505
575
|
setHistoryIndex(result.index);
|
|
506
576
|
historyDraftRef.current = result.draft;
|
|
507
577
|
setSelectedIndex(0);
|
|
578
|
+
setPastedContentRefs([]);
|
|
508
579
|
}
|
|
509
580
|
return;
|
|
510
581
|
}
|
|
@@ -526,8 +597,55 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
526
597
|
const previousViewportRowsRef = useRef(null);
|
|
527
598
|
const previousInputFrameSignatureRef = useRef(null);
|
|
528
599
|
const previousRowCompensationRef = useRef(0);
|
|
600
|
+
const lastCursorResetEpochRef = useRef(null);
|
|
601
|
+
const lastDraftEpochRef = useRef(null);
|
|
602
|
+
const lastWidthRef = useRef(null);
|
|
529
603
|
const { setCursorPosition } = useCursor();
|
|
530
604
|
const { stdout } = useStdout();
|
|
605
|
+
const [cursorTick, setCursorTick] = useState(0);
|
|
606
|
+
useLayoutEffect(() => {
|
|
607
|
+
const isInitialMount = lastCursorResetEpochRef.current === null;
|
|
608
|
+
const shouldReset = !isInitialMount || cursorResetEpoch > 0;
|
|
609
|
+
lastCursorResetEpochRef.current = cursorResetEpoch;
|
|
610
|
+
if (!shouldReset)
|
|
611
|
+
return;
|
|
612
|
+
previousOutputHeightRef.current = null;
|
|
613
|
+
previousViewportRowsRef.current = null;
|
|
614
|
+
previousInputFrameSignatureRef.current = null;
|
|
615
|
+
previousRowCompensationRef.current = 0;
|
|
616
|
+
lastCursorRef.current = null;
|
|
617
|
+
setCursorPosition(undefined);
|
|
618
|
+
setCursorTick((t) => t + 1);
|
|
619
|
+
}, [cursorResetEpoch, setCursorPosition]);
|
|
620
|
+
useLayoutEffect(() => {
|
|
621
|
+
if (lastDraftEpochRef.current === draftEpoch)
|
|
622
|
+
return;
|
|
623
|
+
lastDraftEpochRef.current = draftEpoch;
|
|
624
|
+
if (!draftText)
|
|
625
|
+
return;
|
|
626
|
+
setText(draftText);
|
|
627
|
+
setCursor(draftText.length);
|
|
628
|
+
setSelectedIndex(0);
|
|
629
|
+
setPastedContentRefs([]);
|
|
630
|
+
setHistoryIndex(null);
|
|
631
|
+
historyDraftRef.current = "";
|
|
632
|
+
onDraftApplied?.();
|
|
633
|
+
}, [draftEpoch, draftText, onDraftApplied]);
|
|
634
|
+
// After a terminal resize the previous-frame refs reference a layout that no
|
|
635
|
+
// longer exists; carrying them forward makes `needsCursorRowCompensation`
|
|
636
|
+
// compare new yoga heights against stale ones and offsets the cursor by a
|
|
637
|
+
// row. Reset to a "no previous frame" state so the next layout effect treats
|
|
638
|
+
// the new width as a fresh start.
|
|
639
|
+
useLayoutEffect(() => {
|
|
640
|
+
if (lastWidthRef.current !== null && lastWidthRef.current !== width) {
|
|
641
|
+
previousOutputHeightRef.current = null;
|
|
642
|
+
previousViewportRowsRef.current = null;
|
|
643
|
+
previousInputFrameSignatureRef.current = null;
|
|
644
|
+
previousRowCompensationRef.current = 0;
|
|
645
|
+
lastCursorRef.current = null;
|
|
646
|
+
}
|
|
647
|
+
lastWidthRef.current = width;
|
|
648
|
+
}, [width]);
|
|
531
649
|
const contentWidth = Math.max(1, width - PADDING_X * 2);
|
|
532
650
|
const lineWidth = Math.max(1, contentWidth - PROMPT.length);
|
|
533
651
|
const visualLines = useMemo(() => computeVisualLines(text, lineWidth), [text, lineWidth]);
|
|
@@ -581,7 +699,6 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
581
699
|
// user can't type. Keeping the real cursor visible in the input makes it
|
|
582
700
|
// flicker every time streaming output above it re-lays out the frame, so
|
|
583
701
|
// we hide it entirely until input is active again.
|
|
584
|
-
const [cursorTick, setCursorTick] = useState(0);
|
|
585
702
|
useLayoutEffect(() => {
|
|
586
703
|
let node = cursorLineRef.current ?? undefined;
|
|
587
704
|
if (!node?.yogaNode) {
|
|
@@ -617,9 +734,13 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
617
734
|
const sameRenderedFrame = previousOutputHeight === rootHeight &&
|
|
618
735
|
previousViewportRowsRef.current === viewportRows &&
|
|
619
736
|
previousInputFrameSignatureRef.current === inputFrameSignature;
|
|
620
|
-
const rowCompensation =
|
|
621
|
-
|
|
622
|
-
:
|
|
737
|
+
const rowCompensation = resolveCursorRowCompensation({
|
|
738
|
+
sameRenderedFrame,
|
|
739
|
+
previousRowCompensation: previousRowCompensationRef.current,
|
|
740
|
+
nextOutputHeight: rootHeight,
|
|
741
|
+
viewportRows,
|
|
742
|
+
previousOutputHeight,
|
|
743
|
+
});
|
|
623
744
|
previousOutputHeightRef.current = rootHeight;
|
|
624
745
|
previousViewportRowsRef.current = viewportRows;
|
|
625
746
|
previousInputFrameSignatureRef.current = inputFrameSignature;
|
|
@@ -22,7 +22,7 @@ export function loadHistorySync(filePath = defaultHistoryFilePath()) {
|
|
|
22
22
|
out.push(parsed);
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
25
|
-
// Malformed line
|
|
25
|
+
// Malformed line - skip rather than fail the whole load.
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
|
|
@@ -42,9 +42,9 @@ export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
|
|
|
42
42
|
// Persistence is best-effort; never crash the composer over disk IO.
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
// Pure transition for
|
|
45
|
+
// Pure transition for up/down navigation. `index === null` means the user is
|
|
46
46
|
// editing a fresh draft; otherwise it points at history[index]. When stepping
|
|
47
|
-
// from the draft into history we snapshot the current text so
|
|
47
|
+
// from the draft into history we snapshot the current text so down past the
|
|
48
48
|
// newest entry can restore it.
|
|
49
49
|
export function stepHistory(state, direction, currentText) {
|
|
50
50
|
const { history, index, draft } = state;
|
|
@@ -61,13 +61,11 @@ export function stepHistory(state, direction, currentText) {
|
|
|
61
61
|
}
|
|
62
62
|
return noChange;
|
|
63
63
|
}
|
|
64
|
-
// down
|
|
65
64
|
if (index === null)
|
|
66
65
|
return noChange;
|
|
67
66
|
if (index < history.length - 1) {
|
|
68
67
|
return { text: history[index + 1], index: index + 1, draft, changed: true };
|
|
69
68
|
}
|
|
70
|
-
// Past the newest entry: restore the saved draft and clear it.
|
|
71
69
|
return { text: draft, index: null, draft: "", changed: true };
|
|
72
70
|
}
|
|
73
71
|
// Push to in-memory history with last-entry dedupe so repeated identical
|
|
@@ -30,7 +30,39 @@ interface InlineStyle {
|
|
|
30
30
|
code?: boolean;
|
|
31
31
|
}
|
|
32
32
|
export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
|
|
33
|
+
/**
|
|
34
|
+
* Return the byte offset where the LAST markdown block begins in `text`.
|
|
35
|
+
*
|
|
36
|
+
* Used by the streaming renderer to split incoming content into a "stable
|
|
37
|
+
* prefix" (everything before the in-flight block — already-closed blocks)
|
|
38
|
+
* and an "unstable suffix" (the block currently being typed by the model).
|
|
39
|
+
* Mirrors parseMarkdownBlocks's lexing rules so the boundary it produces is
|
|
40
|
+
* compatible with how MarkdownContent will later parse the prefix.
|
|
41
|
+
*
|
|
42
|
+
* Returns `text.length` when no blocks are present (empty / whitespace-only
|
|
43
|
+
* input), and `0` when the entire text is a single in-flight block.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findLastBlockStart(text: string): number;
|
|
33
46
|
export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
|
|
47
|
+
/**
|
|
48
|
+
* Streaming-aware wrapper around `MarkdownContent`.
|
|
49
|
+
*
|
|
50
|
+
* On every render, splits the incoming `content` into a stable prefix
|
|
51
|
+
* (everything before the in-flight block) and an unstable suffix (the block
|
|
52
|
+
* currently being typed). The two halves are rendered as two separate
|
|
53
|
+
* `MarkdownContent` instances; the stable one uses the same `content` prop
|
|
54
|
+
* across deltas, so its internal `useMemo([content])` short-circuits and
|
|
55
|
+
* does NOT re-parse on each token — which is the whole point. Only the
|
|
56
|
+
* shorter unstable suffix re-parses per delta.
|
|
57
|
+
*
|
|
58
|
+
* The boundary advances monotonically (the prefix only grows). A defensive
|
|
59
|
+
* reset handles the rare case where `content` is replaced wholesale (e.g.,
|
|
60
|
+
* the user re-enters a turn).
|
|
61
|
+
*/
|
|
62
|
+
export declare function StreamingMarkdown({ content, maxWidth, }: {
|
|
63
|
+
content: string;
|
|
64
|
+
maxWidth?: number;
|
|
65
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
34
66
|
export declare function MarkdownContent({ content, maxWidth, }: {
|
|
35
67
|
content: string;
|
|
36
68
|
maxWidth?: number;
|
package/dist/tui-ink/markdown.js
CHANGED
|
@@ -89,6 +89,85 @@ export function parseMarkdownBlocks(text) {
|
|
|
89
89
|
}
|
|
90
90
|
return blocks;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Return the byte offset where the LAST markdown block begins in `text`.
|
|
94
|
+
*
|
|
95
|
+
* Used by the streaming renderer to split incoming content into a "stable
|
|
96
|
+
* prefix" (everything before the in-flight block — already-closed blocks)
|
|
97
|
+
* and an "unstable suffix" (the block currently being typed by the model).
|
|
98
|
+
* Mirrors parseMarkdownBlocks's lexing rules so the boundary it produces is
|
|
99
|
+
* compatible with how MarkdownContent will later parse the prefix.
|
|
100
|
+
*
|
|
101
|
+
* Returns `text.length` when no blocks are present (empty / whitespace-only
|
|
102
|
+
* input), and `0` when the entire text is a single in-flight block.
|
|
103
|
+
*/
|
|
104
|
+
export function findLastBlockStart(text) {
|
|
105
|
+
if (text.length === 0)
|
|
106
|
+
return 0;
|
|
107
|
+
const lines = text.split("\n");
|
|
108
|
+
// `split("\n")` on "abc\n" yields ["abc", ""]; on "abc" yields ["abc"]. The
|
|
109
|
+
// trailing newline only contributes a length-1 separator for every line
|
|
110
|
+
// except the final element produced by split.
|
|
111
|
+
const lineLengthWithSeparator = (idx) => {
|
|
112
|
+
const len = lines[idx].length;
|
|
113
|
+
return idx < lines.length - 1 ? len + 1 : len;
|
|
114
|
+
};
|
|
115
|
+
let i = 0;
|
|
116
|
+
let offset = 0;
|
|
117
|
+
let lastBlockStart = text.length;
|
|
118
|
+
while (i < lines.length) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
// Code block (fenced). Unclosed fences extend through EOF — that's exactly
|
|
121
|
+
// what we want for streaming, since marked-lexer-style "single token until
|
|
122
|
+
// close" keeps the in-flight code in the unstable suffix.
|
|
123
|
+
if (line.startsWith("```")) {
|
|
124
|
+
lastBlockStart = offset;
|
|
125
|
+
offset += lineLengthWithSeparator(i);
|
|
126
|
+
i++;
|
|
127
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
128
|
+
offset += lineLengthWithSeparator(i);
|
|
129
|
+
i++;
|
|
130
|
+
}
|
|
131
|
+
if (i < lines.length) {
|
|
132
|
+
offset += lineLengthWithSeparator(i);
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Table — same shape as parseMarkdownBlocks: consume consecutive `|` lines.
|
|
138
|
+
if (line.trim().startsWith("|")) {
|
|
139
|
+
lastBlockStart = offset;
|
|
140
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
|
141
|
+
offset += lineLengthWithSeparator(i);
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// Heading.
|
|
147
|
+
if (/^#{1,6}\s+/.test(line)) {
|
|
148
|
+
lastBlockStart = offset;
|
|
149
|
+
offset += lineLengthWithSeparator(i);
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Blank line — does not start a block; just advances the offset.
|
|
154
|
+
if (line.trim() === "") {
|
|
155
|
+
offset += lineLengthWithSeparator(i);
|
|
156
|
+
i++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Paragraph — runs until blank line or start of another block.
|
|
160
|
+
lastBlockStart = offset;
|
|
161
|
+
while (i < lines.length &&
|
|
162
|
+
lines[i].trim() !== "" &&
|
|
163
|
+
!lines[i].startsWith("```") &&
|
|
164
|
+
!lines[i].trim().startsWith("|")) {
|
|
165
|
+
offset += lineLengthWithSeparator(i);
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return lastBlockStart;
|
|
170
|
+
}
|
|
92
171
|
function parseTableRow(line) {
|
|
93
172
|
let body = line.trim();
|
|
94
173
|
if (body.startsWith("|"))
|
|
@@ -263,10 +342,8 @@ function InlineText({ text }) {
|
|
|
263
342
|
function CodeBlock({ lang, lines }) {
|
|
264
343
|
const theme = useTheme();
|
|
265
344
|
// Lazy init: try sync highlight when shiki is already warm so the very first
|
|
266
|
-
// paint carries highlighted output.
|
|
267
|
-
//
|
|
268
|
-
// — anything we ship via setState in useEffect lands too late to appear in
|
|
269
|
-
// scrollback. Fall back to raw lines if shiki hasn't loaded yet.
|
|
345
|
+
// paint carries highlighted output. Fall back to raw lines if shiki hasn't
|
|
346
|
+
// loaded yet; useEffect will refresh the mounted transcript item later.
|
|
270
347
|
const [highlighted, setHighlighted] = React.useState(() => {
|
|
271
348
|
const code = lines.join("\n");
|
|
272
349
|
if (!code)
|
|
@@ -377,6 +454,36 @@ function HeadingBlock({ level, text }) {
|
|
|
377
454
|
}
|
|
378
455
|
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
379
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Streaming-aware wrapper around `MarkdownContent`.
|
|
459
|
+
*
|
|
460
|
+
* On every render, splits the incoming `content` into a stable prefix
|
|
461
|
+
* (everything before the in-flight block) and an unstable suffix (the block
|
|
462
|
+
* currently being typed). The two halves are rendered as two separate
|
|
463
|
+
* `MarkdownContent` instances; the stable one uses the same `content` prop
|
|
464
|
+
* across deltas, so its internal `useMemo([content])` short-circuits and
|
|
465
|
+
* does NOT re-parse on each token — which is the whole point. Only the
|
|
466
|
+
* shorter unstable suffix re-parses per delta.
|
|
467
|
+
*
|
|
468
|
+
* The boundary advances monotonically (the prefix only grows). A defensive
|
|
469
|
+
* reset handles the rare case where `content` is replaced wholesale (e.g.,
|
|
470
|
+
* the user re-enters a turn).
|
|
471
|
+
*/
|
|
472
|
+
export function StreamingMarkdown({ content, maxWidth, }) {
|
|
473
|
+
const stablePrefixRef = React.useRef("");
|
|
474
|
+
if (!content.startsWith(stablePrefixRef.current)) {
|
|
475
|
+
stablePrefixRef.current = "";
|
|
476
|
+
}
|
|
477
|
+
const boundary = stablePrefixRef.current.length;
|
|
478
|
+
const tail = content.substring(boundary);
|
|
479
|
+
const advance = findLastBlockStart(tail);
|
|
480
|
+
if (advance > 0) {
|
|
481
|
+
stablePrefixRef.current = content.substring(0, boundary + advance);
|
|
482
|
+
}
|
|
483
|
+
const stablePrefix = stablePrefixRef.current;
|
|
484
|
+
const unstableSuffix = content.substring(stablePrefix.length);
|
|
485
|
+
return (_jsxs(Box, { flexDirection: "column", children: [stablePrefix && _jsx(MarkdownContent, { content: stablePrefix, maxWidth: maxWidth }), unstableSuffix && _jsx(MarkdownContent, { content: unstableSuffix, maxWidth: maxWidth })] }));
|
|
486
|
+
}
|
|
380
487
|
export function MarkdownContent({ content, maxWidth, }) {
|
|
381
488
|
const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
|
|
382
489
|
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => {
|
|
@@ -21,12 +21,7 @@ interface MessageListProps {
|
|
|
21
21
|
pendingApproval?: PendingApprovalHint | null;
|
|
22
22
|
/** Animation tick used to refresh in-progress elapsed counters. */
|
|
23
23
|
nowTick?: number;
|
|
24
|
-
/**
|
|
25
|
-
* Optional banner rendered as the first item of the scrollback Static
|
|
26
|
-
* stream. Committed to scrollback once on initial mount so it doesn't
|
|
27
|
-
* float between older messages and the live tail as the conversation
|
|
28
|
-
* progresses.
|
|
29
|
-
*/
|
|
24
|
+
/** Optional banner rendered as the first item in the app-controlled transcript. */
|
|
30
25
|
welcomeBanner?: React.ReactNode;
|
|
31
26
|
}
|
|
32
27
|
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|