@bubblebrain-ai/bubble 0.0.11 → 0.0.13
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/input-controller.d.ts +11 -0
- package/dist/agent/input-controller.js +30 -0
- package/dist/agent.d.ts +6 -4
- package/dist/agent.js +39 -2
- package/dist/feishu/agent-host/run-driver.js +13 -6
- package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
- package/dist/feishu/router/commands.js +2 -1
- package/dist/feishu/scope/session-binder.js +1 -1
- package/dist/feishu/serve.js +3 -3
- package/dist/main.js +78 -12
- 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 +5 -0
- package/dist/session.js +75 -9
- 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 +29 -22
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +10 -0
- 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/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/detect-theme.d.ts +2 -0
- package/dist/tui/detect-theme.js +87 -0
- package/dist/tui/display-history.d.ts +62 -0
- package/dist/tui/display-history.js +305 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +52 -0
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/image-paste.d.ts +95 -0
- package/dist/tui/image-paste.js +505 -0
- package/dist/tui/input-history.d.ts +16 -0
- package/dist/tui/input-history.js +79 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +22 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +42 -0
- package/dist/tui/prompt-keybindings.js +35 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.d.ts +45 -0
- package/dist/tui/run.js +8816 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +135 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/tui/trace-groups.d.ts +27 -0
- package/dist/tui/trace-groups.js +412 -0
- package/dist/tui/wordmark.d.ts +15 -0
- package/dist/tui/wordmark.js +179 -0
- package/dist/tui-ink/app.js +98 -70
- package/dist/tui-ink/input-box.d.ts +22 -1
- package/dist/tui-ink/input-box.js +105 -11
- package/dist/tui-ink/message-list.js +12 -3
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +80 -23
- package/dist/tui-ink/session-picker.js +5 -7
- package/dist/tui-ink/theme.d.ts +3 -9
- package/dist/tui-ink/theme.js +39 -45
- package/dist/tui-ink/welcome.js +22 -78
- package/dist/tui-opentui/app.d.ts +54 -0
- package/dist/tui-opentui/app.js +1363 -0
- package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
- package/dist/tui-opentui/approval/approval-dialog.js +139 -0
- package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
- package/dist/tui-opentui/approval/diff-view.js +43 -0
- package/dist/tui-opentui/approval/select.d.ts +37 -0
- package/dist/tui-opentui/approval/select.js +91 -0
- package/dist/tui-opentui/detect-theme.d.ts +2 -0
- package/dist/tui-opentui/detect-theme.js +87 -0
- package/dist/tui-opentui/display-history.d.ts +55 -0
- package/dist/tui-opentui/display-history.js +129 -0
- package/dist/tui-opentui/edit-diff.d.ts +11 -0
- package/dist/tui-opentui/edit-diff.js +52 -0
- package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
- package/dist/tui-opentui/feedback-dialog.js +164 -0
- package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
- package/dist/tui-opentui/feishu-setup-picker.js +272 -0
- package/dist/tui-opentui/file-mentions.d.ts +29 -0
- package/dist/tui-opentui/file-mentions.js +174 -0
- package/dist/tui-opentui/footer.d.ts +26 -0
- package/dist/tui-opentui/footer.js +40 -0
- package/dist/tui-opentui/image-paste.d.ts +54 -0
- package/dist/tui-opentui/image-paste.js +288 -0
- package/dist/tui-opentui/input-box.d.ts +34 -0
- package/dist/tui-opentui/input-box.js +471 -0
- package/dist/tui-opentui/input-history.d.ts +16 -0
- package/dist/tui-opentui/input-history.js +79 -0
- package/dist/tui-opentui/markdown.d.ts +66 -0
- package/dist/tui-opentui/markdown.js +127 -0
- package/dist/tui-opentui/message-list.d.ts +31 -0
- package/dist/tui-opentui/message-list.js +125 -0
- package/dist/tui-opentui/model-picker.d.ts +63 -0
- package/dist/tui-opentui/model-picker.js +450 -0
- package/dist/tui-opentui/plan-confirm.d.ts +9 -0
- package/dist/tui-opentui/plan-confirm.js +124 -0
- package/dist/tui-opentui/question-dialog.d.ts +10 -0
- package/dist/tui-opentui/question-dialog.js +110 -0
- package/dist/tui-opentui/recent-activity.d.ts +8 -0
- package/dist/tui-opentui/recent-activity.js +71 -0
- package/dist/tui-opentui/run-session-picker.d.ts +10 -0
- package/dist/tui-opentui/run-session-picker.js +28 -0
- package/dist/tui-opentui/run.d.ts +38 -0
- package/dist/tui-opentui/run.js +48 -0
- package/dist/tui-opentui/session-picker.d.ts +12 -0
- package/dist/tui-opentui/session-picker.js +120 -0
- package/dist/tui-opentui/theme.d.ts +89 -0
- package/dist/tui-opentui/theme.js +157 -0
- package/dist/tui-opentui/todos.d.ts +9 -0
- package/dist/tui-opentui/todos.js +45 -0
- package/dist/tui-opentui/trace-groups.d.ts +27 -0
- package/dist/tui-opentui/trace-groups.js +412 -0
- package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
- package/dist/tui-opentui/use-terminal-size.js +5 -0
- package/dist/tui-opentui/welcome.d.ts +25 -0
- package/dist/tui-opentui/welcome.js +77 -0
- package/dist/types.d.ts +24 -0
- package/package.json +5 -1
package/dist/tui-ink/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Box, Text, useApp, useInput } from "ink";
|
|
|
4
4
|
import { AgentAbortError } from "../agent.js";
|
|
5
5
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
6
6
|
import { UserConfig, maskKey } from "../config.js";
|
|
7
|
-
import { InputBox, isCtrlCInput } from "./input-box.js";
|
|
7
|
+
import { createPastedContentMarker, InputBox, isCtrlCInput, shouldCollapsePastedContent, } from "./input-box.js";
|
|
8
8
|
import { MessageList } from "./message-list.js";
|
|
9
9
|
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
10
10
|
import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
@@ -65,7 +65,9 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
65
65
|
result.push({
|
|
66
66
|
key: nextDisplayMessageKey("user"),
|
|
67
67
|
role: "user",
|
|
68
|
-
content: typeof m.content === "string"
|
|
68
|
+
content: typeof m.content === "string"
|
|
69
|
+
? (shouldCollapsePastedContent(m.content) ? createPastedContentMarker(m.content) : m.content)
|
|
70
|
+
: "(multimedia)",
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
else if (m.role === "assistant") {
|
|
@@ -178,9 +180,40 @@ function withMessageKey(message) {
|
|
|
178
180
|
const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
|
|
179
181
|
return { ...message, key: nextDisplayMessageKey(prefix) };
|
|
180
182
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
// Keep the live (non-Static) region small so non-GPU terminals (xterm.js DOM
|
|
184
|
+
// renderer, ssh into a basic terminal, tmux without GPU) don't flicker when
|
|
185
|
+
// Ink re-reconciles the streaming block on every token. Flushing earlier and
|
|
186
|
+
// in smaller chunks shifts most of the answer into terminal scrollback, where
|
|
187
|
+
// it's a one-time write that doesn't get re-rendered.
|
|
188
|
+
const STREAMING_STATIC_FLUSH_MIN_CHARS = 600;
|
|
189
|
+
const STREAMING_STATIC_FLUSH_TARGET_CHARS = 400;
|
|
190
|
+
const STREAMING_STATIC_FLUSH_MIN_TAIL = 120;
|
|
191
|
+
/**
|
|
192
|
+
* True iff `prefix` ends inside an open ```/~~~ fenced code block. Splitting
|
|
193
|
+
* the streaming buffer at such a point would let the flushed half render
|
|
194
|
+
* without its closing fence — `MarkdownContent` would then treat the body as
|
|
195
|
+
* plain prose and the trailing half would render as an isolated code block
|
|
196
|
+
* with no opener. Fence delimiters of different families don't close each
|
|
197
|
+
* other (a `~~~` inside a ``` block is just text). We use a permissive
|
|
198
|
+
* "line starts with three or more of the same char" rule, ignoring the info
|
|
199
|
+
* string — that's enough to spot when we're mid-block.
|
|
200
|
+
*/
|
|
201
|
+
function endsInsideUnclosedCodeFence(prefix) {
|
|
202
|
+
let openMarker = null;
|
|
203
|
+
for (const rawLine of prefix.split("\n")) {
|
|
204
|
+
const line = rawLine.replace(/^ {0,3}/, "");
|
|
205
|
+
if (openMarker === null) {
|
|
206
|
+
if (line.startsWith("```"))
|
|
207
|
+
openMarker = "`";
|
|
208
|
+
else if (line.startsWith("~~~"))
|
|
209
|
+
openMarker = "~";
|
|
210
|
+
}
|
|
211
|
+
else if (line.startsWith(openMarker.repeat(3))) {
|
|
212
|
+
openMarker = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return openMarker !== null;
|
|
216
|
+
}
|
|
184
217
|
function findStreamingStaticFlushIndex(content) {
|
|
185
218
|
if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
|
|
186
219
|
return -1;
|
|
@@ -190,12 +223,20 @@ function findStreamingStaticFlushIndex(content) {
|
|
|
190
223
|
const search = content.slice(0, upper);
|
|
191
224
|
const paragraphBreak = search.lastIndexOf("\n\n");
|
|
192
225
|
if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
193
|
-
|
|
226
|
+
const splitIndex = paragraphBreak + 2;
|
|
227
|
+
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
228
|
+
return splitIndex;
|
|
229
|
+
}
|
|
194
230
|
}
|
|
195
231
|
const lineBreak = search.lastIndexOf("\n");
|
|
196
232
|
if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
197
|
-
|
|
233
|
+
const splitIndex = lineBreak + 1;
|
|
234
|
+
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
235
|
+
return splitIndex;
|
|
236
|
+
}
|
|
198
237
|
}
|
|
238
|
+
// Inside an open code fence: hold off flushing until the closing fence
|
|
239
|
+
// arrives. The live region grows a bit, but Markdown rendering stays correct.
|
|
199
240
|
return -1;
|
|
200
241
|
}
|
|
201
242
|
function cloneDisplayPart(part) {
|
|
@@ -281,6 +322,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
281
322
|
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
282
323
|
const [pendingFeedback, setPendingFeedback] = useState(null);
|
|
283
324
|
const [pickerMode, setPickerMode] = useState(null);
|
|
325
|
+
const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
|
|
326
|
+
const [composerDraft, setComposerDraft] = useState(null);
|
|
284
327
|
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
285
328
|
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
286
329
|
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
@@ -455,7 +498,6 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
455
498
|
thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
|
|
456
499
|
mode: overrides?.mode ?? agent.mode,
|
|
457
500
|
workingDir: args.cwd,
|
|
458
|
-
skills: safeSkillRegistry?.summaries() ?? [],
|
|
459
501
|
}));
|
|
460
502
|
}, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
|
|
461
503
|
useInput((input, key) => {
|
|
@@ -486,7 +528,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
486
528
|
rebuildSystemPrompt({ thinkingLevel: nextLevel });
|
|
487
529
|
userConfig.setDefaultThinkingLevel(nextLevel);
|
|
488
530
|
setThinkingLevel(nextLevel);
|
|
489
|
-
sessionManager?.
|
|
531
|
+
sessionManager?.updateMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
|
|
490
532
|
sessionManager?.appendMarker("thinking_level_switch", nextLevel);
|
|
491
533
|
return;
|
|
492
534
|
}
|
|
@@ -526,6 +568,19 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
526
568
|
}
|
|
527
569
|
setPickerMode(mode);
|
|
528
570
|
}, []);
|
|
571
|
+
const closePicker = useCallback(() => {
|
|
572
|
+
setPickerMode(null);
|
|
573
|
+
setCursorResetEpoch((epoch) => epoch + 1);
|
|
574
|
+
}, []);
|
|
575
|
+
const fillComposer = useCallback((text) => {
|
|
576
|
+
setComposerDraft((current) => ({
|
|
577
|
+
text,
|
|
578
|
+
epoch: (current?.epoch ?? 0) + 1,
|
|
579
|
+
}));
|
|
580
|
+
}, []);
|
|
581
|
+
const clearComposerDraft = useCallback(() => {
|
|
582
|
+
setComposerDraft(null);
|
|
583
|
+
}, []);
|
|
529
584
|
const openFeedback = useCallback((initialDescription) => {
|
|
530
585
|
const base = collectFeedback(agent, { description: "" });
|
|
531
586
|
const { description: _drop, ...rest } = base;
|
|
@@ -542,7 +597,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
542
597
|
const provider = safeRegistry.getConfigured().find((item) => item.id === providerId);
|
|
543
598
|
if (!provider?.apiKey || !createProvider) {
|
|
544
599
|
addMessage("error", `Provider ${providerId} is not configured or has no active credentials.`);
|
|
545
|
-
|
|
600
|
+
closePicker();
|
|
546
601
|
return;
|
|
547
602
|
}
|
|
548
603
|
const modelId = model.includes(":") ? model.split(":").slice(1).join(":") : model;
|
|
@@ -556,17 +611,16 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
556
611
|
configuredModelId: model,
|
|
557
612
|
thinkingLevel: agent.thinking,
|
|
558
613
|
workingDir: args.cwd,
|
|
559
|
-
skills: safeSkillRegistry?.summaries() ?? [],
|
|
560
614
|
}));
|
|
561
615
|
userConfig.pushRecentModel(model);
|
|
562
616
|
setThinkingLevel(agent.thinking);
|
|
563
|
-
sessionManager?.
|
|
617
|
+
sessionManager?.updateMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
|
|
564
618
|
sessionManager?.appendMarker("model_switch", model);
|
|
565
619
|
addMessage("assistant", `Model switched to ${displayModel(model)}.`);
|
|
566
|
-
|
|
620
|
+
closePicker();
|
|
567
621
|
};
|
|
568
622
|
void run();
|
|
569
|
-
}, [agent, addMessage, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
623
|
+
}, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
570
624
|
const handleProviderSelect = useCallback(async (providerId) => {
|
|
571
625
|
await safeRegistry.prepareProvider(providerId);
|
|
572
626
|
const configured = safeRegistry.getConfigured();
|
|
@@ -574,7 +628,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
574
628
|
const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
|
|
575
629
|
if (!p && !builtin) {
|
|
576
630
|
addMessage("error", `Provider ${providerId} not found.`);
|
|
577
|
-
|
|
631
|
+
closePicker();
|
|
578
632
|
return;
|
|
579
633
|
}
|
|
580
634
|
if (!p?.apiKey) {
|
|
@@ -590,21 +644,21 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
590
644
|
agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
|
|
591
645
|
agent.providerId = providerId;
|
|
592
646
|
addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
|
|
593
|
-
|
|
594
|
-
}, [addMessage, agent, createProvider, safeRegistry]);
|
|
647
|
+
closePicker();
|
|
648
|
+
}, [addMessage, agent, closePicker, createProvider, safeRegistry]);
|
|
595
649
|
const handleProviderAddSelect = useCallback((providerId) => {
|
|
596
650
|
const ok = safeRegistry.addProvider(providerId, "");
|
|
597
651
|
if (!ok) {
|
|
598
652
|
addMessage("error", `Provider ${providerId} could not be added.`);
|
|
599
|
-
|
|
653
|
+
closePicker();
|
|
600
654
|
return;
|
|
601
655
|
}
|
|
602
656
|
safeRegistry.setDefault(providerId);
|
|
603
657
|
setKeyProviderId(providerId);
|
|
604
658
|
setPickerMode("key");
|
|
605
|
-
}, [addMessage, safeRegistry]);
|
|
659
|
+
}, [addMessage, closePicker, safeRegistry]);
|
|
606
660
|
const handleLoginProviderSelect = useCallback(async (providerId) => {
|
|
607
|
-
|
|
661
|
+
closePicker();
|
|
608
662
|
const command = `/login ${providerId}`;
|
|
609
663
|
const { handled, result } = await slashRegistry.execute(command, {
|
|
610
664
|
agent,
|
|
@@ -635,9 +689,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
635
689
|
if (handled && result) {
|
|
636
690
|
addMessage("assistant", result);
|
|
637
691
|
}
|
|
638
|
-
}, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
692
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
639
693
|
const handleLogoutProviderSelect = useCallback(async (providerId) => {
|
|
640
|
-
|
|
694
|
+
closePicker();
|
|
641
695
|
const command = `/logout ${providerId}`;
|
|
642
696
|
const { handled, result } = await slashRegistry.execute(command, {
|
|
643
697
|
agent,
|
|
@@ -668,12 +722,12 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
668
722
|
if (handled && result) {
|
|
669
723
|
addMessage("assistant", result);
|
|
670
724
|
}
|
|
671
|
-
}, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
725
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
672
726
|
const handleKeySubmit = useCallback((key) => {
|
|
673
727
|
const targetId = keyProviderId || safeRegistry.getDefault()?.id;
|
|
674
728
|
if (!targetId) {
|
|
675
729
|
addMessage("error", "No provider selected.");
|
|
676
|
-
|
|
730
|
+
closePicker();
|
|
677
731
|
setKeyProviderId(null);
|
|
678
732
|
return;
|
|
679
733
|
}
|
|
@@ -684,12 +738,13 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
684
738
|
agent.providerId = targetId;
|
|
685
739
|
}
|
|
686
740
|
addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
|
|
687
|
-
|
|
741
|
+
closePicker();
|
|
688
742
|
setKeyProviderId(null);
|
|
689
|
-
}, [addMessage, agent, createProvider, keyProviderId, safeRegistry]);
|
|
743
|
+
}, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
|
|
690
744
|
const handleSubmit = useCallback(async (payload) => {
|
|
691
745
|
const normalized = typeof payload === "string" ? { text: payload, images: [] } : payload;
|
|
692
746
|
const input = normalized.text;
|
|
747
|
+
const displayInput = normalized.displayText ?? input;
|
|
693
748
|
const images = normalized.images;
|
|
694
749
|
if (!input.trim() && images.length === 0)
|
|
695
750
|
return;
|
|
@@ -966,7 +1021,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
966
1021
|
};
|
|
967
1022
|
// Slash commands and skill invocations drop any attached images —
|
|
968
1023
|
// they're meant for pure command routing.
|
|
969
|
-
if (
|
|
1024
|
+
if (displayInput.startsWith("/")) {
|
|
970
1025
|
// Fast-path `/quit` and `/exit` before slash-registry / skill
|
|
971
1026
|
// resolution. This guarantees a literal "/quit" always exits even if
|
|
972
1027
|
// a skill or alias of the same name is later registered. The
|
|
@@ -979,7 +1034,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
979
1034
|
}
|
|
980
1035
|
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
981
1036
|
if (skillInvocation) {
|
|
982
|
-
await runAgentInput(skillInvocation.actualPrompt,
|
|
1037
|
+
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
983
1038
|
return;
|
|
984
1039
|
}
|
|
985
1040
|
const { handled, result, inject } = await slashRegistry.execute(input, {
|
|
@@ -1033,7 +1088,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1033
1088
|
}
|
|
1034
1089
|
}
|
|
1035
1090
|
if (inject) {
|
|
1036
|
-
await runAgentInput(inject,
|
|
1091
|
+
await runAgentInput(inject, displayInput);
|
|
1037
1092
|
}
|
|
1038
1093
|
return;
|
|
1039
1094
|
}
|
|
@@ -1054,7 +1109,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1054
1109
|
})),
|
|
1055
1110
|
]
|
|
1056
1111
|
: expansion.text;
|
|
1057
|
-
await runAgentInput(agentInput,
|
|
1112
|
+
await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
|
|
1058
1113
|
}, [addMessage, agent, args.cwd, openPicker, createProvider, safeRegistry, safeSkillRegistry, updateDisplayMessages]);
|
|
1059
1114
|
const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
1060
1115
|
const keyTarget = keyProviderId
|
|
@@ -1079,7 +1134,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1079
1134
|
const mcpConnectedCount = mcpStates.filter((state) => state.status.kind === "connected").length;
|
|
1080
1135
|
const hasAgentsFile = useMemo(() => existsSync(join(args.cwd, "AGENTS.md")) || existsSync(join(args.cwd, ".bubble", "AGENTS.md")), [args.cwd]);
|
|
1081
1136
|
const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, modelLabel: agent.model ? displayModel(agent.model) : undefined, cwd: friendlyCwd(args.cwd), tips: buildTips(agent, safeRegistry), skillsCount: safeSkillRegistry.summaries().length, mcpConnectedCount: mcpConnectedCount, mcpTotalCount: mcpStates.length, hasAgentsFile: hasAgentsFile })) : null;
|
|
1082
|
-
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, flexShrink: 0, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel:
|
|
1137
|
+
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, flexShrink: 0, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1083
1138
|
.filter((p) => isUserVisibleProvider(p.id))
|
|
1084
1139
|
.map((p) => {
|
|
1085
1140
|
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
@@ -1089,50 +1144,23 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1089
1144
|
name: `${p.name} [${configuredLabel}]`,
|
|
1090
1145
|
enabled: true,
|
|
1091
1146
|
};
|
|
1092
|
-
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel:
|
|
1147
|
+
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1093
1148
|
.filter((p) => isUserVisibleProvider(p.id))
|
|
1094
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel:
|
|
1149
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" })), pickerMode === "login" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1095
1150
|
.filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
|
|
1096
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel:
|
|
1151
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" })), pickerMode === "logout" && (_jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
|
|
1097
1152
|
.filter((p) => safeRegistry.getAuthStorage().has(p.id))
|
|
1098
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel:
|
|
1099
|
-
|
|
1153
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" })), pickerMode === "key" && keyTarget && (_jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
|
|
1154
|
+
closePicker();
|
|
1100
1155
|
setKeyProviderId(null);
|
|
1101
|
-
} })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect:
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
clearMessages,
|
|
1107
|
-
cwd: args.cwd,
|
|
1108
|
-
exit: () => { requestExit(); },
|
|
1109
|
-
sessionManager,
|
|
1110
|
-
createProvider: createProvider ?? (() => {
|
|
1111
|
-
throw new Error("Provider creation not available");
|
|
1112
|
-
}),
|
|
1113
|
-
openPicker,
|
|
1114
|
-
openFeedback,
|
|
1115
|
-
registry: safeRegistry,
|
|
1116
|
-
skillRegistry: safeSkillRegistry,
|
|
1117
|
-
bashAllowlist,
|
|
1118
|
-
settingsManager,
|
|
1119
|
-
lspService,
|
|
1120
|
-
mcpManager,
|
|
1121
|
-
flushMemory,
|
|
1122
|
-
runMemoryCompaction,
|
|
1123
|
-
runMemorySummary,
|
|
1124
|
-
runMemoryRefresh,
|
|
1125
|
-
getThemeMode: () => themeMode,
|
|
1126
|
-
getResolvedTheme: () => themeResolved,
|
|
1127
|
-
setThemeMode: applyThemeMode,
|
|
1128
|
-
});
|
|
1129
|
-
if (handled && result)
|
|
1130
|
-
addMessage("assistant", result);
|
|
1131
|
-
}, onCancel: () => setPickerMode(null) })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1132
|
-
setPickerMode(null);
|
|
1156
|
+
} })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
|
|
1157
|
+
fillComposer(`/${name} `);
|
|
1158
|
+
closePicker();
|
|
1159
|
+
}, onCancel: closePicker })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1160
|
+
closePicker();
|
|
1133
1161
|
addMessage("assistant", summary);
|
|
1134
1162
|
}, onCancel: () => {
|
|
1135
|
-
|
|
1163
|
+
closePicker();
|
|
1136
1164
|
addMessage("assistant", "已取消 Feishu setup。");
|
|
1137
1165
|
} }))] }), todos.length > 0 && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
1138
1166
|
const resolve = pendingPlan.resolve;
|
|
@@ -1161,7 +1189,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1161
1189
|
else if (result.kind === "error") {
|
|
1162
1190
|
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1163
1191
|
}
|
|
1164
|
-
} }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
|
|
1192
|
+
} }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
|
|
1165
1193
|
cwd: args.cwd,
|
|
1166
1194
|
providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
|
|
1167
1195
|
model: displayModel(agent.model) || "no model",
|
|
@@ -1,18 +1,36 @@
|
|
|
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;
|
|
16
34
|
export declare function isCtrlCInput(input: string, key: {
|
|
17
35
|
ctrl?: boolean;
|
|
18
36
|
}): boolean;
|
|
@@ -40,5 +58,8 @@ export declare function insertNewlineAtCursor(text: string, cursor: number): {
|
|
|
40
58
|
text: string;
|
|
41
59
|
cursor: number;
|
|
42
60
|
};
|
|
43
|
-
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;
|
|
44
65
|
export {};
|
|
@@ -14,6 +14,8 @@ const MAX_VISIBLE_LINES = 6;
|
|
|
14
14
|
const PADDING_X = 1;
|
|
15
15
|
const PROMPT = " > ";
|
|
16
16
|
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
17
|
+
const LONG_PASTE_CHAR_THRESHOLD = 1000;
|
|
18
|
+
const LONG_PASTE_LINE_THRESHOLD = 20;
|
|
17
19
|
export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previousOutputHeight) {
|
|
18
20
|
const hadPreviousFrame = previousOutputHeight !== null && previousOutputHeight > 0;
|
|
19
21
|
const isFullscreen = nextOutputHeight >= viewportRows;
|
|
@@ -27,6 +29,11 @@ export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previ
|
|
|
27
29
|
// line below the output, so pass y+1 in those cases.
|
|
28
30
|
return isFullscreen || wasOverflowing || (isOverflowing && hadPreviousFrame) || isLeavingFullscreen;
|
|
29
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
|
+
}
|
|
30
37
|
export function isCtrlCInput(input, key) {
|
|
31
38
|
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
32
39
|
}
|
|
@@ -141,7 +148,42 @@ export function insertNewlineAtCursor(text, cursor) {
|
|
|
141
148
|
cursor: clampedCursor + 1,
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
|
-
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, }) {
|
|
145
187
|
const theme = useTheme();
|
|
146
188
|
const width = terminalColumns;
|
|
147
189
|
const [text, setText] = useState("");
|
|
@@ -149,6 +191,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
149
191
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
150
192
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
151
193
|
const [attachments, setAttachments] = useState([]);
|
|
194
|
+
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
152
195
|
const [history, setHistory] = useState(() => loadHistorySync());
|
|
153
196
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
154
197
|
const historyDraftRef = useRef("");
|
|
@@ -283,7 +326,14 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
283
326
|
const imageTokens = tokens.filter(isImageFilePath);
|
|
284
327
|
if (imageTokens.length === 0) {
|
|
285
328
|
// Plain text paste — insert into the input at the cursor.
|
|
286
|
-
|
|
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
|
+
}
|
|
287
337
|
clearPending();
|
|
288
338
|
return;
|
|
289
339
|
}
|
|
@@ -337,20 +387,28 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
337
387
|
setSelectedIndex(0);
|
|
338
388
|
};
|
|
339
389
|
const submitInput = (submittedText) => {
|
|
340
|
-
|
|
390
|
+
const expandedText = expandPastedContentMarkers(submittedText, pastedContentRefs);
|
|
391
|
+
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
341
392
|
return;
|
|
342
|
-
onSubmit({
|
|
343
|
-
|
|
344
|
-
|
|
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);
|
|
345
402
|
if (nextHistory !== history) {
|
|
346
403
|
setHistory(nextHistory);
|
|
347
|
-
appendHistoryEntry(
|
|
404
|
+
appendHistoryEntry(expandedText);
|
|
348
405
|
}
|
|
349
406
|
}
|
|
350
407
|
setText("");
|
|
351
408
|
setCursor(0);
|
|
352
409
|
setSelectedIndex(0);
|
|
353
410
|
setAttachments([]);
|
|
411
|
+
setPastedContentRefs([]);
|
|
354
412
|
setHistoryIndex(null);
|
|
355
413
|
historyDraftRef.current = "";
|
|
356
414
|
};
|
|
@@ -501,6 +559,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
501
559
|
setHistoryIndex(result.index);
|
|
502
560
|
historyDraftRef.current = result.draft;
|
|
503
561
|
setSelectedIndex(0);
|
|
562
|
+
setPastedContentRefs([]);
|
|
504
563
|
}
|
|
505
564
|
return;
|
|
506
565
|
}
|
|
@@ -516,6 +575,7 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
516
575
|
setHistoryIndex(result.index);
|
|
517
576
|
historyDraftRef.current = result.draft;
|
|
518
577
|
setSelectedIndex(0);
|
|
578
|
+
setPastedContentRefs([]);
|
|
519
579
|
}
|
|
520
580
|
return;
|
|
521
581
|
}
|
|
@@ -537,9 +597,40 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
537
597
|
const previousViewportRowsRef = useRef(null);
|
|
538
598
|
const previousInputFrameSignatureRef = useRef(null);
|
|
539
599
|
const previousRowCompensationRef = useRef(0);
|
|
600
|
+
const lastCursorResetEpochRef = useRef(null);
|
|
601
|
+
const lastDraftEpochRef = useRef(null);
|
|
540
602
|
const lastWidthRef = useRef(null);
|
|
541
603
|
const { setCursorPosition } = useCursor();
|
|
542
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]);
|
|
543
634
|
// After a terminal resize the previous-frame refs reference a layout that no
|
|
544
635
|
// longer exists; carrying them forward makes `needsCursorRowCompensation`
|
|
545
636
|
// compare new yoga heights against stale ones and offsets the cursor by a
|
|
@@ -608,7 +699,6 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
608
699
|
// user can't type. Keeping the real cursor visible in the input makes it
|
|
609
700
|
// flicker every time streaming output above it re-lays out the frame, so
|
|
610
701
|
// we hide it entirely until input is active again.
|
|
611
|
-
const [cursorTick, setCursorTick] = useState(0);
|
|
612
702
|
useLayoutEffect(() => {
|
|
613
703
|
let node = cursorLineRef.current ?? undefined;
|
|
614
704
|
if (!node?.yogaNode) {
|
|
@@ -644,9 +734,13 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
|
|
|
644
734
|
const sameRenderedFrame = previousOutputHeight === rootHeight &&
|
|
645
735
|
previousViewportRowsRef.current === viewportRows &&
|
|
646
736
|
previousInputFrameSignatureRef.current === inputFrameSignature;
|
|
647
|
-
const rowCompensation =
|
|
648
|
-
|
|
649
|
-
:
|
|
737
|
+
const rowCompensation = resolveCursorRowCompensation({
|
|
738
|
+
sameRenderedFrame,
|
|
739
|
+
previousRowCompensation: previousRowCompensationRef.current,
|
|
740
|
+
nextOutputHeight: rootHeight,
|
|
741
|
+
viewportRows,
|
|
742
|
+
previousOutputHeight,
|
|
743
|
+
});
|
|
650
744
|
previousOutputHeightRef.current = rootHeight;
|
|
651
745
|
previousViewportRowsRef.current = viewportRows;
|
|
652
746
|
previousInputFrameSignatureRef.current = inputFrameSignature;
|
|
@@ -59,7 +59,15 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
|
|
|
59
59
|
const visibleParts = deferredParts.length > 0
|
|
60
60
|
? deferredParts
|
|
61
61
|
: fallbackStreamingParts(deferredContent, tools);
|
|
62
|
-
return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", children: [deferredReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: deferredReasoning }) })), visibleParts.length > 0 && (
|
|
63
|
+
// marginTop intentionally 0: this Box only mounts on the first non-empty
|
|
64
|
+
// streaming frame, so a marginTop=1 here would visibly insert a blank
|
|
65
|
+
// line under the user message right at that moment (the "spinner sits
|
|
66
|
+
// close, then content appears with a sudden gap, then spinner slides
|
|
67
|
+
// down" effect users perceive as flicker on the DOM xterm renderer).
|
|
68
|
+
// marginBottom=1 stays so streamed text doesn't collide with the
|
|
69
|
+
// WaitingIndicator rendered below.
|
|
70
|
+
_jsx(Box, { marginTop: 0, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
|
|
63
71
|
}
|
|
64
72
|
function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
|
|
65
73
|
const lastToolsPartIndex = findLastToolsPartIndex(parts);
|
|
@@ -201,9 +209,10 @@ function ReasoningTraceBlock({ reasoning }) {
|
|
|
201
209
|
}
|
|
202
210
|
function CompactionSummaryBlock({ message }) {
|
|
203
211
|
const theme = useTheme();
|
|
204
|
-
const
|
|
212
|
+
const rawStatus = message.content.replace(/^✓\s*/, "").trim();
|
|
213
|
+
const status = rawStatus.replace(/^Compaction complete\s*(?:·\s*)?/i, "").trim() || "Session compacted";
|
|
205
214
|
const summary = message.compactionSummary?.trim();
|
|
206
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction" }), _jsxs(Text, { color: theme.muted, children: ["
|
|
215
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
|
|
207
216
|
}
|
|
208
217
|
function UserMessageBlock({ content, terminalColumns }) {
|
|
209
218
|
const theme = useTheme();
|