@bubblebrain-ai/bubble 0.0.11 → 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.js +1 -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 +20 -3
- 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 +2 -22
- package/dist/slash-commands/registry.js +1 -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 +54 -65
- 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 +3 -2
- 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.js +2 -2
- package/package.json +1 -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") {
|
|
@@ -281,6 +283,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
281
283
|
const [pendingQuestion, setPendingQuestion] = useState(null);
|
|
282
284
|
const [pendingFeedback, setPendingFeedback] = useState(null);
|
|
283
285
|
const [pickerMode, setPickerMode] = useState(null);
|
|
286
|
+
const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
|
|
287
|
+
const [composerDraft, setComposerDraft] = useState(null);
|
|
284
288
|
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
285
289
|
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
286
290
|
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
@@ -455,7 +459,6 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
455
459
|
thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
|
|
456
460
|
mode: overrides?.mode ?? agent.mode,
|
|
457
461
|
workingDir: args.cwd,
|
|
458
|
-
skills: safeSkillRegistry?.summaries() ?? [],
|
|
459
462
|
}));
|
|
460
463
|
}, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
|
|
461
464
|
useInput((input, key) => {
|
|
@@ -486,7 +489,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
486
489
|
rebuildSystemPrompt({ thinkingLevel: nextLevel });
|
|
487
490
|
userConfig.setDefaultThinkingLevel(nextLevel);
|
|
488
491
|
setThinkingLevel(nextLevel);
|
|
489
|
-
sessionManager?.
|
|
492
|
+
sessionManager?.updateMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
|
|
490
493
|
sessionManager?.appendMarker("thinking_level_switch", nextLevel);
|
|
491
494
|
return;
|
|
492
495
|
}
|
|
@@ -526,6 +529,19 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
526
529
|
}
|
|
527
530
|
setPickerMode(mode);
|
|
528
531
|
}, []);
|
|
532
|
+
const closePicker = useCallback(() => {
|
|
533
|
+
setPickerMode(null);
|
|
534
|
+
setCursorResetEpoch((epoch) => epoch + 1);
|
|
535
|
+
}, []);
|
|
536
|
+
const fillComposer = useCallback((text) => {
|
|
537
|
+
setComposerDraft((current) => ({
|
|
538
|
+
text,
|
|
539
|
+
epoch: (current?.epoch ?? 0) + 1,
|
|
540
|
+
}));
|
|
541
|
+
}, []);
|
|
542
|
+
const clearComposerDraft = useCallback(() => {
|
|
543
|
+
setComposerDraft(null);
|
|
544
|
+
}, []);
|
|
529
545
|
const openFeedback = useCallback((initialDescription) => {
|
|
530
546
|
const base = collectFeedback(agent, { description: "" });
|
|
531
547
|
const { description: _drop, ...rest } = base;
|
|
@@ -542,7 +558,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
542
558
|
const provider = safeRegistry.getConfigured().find((item) => item.id === providerId);
|
|
543
559
|
if (!provider?.apiKey || !createProvider) {
|
|
544
560
|
addMessage("error", `Provider ${providerId} is not configured or has no active credentials.`);
|
|
545
|
-
|
|
561
|
+
closePicker();
|
|
546
562
|
return;
|
|
547
563
|
}
|
|
548
564
|
const modelId = model.includes(":") ? model.split(":").slice(1).join(":") : model;
|
|
@@ -556,17 +572,16 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
556
572
|
configuredModelId: model,
|
|
557
573
|
thinkingLevel: agent.thinking,
|
|
558
574
|
workingDir: args.cwd,
|
|
559
|
-
skills: safeSkillRegistry?.summaries() ?? [],
|
|
560
575
|
}));
|
|
561
576
|
userConfig.pushRecentModel(model);
|
|
562
577
|
setThinkingLevel(agent.thinking);
|
|
563
|
-
sessionManager?.
|
|
578
|
+
sessionManager?.updateMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
|
|
564
579
|
sessionManager?.appendMarker("model_switch", model);
|
|
565
580
|
addMessage("assistant", `Model switched to ${displayModel(model)}.`);
|
|
566
|
-
|
|
581
|
+
closePicker();
|
|
567
582
|
};
|
|
568
583
|
void run();
|
|
569
|
-
}, [agent, addMessage, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
584
|
+
}, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
|
|
570
585
|
const handleProviderSelect = useCallback(async (providerId) => {
|
|
571
586
|
await safeRegistry.prepareProvider(providerId);
|
|
572
587
|
const configured = safeRegistry.getConfigured();
|
|
@@ -574,7 +589,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
574
589
|
const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
|
|
575
590
|
if (!p && !builtin) {
|
|
576
591
|
addMessage("error", `Provider ${providerId} not found.`);
|
|
577
|
-
|
|
592
|
+
closePicker();
|
|
578
593
|
return;
|
|
579
594
|
}
|
|
580
595
|
if (!p?.apiKey) {
|
|
@@ -590,21 +605,21 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
590
605
|
agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
|
|
591
606
|
agent.providerId = providerId;
|
|
592
607
|
addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
|
|
593
|
-
|
|
594
|
-
}, [addMessage, agent, createProvider, safeRegistry]);
|
|
608
|
+
closePicker();
|
|
609
|
+
}, [addMessage, agent, closePicker, createProvider, safeRegistry]);
|
|
595
610
|
const handleProviderAddSelect = useCallback((providerId) => {
|
|
596
611
|
const ok = safeRegistry.addProvider(providerId, "");
|
|
597
612
|
if (!ok) {
|
|
598
613
|
addMessage("error", `Provider ${providerId} could not be added.`);
|
|
599
|
-
|
|
614
|
+
closePicker();
|
|
600
615
|
return;
|
|
601
616
|
}
|
|
602
617
|
safeRegistry.setDefault(providerId);
|
|
603
618
|
setKeyProviderId(providerId);
|
|
604
619
|
setPickerMode("key");
|
|
605
|
-
}, [addMessage, safeRegistry]);
|
|
620
|
+
}, [addMessage, closePicker, safeRegistry]);
|
|
606
621
|
const handleLoginProviderSelect = useCallback(async (providerId) => {
|
|
607
|
-
|
|
622
|
+
closePicker();
|
|
608
623
|
const command = `/login ${providerId}`;
|
|
609
624
|
const { handled, result } = await slashRegistry.execute(command, {
|
|
610
625
|
agent,
|
|
@@ -635,9 +650,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
635
650
|
if (handled && result) {
|
|
636
651
|
addMessage("assistant", result);
|
|
637
652
|
}
|
|
638
|
-
}, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
653
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
639
654
|
const handleLogoutProviderSelect = useCallback(async (providerId) => {
|
|
640
|
-
|
|
655
|
+
closePicker();
|
|
641
656
|
const command = `/logout ${providerId}`;
|
|
642
657
|
const { handled, result } = await slashRegistry.execute(command, {
|
|
643
658
|
agent,
|
|
@@ -668,12 +683,12 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
668
683
|
if (handled && result) {
|
|
669
684
|
addMessage("assistant", result);
|
|
670
685
|
}
|
|
671
|
-
}, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
686
|
+
}, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
|
|
672
687
|
const handleKeySubmit = useCallback((key) => {
|
|
673
688
|
const targetId = keyProviderId || safeRegistry.getDefault()?.id;
|
|
674
689
|
if (!targetId) {
|
|
675
690
|
addMessage("error", "No provider selected.");
|
|
676
|
-
|
|
691
|
+
closePicker();
|
|
677
692
|
setKeyProviderId(null);
|
|
678
693
|
return;
|
|
679
694
|
}
|
|
@@ -684,12 +699,13 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
684
699
|
agent.providerId = targetId;
|
|
685
700
|
}
|
|
686
701
|
addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
|
|
687
|
-
|
|
702
|
+
closePicker();
|
|
688
703
|
setKeyProviderId(null);
|
|
689
|
-
}, [addMessage, agent, createProvider, keyProviderId, safeRegistry]);
|
|
704
|
+
}, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
|
|
690
705
|
const handleSubmit = useCallback(async (payload) => {
|
|
691
706
|
const normalized = typeof payload === "string" ? { text: payload, images: [] } : payload;
|
|
692
707
|
const input = normalized.text;
|
|
708
|
+
const displayInput = normalized.displayText ?? input;
|
|
693
709
|
const images = normalized.images;
|
|
694
710
|
if (!input.trim() && images.length === 0)
|
|
695
711
|
return;
|
|
@@ -966,7 +982,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
966
982
|
};
|
|
967
983
|
// Slash commands and skill invocations drop any attached images —
|
|
968
984
|
// they're meant for pure command routing.
|
|
969
|
-
if (
|
|
985
|
+
if (displayInput.startsWith("/")) {
|
|
970
986
|
// Fast-path `/quit` and `/exit` before slash-registry / skill
|
|
971
987
|
// resolution. This guarantees a literal "/quit" always exits even if
|
|
972
988
|
// a skill or alias of the same name is later registered. The
|
|
@@ -979,7 +995,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
979
995
|
}
|
|
980
996
|
const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
|
|
981
997
|
if (skillInvocation) {
|
|
982
|
-
await runAgentInput(skillInvocation.actualPrompt,
|
|
998
|
+
await runAgentInput(skillInvocation.actualPrompt, displayInput);
|
|
983
999
|
return;
|
|
984
1000
|
}
|
|
985
1001
|
const { handled, result, inject } = await slashRegistry.execute(input, {
|
|
@@ -1033,7 +1049,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1033
1049
|
}
|
|
1034
1050
|
}
|
|
1035
1051
|
if (inject) {
|
|
1036
|
-
await runAgentInput(inject,
|
|
1052
|
+
await runAgentInput(inject, displayInput);
|
|
1037
1053
|
}
|
|
1038
1054
|
return;
|
|
1039
1055
|
}
|
|
@@ -1054,7 +1070,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1054
1070
|
})),
|
|
1055
1071
|
]
|
|
1056
1072
|
: expansion.text;
|
|
1057
|
-
await runAgentInput(agentInput,
|
|
1073
|
+
await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
|
|
1058
1074
|
}, [addMessage, agent, args.cwd, openPicker, createProvider, safeRegistry, safeSkillRegistry, updateDisplayMessages]);
|
|
1059
1075
|
const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
|
|
1060
1076
|
const keyTarget = keyProviderId
|
|
@@ -1079,7 +1095,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1079
1095
|
const mcpConnectedCount = mcpStates.filter((state) => state.status.kind === "connected").length;
|
|
1080
1096
|
const hasAgentsFile = useMemo(() => existsSync(join(args.cwd, "AGENTS.md")) || existsSync(join(args.cwd, ".bubble", "AGENTS.md")), [args.cwd]);
|
|
1081
1097
|
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:
|
|
1098
|
+
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
1099
|
.filter((p) => isUserVisibleProvider(p.id))
|
|
1084
1100
|
.map((p) => {
|
|
1085
1101
|
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
@@ -1089,50 +1105,23 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1089
1105
|
name: `${p.name} [${configuredLabel}]`,
|
|
1090
1106
|
enabled: true,
|
|
1091
1107
|
};
|
|
1092
|
-
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel:
|
|
1108
|
+
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1093
1109
|
.filter((p) => isUserVisibleProvider(p.id))
|
|
1094
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel:
|
|
1110
|
+
.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
1111
|
.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:
|
|
1112
|
+
.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
1113
|
.filter((p) => safeRegistry.getAuthStorage().has(p.id))
|
|
1098
|
-
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel:
|
|
1099
|
-
|
|
1114
|
+
.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: () => {
|
|
1115
|
+
closePicker();
|
|
1100
1116
|
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);
|
|
1117
|
+
} })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
|
|
1118
|
+
fillComposer(`/${name} `);
|
|
1119
|
+
closePicker();
|
|
1120
|
+
}, onCancel: closePicker })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1121
|
+
closePicker();
|
|
1133
1122
|
addMessage("assistant", summary);
|
|
1134
1123
|
}, onCancel: () => {
|
|
1135
|
-
|
|
1124
|
+
closePicker();
|
|
1136
1125
|
addMessage("assistant", "已取消 Feishu setup。");
|
|
1137
1126
|
} }))] }), 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
1127
|
const resolve = pendingPlan.resolve;
|
|
@@ -1161,7 +1150,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1161
1150
|
else if (result.kind === "error") {
|
|
1162
1151
|
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1163
1152
|
}
|
|
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({
|
|
1153
|
+
} }) })), !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
1154
|
cwd: args.cwd,
|
|
1166
1155
|
providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
|
|
1167
1156
|
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;
|
|
@@ -201,9 +201,10 @@ function ReasoningTraceBlock({ reasoning }) {
|
|
|
201
201
|
}
|
|
202
202
|
function CompactionSummaryBlock({ message }) {
|
|
203
203
|
const theme = useTheme();
|
|
204
|
-
const
|
|
204
|
+
const rawStatus = message.content.replace(/^✓\s*/, "").trim();
|
|
205
|
+
const status = rawStatus.replace(/^Compaction complete\s*(?:·\s*)?/i, "").trim() || "Session compacted";
|
|
205
206
|
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: ["
|
|
207
|
+
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
208
|
}
|
|
208
209
|
function UserMessageBlock({ content, terminalColumns }) {
|
|
209
210
|
const theme = useTheme();
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { ProviderRegistry } from "../provider-registry.js";
|
|
2
|
+
export { padVisual, truncateVisual } from "../text-display.js";
|
|
2
3
|
export interface ModelPickerOption {
|
|
3
4
|
id: string;
|
|
4
5
|
label: string;
|
|
5
6
|
group: string;
|
|
6
7
|
providerBadge: string;
|
|
7
8
|
}
|
|
9
|
+
export type PickerKeyAction = "up" | "down" | "enter" | "escape" | "backspace" | "delete";
|
|
10
|
+
export declare function resolvePickerKeyAction(input: string, key: {
|
|
11
|
+
upArrow?: boolean;
|
|
12
|
+
downArrow?: boolean;
|
|
13
|
+
return?: boolean;
|
|
14
|
+
escape?: boolean;
|
|
15
|
+
backspace?: boolean;
|
|
16
|
+
delete?: boolean;
|
|
17
|
+
}): PickerKeyAction | undefined;
|
|
18
|
+
export declare function isPrintablePickerInput(input: string): boolean;
|
|
19
|
+
export declare function formatSkillPickerRow(skill: {
|
|
20
|
+
name: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}, options: {
|
|
23
|
+
selected: boolean;
|
|
24
|
+
width: number;
|
|
25
|
+
}): string;
|
|
8
26
|
export interface ModelPickerProps {
|
|
9
27
|
registry: ProviderRegistry;
|
|
10
28
|
current: string;
|