xibecode 1.3.7 → 1.3.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.
@@ -23,6 +23,7 @@ import { loadImageAttachment, mimeFromExtension } from '../utils/image-attachmen
23
23
  import { SessionManager } from 'xibecode-core';
24
24
  import { AutoMemoryManager, HooksManager, SettingsManager as CoreSettingsManager } from 'xibecode-core';
25
25
  import { cloudPullCommand } from '../commands/cloud-pull.js';
26
+ import { listWorkspaceFiles } from '../utils/list-files.js';
26
27
  import { attachRemoteExecution, codingToolExecutorRemoteOptions, getRuntimeStatusLabel, remoteToolSandboxIdForAgent, remoteToolWorkspaceRootForAgent, resolveRemoteExecutionConfig, } from '../utils/remote-execution.js';
27
28
  import { syncWorkspaceToSandbox } from '../utils/sandbox-sync.js';
28
29
  import { withCloudWorkspaceSyncSpinner } from '../utils/cloud-sync-feedback.js';
@@ -96,6 +97,52 @@ function summarizeToolResultContent(content) {
96
97
  }
97
98
  return raw.replace(/\s+/g, ' ').trim().slice(0, 300);
98
99
  }
100
+ /** Mathem. brackets — wrap @-file picks so each pick is one immutable token */
101
+ const AT_MENTION_OPEN = '\u27e6'; // ⟦
102
+ const AT_MENTION_CLOSE = '\u27e7'; // ⟧
103
+ /** Drop unknown ⟦…⟧ groups; keep only exact picker-produced tokens still registered in `locked` */
104
+ function reconcileTaggedAtMentions(next, locked) {
105
+ let out = '';
106
+ let i = 0;
107
+ while (i < next.length) {
108
+ if (next.startsWith(AT_MENTION_OPEN, i)) {
109
+ const closeIdx = next.indexOf(AT_MENTION_CLOSE, i + AT_MENTION_OPEN.length);
110
+ if (closeIdx === -1) {
111
+ return out;
112
+ }
113
+ const block = next.slice(i, closeIdx + AT_MENTION_CLOSE.length);
114
+ if (locked.current.has(block)) {
115
+ out += block;
116
+ }
117
+ i = closeIdx + AT_MENTION_CLOSE.length;
118
+ continue;
119
+ }
120
+ out += next[i];
121
+ i += 1;
122
+ }
123
+ for (const t of [...locked.current]) {
124
+ if (!out.includes(t))
125
+ locked.current.delete(t);
126
+ }
127
+ return out;
128
+ }
129
+ /** Strip pick wrappers before commands / history / prompts — core expects plain `@path` */
130
+ function flattenTaggedAtMentions(s) {
131
+ return s.replace(/\u27e6(@[A-Za-z0-9._\-\/]+)\u27e7/g, (_, inner) => {
132
+ let body = inner.slice(1);
133
+ if (body.startsWith('/'))
134
+ body = body.slice(1);
135
+ return `@${body}`;
136
+ });
137
+ }
138
+ /** Inserted path for dirs: `/relative/` — files stay repo-relative without a leading slash */
139
+ function mentionPathForPick(entry) {
140
+ if (entry.isDirectory) {
141
+ const trimmed = entry.relativePath.replace(/\/+$/, '');
142
+ return trimmed ? `/${trimmed}/` : '/';
143
+ }
144
+ return entry.relativePath;
145
+ }
99
146
  function transcriptLinesFromMessage(message) {
100
147
  if (typeof message.content === 'string') {
101
148
  const text = message.content.trim();
@@ -155,7 +202,7 @@ const CHAT_COMMANDS = [
155
202
  { name: '/config', description: 'Show current config and quick config hints' },
156
203
  { name: '/memory', description: 'Show auto-memories for this project' },
157
204
  { name: '/hooks', description: 'Show registered lifecycle hooks' },
158
- { name: '/cpull', description: 'Pull current cloud sandbox changes locally (supports --apply)' },
205
+ { name: '/cpull', description: 'Pull sandbox workspace; /cpull --apply merges only new/changed files (use --full to replace all)' },
159
206
  { name: '/commit', description: 'Stage all changes and commit (auto message or custom text)' },
160
207
  { name: '/donate', description: 'Open the donation page in your browser' },
161
208
  { name: '/sponsor', description: 'Open the sponsorship page in your browser' },
@@ -217,11 +264,15 @@ function prefixForType(type) {
217
264
  return 'Info';
218
265
  }
219
266
  }
220
- function prefixColorKey(type) {
267
+ function prefixColorKey(type, mode) {
221
268
  switch (type) {
222
269
  case 'user':
223
270
  return 'briefLabelYou';
224
271
  case 'assistant':
272
+ if (mode === 'plan')
273
+ return 'briefLabelPlan';
274
+ if (mode === 'review')
275
+ return 'briefLabelReview';
225
276
  return 'briefLabelClaude';
226
277
  case 'tool':
227
278
  return 'suggestion';
@@ -262,6 +313,7 @@ function XibeCodeChatApp(props) {
262
313
  const [setupSelectedModelIndex, setSetupSelectedModelIndex] = useState(0);
263
314
  const [setupProviderPickerOpen, setSetupProviderPickerOpen] = useState(false);
264
315
  const [setupProviderIndex, setSetupProviderIndex] = useState(0);
316
+ const [setupProvider, setSetupProvider] = useState(null);
265
317
  const CONFIG_MENU = [
266
318
  { label: 'Set Base URL (OpenAI format)', value: 'set_baseurl', description: 'Example: https://api.openai.com/v1' },
267
319
  { label: 'Set API key', value: 'set_apikey', description: 'Bearer token used for /models and chat calls' },
@@ -279,6 +331,15 @@ function XibeCodeChatApp(props) {
279
331
  const [configProviderIndex, setConfigProviderIndex] = useState(0);
280
332
  const [configCostModePickerOpen, setConfigCostModePickerOpen] = useState(false);
281
333
  const [configCostModeIndex, setConfigCostModeIndex] = useState(0);
334
+ // File picker state (@-triggered)
335
+ const [filePickerOpen, setFilePickerOpen] = useState(false);
336
+ const [filteredFileEntries, setFilteredFileEntries] = useState([]);
337
+ const [selectedFileIndex, setSelectedFileIndex] = useState(0);
338
+ const [fileQuery, setFileQuery] = useState('');
339
+ const [atPos, setAtPos] = useState(-1);
340
+ const [filePickerLoading, setFilePickerLoading] = useState(false);
341
+ /** Bump when the file picker inserts text so Ink TextInput remounts with the caret at the end */
342
+ const [chatInputMountKey, setChatInputMountKey] = useState(0);
282
343
  const [questionsState, setQuestionsState] = useState(null);
283
344
  const [workSpinnerFrame, setWorkSpinnerFrame] = useState(0);
284
345
  const [workVerbIndex, setWorkVerbIndex] = useState(0);
@@ -322,6 +383,9 @@ function XibeCodeChatApp(props) {
322
383
  // Keep a much larger in-memory transcript so context doesn't vanish quickly.
323
384
  setLines((prev) => [...prev.slice(-5000), withId]);
324
385
  }, [props]);
386
+ const handleChatInputChange = useCallback((next) => {
387
+ setInput(reconcileTaggedAtMentions(next, lockedPickTagsRef));
388
+ }, []);
325
389
  useEffect(() => {
326
390
  props.registerUiSink?.(pushLine);
327
391
  }, [props, pushLine]);
@@ -341,6 +405,12 @@ function XibeCodeChatApp(props) {
341
405
  const lastVisibleOutputAtRef = useRef(Date.now());
342
406
  const currentPromptRef = useRef(null);
343
407
  const restartAttemptsRef = useRef(0);
408
+ const promptHistoryRef = useRef([]);
409
+ const historyIndexRef = useRef(-1);
410
+ const savedInputRef = useRef('');
411
+ const cachedFileEntriesRef = useRef([]);
412
+ /** Exact ⟦@path⟧ strings inserted via the file picker — edits inside remove the whole token */
413
+ const lockedPickTagsRef = useRef(new Set());
344
414
  const sessionMessagesRef = useRef(props.initialMessages || []);
345
415
  const lastBgLineByTask = useRef(new Map());
346
416
  useEffect(() => {
@@ -433,6 +503,64 @@ function XibeCodeChatApp(props) {
433
503
  }, WORK_VERB_ROTATE_MS);
434
504
  return () => clearInterval(id);
435
505
  }, [isRunning]);
506
+ // Load workspace file list once on mount for @-picker
507
+ useEffect(() => {
508
+ let cancelled = false;
509
+ setFilePickerLoading(true);
510
+ (async () => {
511
+ try {
512
+ const entries = await listWorkspaceFiles(process.cwd());
513
+ if (!cancelled) {
514
+ cachedFileEntriesRef.current = entries;
515
+ }
516
+ }
517
+ catch {
518
+ // silently fail
519
+ }
520
+ finally {
521
+ if (!cancelled)
522
+ setFilePickerLoading(false);
523
+ }
524
+ })();
525
+ return () => { cancelled = true; };
526
+ }, []);
527
+ // Detect '@' in input to open file picker
528
+ useEffect(() => {
529
+ const lastAtIndex = input.lastIndexOf('@');
530
+ if (lastAtIndex === -1) {
531
+ setFilePickerOpen(false);
532
+ setSelectedFileIndex(0);
533
+ return;
534
+ }
535
+ // '@' must be at a word boundary
536
+ if (lastAtIndex > 0) {
537
+ const prev = input[lastAtIndex - 1];
538
+ if (prev !== ' ' && prev !== '(' && prev !== AT_MENTION_CLOSE) {
539
+ setFilePickerOpen(false);
540
+ setSelectedFileIndex(0);
541
+ return;
542
+ }
543
+ }
544
+ const afterAt = input.slice(lastAtIndex + 1);
545
+ // Space ends the active @-mention — hide suggestions (typing past first word breaks mention mode)
546
+ if (afterAt.includes(' ')) {
547
+ setFilePickerOpen(false);
548
+ setSelectedFileIndex(0);
549
+ return;
550
+ }
551
+ const query = afterAt;
552
+ setAtPos(lastAtIndex);
553
+ setFileQuery(query);
554
+ setFilePickerOpen(true);
555
+ const lowerQuery = query.toLowerCase();
556
+ const filtered = cachedFileEntriesRef.current.filter((e) => {
557
+ const rel = e.relativePath.toLowerCase();
558
+ const label = mentionPathForPick(e).toLowerCase();
559
+ return rel.includes(lowerQuery) || label.includes(lowerQuery);
560
+ });
561
+ setFilteredFileEntries(filtered);
562
+ setSelectedFileIndex((prev) => Math.min(prev, Math.max(0, filtered.length - 1)));
563
+ }, [input]);
436
564
  const ensureModelsLoaded = useCallback(async () => {
437
565
  if (availableModels.length > 0) {
438
566
  return availableModels;
@@ -487,6 +615,7 @@ function XibeCodeChatApp(props) {
487
615
  setSetupSelectedModelIndex(0);
488
616
  setSetupProviderPickerOpen(true);
489
617
  setSetupProviderIndex(0);
618
+ setSetupProvider(null);
490
619
  pushLine({ type: 'info', text: 'Setup started.' });
491
620
  }, [pushLine]);
492
621
  useEffect(() => {
@@ -603,17 +732,22 @@ function XibeCodeChatApp(props) {
603
732
  if (questionsState) {
604
733
  return;
605
734
  }
735
+ // If file picker is open, ignore Enter (handled by useInput)
736
+ if (filePickerOpen) {
737
+ return;
738
+ }
739
+ const flat = flattenTaggedAtMentions(trimmed);
606
740
  if (isRunning) {
607
- queuedPromptRef.current = trimmed;
741
+ queuedPromptRef.current = flat;
608
742
  setInput('');
609
743
  pushLine({ type: 'info', text: 'Queued message — will run next.' });
610
744
  return;
611
745
  }
612
- const commandMatches = CHAT_COMMANDS.filter((command) => command.name.toLowerCase().startsWith(trimmed.toLowerCase()));
613
- const exactMatch = CHAT_COMMANDS.some((command) => command.name.toLowerCase() === trimmed.toLowerCase());
614
- const resolvedInput = trimmed.startsWith('/') && !exactMatch && commandMatches[selectedCommandIndex]
746
+ const commandMatches = CHAT_COMMANDS.filter((command) => command.name.toLowerCase().startsWith(flat.toLowerCase()));
747
+ const exactMatch = CHAT_COMMANDS.some((command) => command.name.toLowerCase() === flat.toLowerCase());
748
+ const resolvedInput = flat.startsWith('/') && !exactMatch && commandMatches[selectedCommandIndex]
615
749
  ? commandMatches[selectedCommandIndex].name
616
- : trimmed;
750
+ : flat;
617
751
  setInput('');
618
752
  // Setup wizard input capture
619
753
  if (setupStep !== 'idle') {
@@ -876,6 +1010,29 @@ function XibeCodeChatApp(props) {
876
1010
  }
877
1011
  return;
878
1012
  }
1013
+ // Intercept natural language mode-switching commands (e.g., "switch to review mode", "switch to review model", "change to plan mode")
1014
+ const modeSwitchRegex = /^(?:please\s+)?(?:switch|change|go|use|activate|turn\s+on)\s+(?:to\s+(?:the\s+)?|mode\s+to\s+)?(agent|plan|review|debugger|tester|security|pentest|team_leader|seo|product|architect|engineer|data|researcher)\s*(?:mode|model|persona)?(?:\s+please)?[\s.!?]*$/i;
1015
+ const matchMode = resolvedInput.match(modeSwitchRegex);
1016
+ if (matchMode) {
1017
+ const modeArg = matchMode[1].toLowerCase();
1018
+ const selectedMode = props.modeOptions.find((mode) => mode.id === modeArg);
1019
+ if (selectedMode) {
1020
+ try {
1021
+ await applyMode(selectedMode.id);
1022
+ }
1023
+ catch (error) {
1024
+ const message = error instanceof Error ? error.message : 'Failed to switch mode';
1025
+ pushLine({ type: 'error', text: message });
1026
+ }
1027
+ }
1028
+ else {
1029
+ pushLine({
1030
+ type: 'error',
1031
+ text: `Mode "${modeArg}" is not enabled. Enabled modes are: ${props.modeOptions.map(m => m.id).join(', ')}.`,
1032
+ });
1033
+ }
1034
+ return;
1035
+ }
879
1036
  if (resolvedInput === '/mode' || resolvedInput.startsWith('/mode ')) {
880
1037
  const modeArg = resolvedInput.replace('/mode', '').trim().toLowerCase();
881
1038
  if (!modeArg) {
@@ -965,6 +1122,14 @@ function XibeCodeChatApp(props) {
965
1122
  const anyErr = err;
966
1123
  return anyErr.name === 'AbortError' || String(anyErr.message || '').toLowerCase().includes('aborted');
967
1124
  };
1125
+ // Save to prompt history (avoid duplicates for consecutive same prompts)
1126
+ if (resolvedInput && !resolvedInput.startsWith('/')) {
1127
+ const hist = promptHistoryRef.current;
1128
+ if (hist[hist.length - 1] !== resolvedInput) {
1129
+ hist.push(resolvedInput);
1130
+ }
1131
+ historyIndexRef.current = hist.length;
1132
+ }
968
1133
  // Watchdog auto-restart loop (up to 2 restarts)
969
1134
  restartAttemptsRef.current = 0;
970
1135
  let promptToRun = resolvedInput;
@@ -1004,9 +1169,11 @@ function XibeCodeChatApp(props) {
1004
1169
  configPrompt.kind,
1005
1170
  ensureModelsLoaded,
1006
1171
  exit,
1172
+ filePickerOpen,
1007
1173
  isRunning,
1008
1174
  props,
1009
1175
  pushLine,
1176
+ questionsState,
1010
1177
  requestOpenAIModelsFrom,
1011
1178
  selectedCommandIndex,
1012
1179
  activeMode,
@@ -1015,7 +1182,6 @@ function XibeCodeChatApp(props) {
1015
1182
  wireFormat,
1016
1183
  applyMode,
1017
1184
  startSetupWizard,
1018
- isRunning,
1019
1185
  ]);
1020
1186
  const normalizedInput = input.trim().toLowerCase();
1021
1187
  const isSlashMode = input.startsWith('/');
@@ -1035,6 +1201,9 @@ function XibeCodeChatApp(props) {
1035
1201
  const visibleModelOptions = filteredModels.slice(modelPickerStart, modelPickerStart + MODEL_PICKER_WINDOW);
1036
1202
  const setupModelPickerStart = computeWindowStart(setupModels.length, setupSelectedModelIndex, MODEL_PICKER_WINDOW);
1037
1203
  const visibleSetupModelOptions = setupModels.slice(setupModelPickerStart, setupModelPickerStart + MODEL_PICKER_WINDOW);
1204
+ const FILE_PICKER_WINDOW = 14;
1205
+ const filePickerStart = computeWindowStart(filteredFileEntries.length, selectedFileIndex, FILE_PICKER_WINDOW);
1206
+ const visibleFileEntries = filteredFileEntries.slice(filePickerStart, filePickerStart + FILE_PICKER_WINDOW);
1038
1207
  useInput((inputKey, key) => {
1039
1208
  if (key.ctrl && inputKey === 'c') {
1040
1209
  exit();
@@ -1326,6 +1495,7 @@ function XibeCodeChatApp(props) {
1326
1495
  return;
1327
1496
  const config = new ConfigManager(props.profile);
1328
1497
  setSetupProviderPickerOpen(false);
1498
+ setSetupProvider(picked.value);
1329
1499
  if (picked.value === 'custom') {
1330
1500
  setSetupStep('baseUrl');
1331
1501
  pushLine({
@@ -1424,6 +1594,69 @@ function XibeCodeChatApp(props) {
1424
1594
  return;
1425
1595
  }
1426
1596
  }
1597
+ // File picker arrow navigation (@-triggered)
1598
+ if (filePickerOpen && !isRunning && !isSlashMode) {
1599
+ if (key.escape) {
1600
+ setFilePickerOpen(false);
1601
+ setSelectedFileIndex(0);
1602
+ return;
1603
+ }
1604
+ if (key.upArrow) {
1605
+ setSelectedFileIndex((prev) => prev === 0 ? filteredFileEntries.length - 1 : prev - 1);
1606
+ return;
1607
+ }
1608
+ if (key.downArrow) {
1609
+ setSelectedFileIndex((prev) => prev >= filteredFileEntries.length - 1 ? 0 : prev + 1);
1610
+ return;
1611
+ }
1612
+ if (key.return) {
1613
+ const selected = filteredFileEntries[selectedFileIndex];
1614
+ if (selected) {
1615
+ const beforeAt = input.slice(0, atPos);
1616
+ const afterAtPart = input.slice(atPos + 1);
1617
+ const spaceIdx = afterAtPart.indexOf(' ');
1618
+ const rest = spaceIdx === -1 ? '' : afterAtPart.slice(spaceIdx);
1619
+ const pickedPath = mentionPathForPick(selected);
1620
+ const tag = `${AT_MENTION_OPEN}@${pickedPath}${AT_MENTION_CLOSE}`;
1621
+ lockedPickTagsRef.current.add(tag);
1622
+ const newInput = `${beforeAt}${tag}${rest} `;
1623
+ setInput(reconcileTaggedAtMentions(newInput, lockedPickTagsRef));
1624
+ setChatInputMountKey((k) => k + 1);
1625
+ }
1626
+ setFilePickerOpen(false);
1627
+ setSelectedFileIndex(0);
1628
+ return;
1629
+ }
1630
+ }
1631
+ // Prompt history browsing with UP/DOWN arrows (only in normal chat input)
1632
+ if (!isSlashMode && !isRunning && !questionsState && !configMenuOpen && !modePickerOpen && !modelPickerOpen && !setupModelPickerOpen && !setupProviderPickerOpen && !configProviderPickerOpen && !configCostModePickerOpen) {
1633
+ const hist = promptHistoryRef.current;
1634
+ if (key.upArrow) {
1635
+ if (hist.length === 0)
1636
+ return;
1637
+ // Save current input before browsing (only the first time we press up)
1638
+ if (historyIndexRef.current >= hist.length) {
1639
+ savedInputRef.current = input;
1640
+ }
1641
+ if (historyIndexRef.current > 0) {
1642
+ historyIndexRef.current -= 1;
1643
+ setInput(hist[historyIndexRef.current]);
1644
+ }
1645
+ return;
1646
+ }
1647
+ if (key.downArrow) {
1648
+ if (historyIndexRef.current < hist.length) {
1649
+ historyIndexRef.current += 1;
1650
+ if (historyIndexRef.current >= hist.length) {
1651
+ setInput(savedInputRef.current);
1652
+ }
1653
+ else {
1654
+ setInput(hist[historyIndexRef.current]);
1655
+ }
1656
+ }
1657
+ return;
1658
+ }
1659
+ }
1427
1660
  if (!isSlashMode || isRunning || filteredCommands.length === 0) {
1428
1661
  return;
1429
1662
  }
@@ -1487,8 +1720,8 @@ function XibeCodeChatApp(props) {
1487
1720
  ? 'Anthropic Messages'
1488
1721
  : 'OpenAI chat', ")"] })] })] })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "suggestion", children: [props.runtimeStatus, ":"] }), _jsx(Text, { color: "inactive", children: " Ready - type " }), _jsx(Text, { color: "claude", children: "/help" }), _jsx(Text, { color: "inactive", children: " to begin" })] }), props.sandboxLabel ? (_jsxs(Text, { color: "inactive", children: ["sandbox: ", props.sandboxLabel] })) : null, props.sandboxId ? (_jsxs(Text, { color: "inactive", children: ["sandbox id: ", props.sandboxId] })) : null, props.previewUrl ? (_jsxs(Text, { color: "inactive", children: ["preview: ", props.previewUrl] })) : null, props.pullHint ? (_jsxs(Text, { color: "inactive", children: ["pull: ", props.pullHint] })) : null, _jsxs(Text, { color: "inactive", children: ["xibecode ", _jsxs(Text, { color: "claude", children: ["v", APP_VERSION] })] }), _jsx(Text, { color: "subtle", children: '─'.repeat(98) }), _jsx(Text, { color: "inactive", children: "Agent transcript" })] }, item.id));
1489
1722
  }
1490
- return (_jsx(React.Fragment, { children: item.type === 'assistant' ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: prefixColorKey('assistant'), children: [prefixForType('assistant'), ":"] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(AssistantMarkdown, { content: item.text }) })] })) : (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: prefixColorKey(item.type), children: [prefixForType(item.type), ":", ' '] }), _jsx(Text, { color: lineColorKey(item.type), children: item.text })] })) }, item.id));
1491
- } }), !hasChatContent && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: "inactive", children: "(send a message to start)" }) })), _jsx(Text, { color: "subtle", children: divider }), isRunning && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "claudeBlue_FOR_SYSTEM_SPINNER", flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsxs(Text, { bold: true, color: "claudeBlue_FOR_SYSTEM_SPINNER", children: [WORK_SPINNER_FRAMES[workSpinnerFrame], ' '] }), _jsx(Text, { bold: true, color: "briefLabelClaude", children: workVerbPhrase })] }) })), questionsState && (() => {
1723
+ return (_jsx(React.Fragment, { children: item.type === 'assistant' ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: [prefixForType('assistant'), ":"] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(AssistantMarkdown, { content: item.text }) })] })) : (_jsxs(Text, { children: [_jsxs(Text, { bold: true, color: prefixColorKey(item.type, activeMode), children: [prefixForType(item.type), ":", ' '] }), _jsx(Text, { color: lineColorKey(item.type), children: item.text })] })) }, item.id));
1724
+ } }), !hasChatContent && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: "inactive", children: "(send a message to start)" }) })), _jsx(Text, { color: "subtle", children: divider }), isRunning && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "claudeBlue_FOR_SYSTEM_SPINNER", flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsxs(Text, { bold: true, color: "claudeBlue_FOR_SYSTEM_SPINNER", children: [WORK_SPINNER_FRAMES[workSpinnerFrame], ' '] }), _jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: workVerbPhrase })] }) })), questionsState && (() => {
1492
1725
  const { questions, currentIndex, selectedOptionIndex, isTypingCustom } = questionsState;
1493
1726
  const q = questions[currentIndex];
1494
1727
  const optLetters = 'abcdefghij';
@@ -1504,19 +1737,36 @@ function XibeCodeChatApp(props) {
1504
1737
  const isSelected = otherIdx === selectedOptionIndex;
1505
1738
  return (_jsxs(Text, { children: [_jsx(Text, { bold: true, color: isSelected ? 'green' : 'inactive', children: isSelected ? ' ▸ ' : ' ' }), _jsxs(Text, { bold: true, color: isSelected ? 'green' : 'yellow', children: [optLetters[otherIdx], ")"] }), _jsx(Text, { color: isSelected ? 'green' : 'inactive', children: " type yourself" })] }));
1506
1739
  })()] }), _jsx(Text, { color: "inactive", dimColor: true, children: "\u2191/\u2193 to navigate, Enter to select, Esc to cancel" })] }));
1507
- })(), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: questionsState ? (questionsState.isTypingCustom ? 'green' : 'yellow') : 'claude', paddingX: 1, children: [_jsx(Text, { color: questionsState ? (questionsState.isTypingCustom ? 'green' : 'yellow') : 'claude', children: '> ' }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: questionsState
1740
+ })(), _jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: questionsState ? (questionsState.isTypingCustom ? 'green' : 'yellow') : prefixColorKey('assistant', activeMode), paddingX: 1, children: [_jsx(Text, { color: questionsState ? (questionsState.isTypingCustom ? 'green' : 'yellow') : prefixColorKey('assistant', activeMode), children: '> ' }), _jsx(TextInput, { value: input, onChange: handleChatInputChange, onSubmit: onSubmit, placeholder: questionsState
1508
1741
  ? questionsState.isTypingCustom
1509
1742
  ? `Type your answer for Q${questionsState.currentIndex + 1} and press Enter`
1510
1743
  : `Use ↑/↓ and Enter to pick an option (Q${questionsState.currentIndex + 1}/${questionsState.questions.length})`
1511
- : isRunning ? 'Waiting for response…' : 'Message XibeCode…' })] }), isSlashMode && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "suggestion", bold: true, children: "Commands" }), filteredCommands.length === 0 ? (_jsxs(Text, { color: "inactive", children: ["No commands match \"", input, "\""] })) : (filteredCommands.map((command, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === selectedCommandIndex ? 'claude' : 'inactive', children: index === selectedCommandIndex ? '' : ' ' }), _jsx(Text, { bold: true, color: index === selectedCommandIndex ? 'claude' : 'text', children: command.name }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", command.description] })] }) }, command.name)))), _jsx(Text, { color: "subtle", children: "Use \u2191/\u2193 to navigate, Tab to autocomplete." })] })), modelPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "claude", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "claude", children: "Select model" }), isModelListLoading ? (_jsx(Text, { color: "inactive", children: "Loading models from provider..." })) : filteredModels.length === 0 ? (_jsx(Text, { color: "inactive", children: "No models matched current filter." })) : (visibleModelOptions.map((modelName, index) => {
1744
+ : (setupStep === 'apiKey' || configPrompt.kind === 'apiKey')
1745
+ ? 'Enter the API key'
1746
+ : isRunning ? 'Waiting for response…' : 'Message XibeCode…' }, chatInputMountKey)] }), ((setupStep === 'apiKey' && !setupProviderPickerOpen) || configPrompt.kind === 'apiKey') && (() => {
1747
+ const activeSetupProvider = setupStep === 'apiKey'
1748
+ ? (setupProvider || new ConfigManager(props.profile).get('provider'))
1749
+ : new ConfigManager(props.profile).get('provider');
1750
+ const configVal = activeSetupProvider && activeSetupProvider !== 'custom'
1751
+ ? PROVIDER_CONFIGS[activeSetupProvider]
1752
+ : null;
1753
+ const apiKeyUrl = configVal?.apiKeyUrl;
1754
+ if (!apiKeyUrl)
1755
+ return null;
1756
+ return (_jsxs(Box, { marginTop: 1, marginLeft: 1, children: [_jsx(Text, { color: "cyan", underline: true, children: `\u001b]8;;${apiKeyUrl}\u001b\\get api key here\u001b]8;;\u001b\\` }), _jsx(Text, { color: "inactive", children: " (Ctrl+Click / Cmd+Click to open link)" })] }));
1757
+ })(), filePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: ["Files", filePickerLoading ? ' (loading...)' : ''] }), filePickerLoading ? (_jsx(Text, { color: "inactive", children: "Scanning workspace\u2026" })) : filteredFileEntries.length === 0 ? (_jsx(Text, { color: "inactive", children: fileQuery ? `No files match "${fileQuery}"` : 'No files in workspace' })) : (visibleFileEntries.map((entry, index) => {
1758
+ const absoluteIndex = filePickerStart + index;
1759
+ const isSelected = absoluteIndex === selectedFileIndex;
1760
+ return (_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : (entry.isDirectory ? 'yellow' : 'text'), children: mentionPathForPick(entry) })] }, entry.relativePath));
1761
+ })), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter inserts locked \u27E6@path\u27E7 \u2022 Space ends mention \u2022 Esc close" })] })), isSlashMode && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: prefixColorKey('assistant', activeMode), bold: true, children: "Commands" }), filteredCommands.length === 0 ? (_jsxs(Text, { color: "inactive", children: ["No commands match \"", input, "\""] })) : (filteredCommands.map((command, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === selectedCommandIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === selectedCommandIndex ? '▸ ' : ' ' }), _jsx(Text, { bold: true, color: index === selectedCommandIndex ? prefixColorKey('assistant', activeMode) : 'text', children: command.name }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", command.description] })] }) }, command.name)))), _jsx(Text, { color: "subtle", children: "Use \u2191/\u2193 to navigate, Tab to autocomplete." })] })), modelPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Select model" }), isModelListLoading ? (_jsx(Text, { color: "inactive", children: "Loading models from provider..." })) : filteredModels.length === 0 ? (_jsx(Text, { color: "inactive", children: "No models matched current filter." })) : (visibleModelOptions.map((modelName, index) => {
1512
1762
  const absoluteIndex = modelPickerStart + index;
1513
1763
  const isSelected = absoluteIndex === selectedModelIndex;
1514
- return (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: isSelected ? 'claude' : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? 'claude' : 'text', children: modelName })] }) }, modelName));
1515
- })), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), setupModelPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "claude", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "claude", children: "Setup: select model" }), setupModels.length === 0 ? (_jsx(Text, { color: "inactive", children: "No models loaded." })) : (visibleSetupModelOptions.map((modelName, index) => {
1764
+ return (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : 'text', children: modelName })] }) }, modelName));
1765
+ })), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), setupModelPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Setup: select model" }), setupModels.length === 0 ? (_jsx(Text, { color: "inactive", children: "No models loaded." })) : (visibleSetupModelOptions.map((modelName, index) => {
1516
1766
  const absoluteIndex = setupModelPickerStart + index;
1517
1767
  const isSelected = absoluteIndex === setupSelectedModelIndex;
1518
- return (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: isSelected ? 'claude' : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? 'claude' : 'text', children: modelName })] }) }, modelName));
1519
- })), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc cancel" })] })), (setupStep !== 'idle' || setupProviderPickerOpen) && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "suggestion", children: "Setup wizard" }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "claude", bold: true, children: "You are configuring your provider connection." }), _jsxs(Text, { color: "inactive", children: [' ', "This is required before the agent can run."] })] }), setupProviderPickerOpen && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "inactive", children: "Pick a provider preset (\u2191/\u2193, Enter). Esc cancels." }), [
1768
+ return (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? prefixColorKey('assistant', activeMode) : 'text', children: modelName })] }) }, modelName));
1769
+ })), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc cancel" })] })), (setupStep !== 'idle' || setupProviderPickerOpen) && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Setup wizard" }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: prefixColorKey('assistant', activeMode), bold: true, children: "You are configuring your provider connection." }), _jsxs(Text, { color: "inactive", children: [' ', "This is required before the agent can run."] })] }), setupProviderPickerOpen && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "inactive", children: "Pick a provider preset (\u2191/\u2193, Enter). Esc cancels." }), [
1520
1770
  'Routing.run (recommended) (cheapest opensource model provider)',
1521
1771
  'zenllm.org (recommended) (best ai provider with 200+ models)',
1522
1772
  'OpenAI',
@@ -1529,7 +1779,7 @@ function XibeCodeChatApp(props) {
1529
1779
  'Moonshot (Kimi)',
1530
1780
  'Zhipu AI (z.ai)',
1531
1781
  'Custom (paste your own Base URL)',
1532
- ].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === setupProviderIndex ? 'claude' : 'inactive', children: index === setupProviderIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === setupProviderIndex ? 'claude' : 'text', children: label })] }) }, label)))] })), setupStep === 'baseUrl' && !setupProviderPickerOpen && (_jsx(Text, { color: "inactive", children: "Step: Base URL \u2014 paste an OpenAI-compatible endpoint (example: https://api.openai.com/v1)" })), setupStep === 'apiKey' && !setupProviderPickerOpen && (_jsx(Text, { color: "inactive", children: "Step: API key \u2014 paste your key (stored locally)" })), setupStep === 'loadingModels' && (_jsx(Text, { color: "inactive", children: "Step: Models \u2014 fetching `/models`\u2026" })), setupStep === 'pickModel' && (_jsx(Text, { color: "inactive", children: "Step: Model \u2014 select one below" }))] })), configMenuOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "suggestion", children: "Config" }), CONFIG_MENU.map((item, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configSelectedIndex ? 'claude' : 'inactive', children: index === configSelectedIndex ? '▸ ' : ' ' }), _jsx(Text, { bold: true, color: index === configSelectedIndex ? 'claude' : 'text', children: item.label }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", item.description] })] }) }, item.value))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter select \u2022 Esc close" })] })), configProviderPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "suggestion", children: "Provider" }), [
1782
+ ].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === setupProviderIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === setupProviderIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === setupProviderIndex ? prefixColorKey('assistant', activeMode) : 'text', children: label })] }) }, label)))] })), setupStep === 'baseUrl' && !setupProviderPickerOpen && (_jsx(Text, { color: "inactive", children: "Step: Base URL \u2014 paste an OpenAI-compatible endpoint (example: https://api.openai.com/v1)" })), setupStep === 'apiKey' && !setupProviderPickerOpen && (_jsx(Text, { color: "inactive", children: "Step: API key \u2014 paste your key (stored locally)" })), setupStep === 'loadingModels' && (_jsx(Text, { color: "inactive", children: "Step: Models \u2014 fetching `/models`\u2026" })), setupStep === 'pickModel' && (_jsx(Text, { color: "inactive", children: "Step: Model \u2014 select one below" }))] })), configMenuOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Config" }), CONFIG_MENU.map((item, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configSelectedIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === configSelectedIndex ? '▸ ' : ' ' }), _jsx(Text, { bold: true, color: index === configSelectedIndex ? prefixColorKey('assistant', activeMode) : 'text', children: item.label }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", item.description] })] }) }, item.value))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter select \u2022 Esc close" })] })), configProviderPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Provider" }), [
1533
1783
  'auto-detect',
1534
1784
  'openai',
1535
1785
  'anthropic',
@@ -1539,9 +1789,14 @@ function XibeCodeChatApp(props) {
1539
1789
  'grok',
1540
1790
  'kimi',
1541
1791
  'zai',
1542
- ].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configProviderIndex ? 'claude' : 'inactive', children: index === configProviderIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === configProviderIndex ? 'claude' : 'text', children: label })] }) }, label))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), configCostModePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "suggestion", children: "Cost mode" }), ['normal', 'economy'].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configCostModeIndex ? 'claude' : 'inactive', children: index === configCostModeIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === configCostModeIndex ? 'claude' : 'text', children: label })] }) }, label))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), modePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "suggestion", children: "Select mode" }), filteredModeOptions.length === 0 ? (_jsx(Text, { color: "inactive", children: "No modes matched current filter." })) : (filteredModeOptions.map((mode, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === selectedModeIndex ? 'claude' : 'inactive', children: index === selectedModeIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === selectedModeIndex ? 'claude' : 'text', children: mode.id }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", mode.description] })] }) }, mode.id)))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] }))] }));
1792
+ ].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configProviderIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === configProviderIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === configProviderIndex ? prefixColorKey('assistant', activeMode) : 'text', children: label })] }) }, label))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), configCostModePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Cost mode" }), ['normal', 'economy'].map((label, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === configCostModeIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === configCostModeIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === configCostModeIndex ? prefixColorKey('assistant', activeMode) : 'text', children: label })] }) }, label))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), modePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: prefixColorKey('assistant', activeMode), flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: prefixColorKey('assistant', activeMode), children: "Select mode" }), filteredModeOptions.length === 0 ? (_jsx(Text, { color: "inactive", children: "No modes matched current filter." })) : (filteredModeOptions.map((mode, index) => (_jsx(React.Fragment, { children: _jsxs(Text, { children: [_jsx(Text, { color: index === selectedModeIndex ? prefixColorKey('assistant', activeMode) : 'inactive', children: index === selectedModeIndex ? '▸ ' : ' ' }), _jsx(Text, { color: index === selectedModeIndex ? prefixColorKey('assistant', activeMode) : 'text', children: mode.id }), _jsxs(Text, { color: "inactive", children: [" \u2014 ", mode.description] })] }) }, mode.id)))), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] }))] }));
1543
1793
  }
1544
1794
  export async function launchClaudeStyleChat(options) {
1795
+ if (options.forceLocalRuntime) {
1796
+ process.env.XIBECODE_SANDBOX_MODE = 'local';
1797
+ delete process.env.XIBECODE_SANDBOX_SESSION_ID;
1798
+ delete process.env.XIBECODE_SANDBOX_SKIP_SYNC;
1799
+ }
1545
1800
  const config = new ConfigManager(options.profile);
1546
1801
  const apiKey = options.apiKey || config.getApiKey() || '';
1547
1802
  const needsFirstRunSetup = !apiKey;
@@ -1702,6 +1957,7 @@ export async function launchClaudeStyleChat(options) {
1702
1957
  activeAgent.removeAllListeners('event');
1703
1958
  let streamedBuffer = '';
1704
1959
  let streamFlushTimer = null;
1960
+ let didOutputContent = false;
1705
1961
  const flushStreamedBuffer = () => {
1706
1962
  if (streamedBuffer.trim()) {
1707
1963
  onLine({ type: 'assistant', text: streamedBuffer.trim() });
@@ -1725,6 +1981,7 @@ export async function launchClaudeStyleChat(options) {
1725
1981
  });
1726
1982
  break;
1727
1983
  case 'tool_call': {
1984
+ didOutputContent = true;
1728
1985
  // Flush any pending streamed text before showing tool call
1729
1986
  flushStreamedBuffer();
1730
1987
  if (streamFlushTimer) {
@@ -1770,6 +2027,8 @@ export async function launchClaudeStyleChat(options) {
1770
2027
  break;
1771
2028
  }
1772
2029
  case 'stream_text':
2030
+ if (event.data?.text)
2031
+ didOutputContent = true;
1773
2032
  opts?.onVisibleOutput?.();
1774
2033
  streamedBuffer += event.data?.text || '';
1775
2034
  // Accumulate streamed text and flush periodically or when buffer is large.
@@ -1798,6 +2057,8 @@ export async function launchClaudeStyleChat(options) {
1798
2057
  flushStreamedBuffer();
1799
2058
  break;
1800
2059
  case 'response':
2060
+ if (event.data?.text)
2061
+ didOutputContent = true;
1801
2062
  opts?.onVisibleOutput?.();
1802
2063
  onLine({ type: 'assistant', text: event.data?.text || '' });
1803
2064
  break;
@@ -1844,6 +2105,12 @@ export async function launchClaudeStyleChat(options) {
1844
2105
  images: opts?.images,
1845
2106
  signal: opts?.signal,
1846
2107
  });
2108
+ if (!didOutputContent) {
2109
+ onLine({
2110
+ type: 'error',
2111
+ text: 'The model returned an empty response. This often happens if the selected model is incompatible with the system prompt, or the API provider dropped the response due to context length.',
2112
+ });
2113
+ }
1847
2114
  // Write the latest messages to the transcript incrementally.
1848
2115
  // The agent's run() method appends to this.messages; we persist
1849
2116
  // the user prompt and assistant response as separate transcript entries.
@@ -2079,6 +2346,7 @@ export async function launchClaudeStyleChat(options) {
2079
2346
  const tokens = argsRaw ? argsRaw.split(/\s+/).filter(Boolean) : [];
2080
2347
  let apply = false;
2081
2348
  let force = false;
2349
+ let full = false;
2082
2350
  let output;
2083
2351
  for (let i = 0; i < tokens.length; i += 1) {
2084
2352
  const token = tokens[i];
@@ -2086,6 +2354,10 @@ export async function launchClaudeStyleChat(options) {
2086
2354
  apply = true;
2087
2355
  continue;
2088
2356
  }
2357
+ if (token === '--full') {
2358
+ full = true;
2359
+ continue;
2360
+ }
2089
2361
  if (token === '--force') {
2090
2362
  force = true;
2091
2363
  continue;
@@ -2093,18 +2365,19 @@ export async function launchClaudeStyleChat(options) {
2093
2365
  if (token === '--output') {
2094
2366
  const next = tokens[i + 1];
2095
2367
  if (!next || next.startsWith('--')) {
2096
- throw new Error('Usage: /cpull [--apply] [--force] [--output <path>]');
2368
+ throw new Error('Usage: /cpull [--apply] [--full] [--force] [--output <path>]');
2097
2369
  }
2098
2370
  output = next;
2099
2371
  i += 1;
2100
2372
  continue;
2101
2373
  }
2102
- throw new Error(`Unknown /cpull option "${token}". Usage: /cpull [--apply] [--force] [--output <path>]`);
2374
+ throw new Error(`Unknown /cpull option "${token}". Usage: /cpull [--apply] [--full] [--force] [--output <path>]`);
2103
2375
  }
2104
2376
  await cloudPullCommand({
2105
2377
  profile: options.profile,
2106
2378
  session: sessionId,
2107
2379
  apply,
2380
+ full,
2108
2381
  force,
2109
2382
  output,
2110
2383
  onStatus: (text) => pushLine({ type: 'info', text }),