@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.
Files changed (39) hide show
  1. package/dist/agent.js +1 -2
  2. package/dist/feishu/agent-host/run-driver.js +13 -6
  3. package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
  4. package/dist/feishu/router/commands.js +2 -1
  5. package/dist/feishu/scope/session-binder.js +1 -1
  6. package/dist/feishu/serve.js +3 -3
  7. package/dist/main.js +20 -3
  8. package/dist/prompt/compose.js +3 -3
  9. package/dist/prompt/environment.js +2 -0
  10. package/dist/prompt/reminders.js +1 -1
  11. package/dist/provider-openai-codex.d.ts +8 -1
  12. package/dist/provider-openai-codex.js +33 -9
  13. package/dist/provider.d.ts +2 -0
  14. package/dist/session-title.d.ts +16 -0
  15. package/dist/session-title.js +134 -0
  16. package/dist/session-types.d.ts +5 -0
  17. package/dist/session.d.ts +5 -0
  18. package/dist/session.js +75 -9
  19. package/dist/skills/invocation.js +0 -18
  20. package/dist/skills/registry.d.ts +1 -0
  21. package/dist/skills/registry.js +2 -0
  22. package/dist/slash-commands/commands.js +2 -22
  23. package/dist/slash-commands/registry.js +1 -1
  24. package/dist/text-display.d.ts +3 -0
  25. package/dist/text-display.js +25 -0
  26. package/dist/tools/index.d.ts +1 -0
  27. package/dist/tools/index.js +3 -1
  28. package/dist/tools/skill-search.d.ts +10 -0
  29. package/dist/tools/skill-search.js +134 -0
  30. package/dist/tools/skill.js +1 -4
  31. package/dist/tui-ink/app.js +54 -65
  32. package/dist/tui-ink/input-box.d.ts +22 -1
  33. package/dist/tui-ink/input-box.js +105 -11
  34. package/dist/tui-ink/message-list.js +3 -2
  35. package/dist/tui-ink/model-picker.d.ts +18 -0
  36. package/dist/tui-ink/model-picker.js +80 -23
  37. package/dist/tui-ink/session-picker.js +5 -7
  38. package/dist/tui-ink/theme.js +2 -2
  39. package/package.json +1 -1
@@ -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" ? m.content : "(multimedia)",
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?.setMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
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
- setPickerMode(null);
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?.setMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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
- setPickerMode(null);
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 (input.startsWith("/")) {
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, input);
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, input);
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, input, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
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: () => setPickerMode(null) })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
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: () => setPickerMode(null) })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
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: () => setPickerMode(null), title: "Add Provider" })), pickerMode === "login" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
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: () => setPickerMode(null), title: "Select Login Provider" })), pickerMode === "logout" && (_jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
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: () => setPickerMode(null), title: "Select Logout Provider" })), pickerMode === "key" && keyTarget && (_jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
1099
- setPickerMode(null);
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: async (name) => {
1102
- setPickerMode(null);
1103
- const { handled, result } = await slashRegistry.execute(`/skill ${name}`, {
1104
- agent,
1105
- addMessage,
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
- setPickerMode(null);
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 InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, terminalColumns, cwd }: InputBoxProps): import("react/jsx-runtime").JSX.Element;
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 InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, terminalColumns, cwd }) {
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
- insertTextAtCursor(clean);
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
- if (submittedText.trim().length === 0 && attachments.length === 0)
390
+ const expandedText = expandPastedContentMarkers(submittedText, pastedContentRefs);
391
+ if (expandedText.trim().length === 0 && attachments.length === 0)
341
392
  return;
342
- onSubmit({ text: submittedText, images: attachments });
343
- if (submittedText.trim().length > 0) {
344
- const nextHistory = pushHistoryEntry(history, submittedText);
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(submittedText);
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 = sameRenderedFrame
648
- ? previousRowCompensationRef.current
649
- : needsCursorRowCompensation(rootHeight, viewportRows, previousOutputHeight) ? 1 : 0;
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 status = message.content.replace(/^✓\s*/, "").trim() || "Session compacted";
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: [" \u00B7 ", status] })] }), summary && (_jsx(Box, { marginTop: 1, paddingLeft: 3, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) }))] }));
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;