xibecode 1.3.5 → 1.3.11

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' },
@@ -279,6 +326,15 @@ function XibeCodeChatApp(props) {
279
326
  const [configProviderIndex, setConfigProviderIndex] = useState(0);
280
327
  const [configCostModePickerOpen, setConfigCostModePickerOpen] = useState(false);
281
328
  const [configCostModeIndex, setConfigCostModeIndex] = useState(0);
329
+ // File picker state (@-triggered)
330
+ const [filePickerOpen, setFilePickerOpen] = useState(false);
331
+ const [filteredFileEntries, setFilteredFileEntries] = useState([]);
332
+ const [selectedFileIndex, setSelectedFileIndex] = useState(0);
333
+ const [fileQuery, setFileQuery] = useState('');
334
+ const [atPos, setAtPos] = useState(-1);
335
+ const [filePickerLoading, setFilePickerLoading] = useState(false);
336
+ /** Bump when the file picker inserts text so Ink TextInput remounts with the caret at the end */
337
+ const [chatInputMountKey, setChatInputMountKey] = useState(0);
282
338
  const [questionsState, setQuestionsState] = useState(null);
283
339
  const [workSpinnerFrame, setWorkSpinnerFrame] = useState(0);
284
340
  const [workVerbIndex, setWorkVerbIndex] = useState(0);
@@ -322,6 +378,9 @@ function XibeCodeChatApp(props) {
322
378
  // Keep a much larger in-memory transcript so context doesn't vanish quickly.
323
379
  setLines((prev) => [...prev.slice(-5000), withId]);
324
380
  }, [props]);
381
+ const handleChatInputChange = useCallback((next) => {
382
+ setInput(reconcileTaggedAtMentions(next, lockedPickTagsRef));
383
+ }, []);
325
384
  useEffect(() => {
326
385
  props.registerUiSink?.(pushLine);
327
386
  }, [props, pushLine]);
@@ -341,6 +400,12 @@ function XibeCodeChatApp(props) {
341
400
  const lastVisibleOutputAtRef = useRef(Date.now());
342
401
  const currentPromptRef = useRef(null);
343
402
  const restartAttemptsRef = useRef(0);
403
+ const promptHistoryRef = useRef([]);
404
+ const historyIndexRef = useRef(-1);
405
+ const savedInputRef = useRef('');
406
+ const cachedFileEntriesRef = useRef([]);
407
+ /** Exact ⟦@path⟧ strings inserted via the file picker — edits inside remove the whole token */
408
+ const lockedPickTagsRef = useRef(new Set());
344
409
  const sessionMessagesRef = useRef(props.initialMessages || []);
345
410
  const lastBgLineByTask = useRef(new Map());
346
411
  useEffect(() => {
@@ -433,6 +498,64 @@ function XibeCodeChatApp(props) {
433
498
  }, WORK_VERB_ROTATE_MS);
434
499
  return () => clearInterval(id);
435
500
  }, [isRunning]);
501
+ // Load workspace file list once on mount for @-picker
502
+ useEffect(() => {
503
+ let cancelled = false;
504
+ setFilePickerLoading(true);
505
+ (async () => {
506
+ try {
507
+ const entries = await listWorkspaceFiles(process.cwd());
508
+ if (!cancelled) {
509
+ cachedFileEntriesRef.current = entries;
510
+ }
511
+ }
512
+ catch {
513
+ // silently fail
514
+ }
515
+ finally {
516
+ if (!cancelled)
517
+ setFilePickerLoading(false);
518
+ }
519
+ })();
520
+ return () => { cancelled = true; };
521
+ }, []);
522
+ // Detect '@' in input to open file picker
523
+ useEffect(() => {
524
+ const lastAtIndex = input.lastIndexOf('@');
525
+ if (lastAtIndex === -1) {
526
+ setFilePickerOpen(false);
527
+ setSelectedFileIndex(0);
528
+ return;
529
+ }
530
+ // '@' must be at a word boundary
531
+ if (lastAtIndex > 0) {
532
+ const prev = input[lastAtIndex - 1];
533
+ if (prev !== ' ' && prev !== '(' && prev !== AT_MENTION_CLOSE) {
534
+ setFilePickerOpen(false);
535
+ setSelectedFileIndex(0);
536
+ return;
537
+ }
538
+ }
539
+ const afterAt = input.slice(lastAtIndex + 1);
540
+ // Space ends the active @-mention — hide suggestions (typing past first word breaks mention mode)
541
+ if (afterAt.includes(' ')) {
542
+ setFilePickerOpen(false);
543
+ setSelectedFileIndex(0);
544
+ return;
545
+ }
546
+ const query = afterAt;
547
+ setAtPos(lastAtIndex);
548
+ setFileQuery(query);
549
+ setFilePickerOpen(true);
550
+ const lowerQuery = query.toLowerCase();
551
+ const filtered = cachedFileEntriesRef.current.filter((e) => {
552
+ const rel = e.relativePath.toLowerCase();
553
+ const label = mentionPathForPick(e).toLowerCase();
554
+ return rel.includes(lowerQuery) || label.includes(lowerQuery);
555
+ });
556
+ setFilteredFileEntries(filtered);
557
+ setSelectedFileIndex((prev) => Math.min(prev, Math.max(0, filtered.length - 1)));
558
+ }, [input]);
436
559
  const ensureModelsLoaded = useCallback(async () => {
437
560
  if (availableModels.length > 0) {
438
561
  return availableModels;
@@ -603,17 +726,22 @@ function XibeCodeChatApp(props) {
603
726
  if (questionsState) {
604
727
  return;
605
728
  }
729
+ // If file picker is open, ignore Enter (handled by useInput)
730
+ if (filePickerOpen) {
731
+ return;
732
+ }
733
+ const flat = flattenTaggedAtMentions(trimmed);
606
734
  if (isRunning) {
607
- queuedPromptRef.current = trimmed;
735
+ queuedPromptRef.current = flat;
608
736
  setInput('');
609
737
  pushLine({ type: 'info', text: 'Queued message — will run next.' });
610
738
  return;
611
739
  }
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]
740
+ const commandMatches = CHAT_COMMANDS.filter((command) => command.name.toLowerCase().startsWith(flat.toLowerCase()));
741
+ const exactMatch = CHAT_COMMANDS.some((command) => command.name.toLowerCase() === flat.toLowerCase());
742
+ const resolvedInput = flat.startsWith('/') && !exactMatch && commandMatches[selectedCommandIndex]
615
743
  ? commandMatches[selectedCommandIndex].name
616
- : trimmed;
744
+ : flat;
617
745
  setInput('');
618
746
  // Setup wizard input capture
619
747
  if (setupStep !== 'idle') {
@@ -965,6 +1093,14 @@ function XibeCodeChatApp(props) {
965
1093
  const anyErr = err;
966
1094
  return anyErr.name === 'AbortError' || String(anyErr.message || '').toLowerCase().includes('aborted');
967
1095
  };
1096
+ // Save to prompt history (avoid duplicates for consecutive same prompts)
1097
+ if (resolvedInput && !resolvedInput.startsWith('/')) {
1098
+ const hist = promptHistoryRef.current;
1099
+ if (hist[hist.length - 1] !== resolvedInput) {
1100
+ hist.push(resolvedInput);
1101
+ }
1102
+ historyIndexRef.current = hist.length;
1103
+ }
968
1104
  // Watchdog auto-restart loop (up to 2 restarts)
969
1105
  restartAttemptsRef.current = 0;
970
1106
  let promptToRun = resolvedInput;
@@ -1004,9 +1140,11 @@ function XibeCodeChatApp(props) {
1004
1140
  configPrompt.kind,
1005
1141
  ensureModelsLoaded,
1006
1142
  exit,
1143
+ filePickerOpen,
1007
1144
  isRunning,
1008
1145
  props,
1009
1146
  pushLine,
1147
+ questionsState,
1010
1148
  requestOpenAIModelsFrom,
1011
1149
  selectedCommandIndex,
1012
1150
  activeMode,
@@ -1015,7 +1153,6 @@ function XibeCodeChatApp(props) {
1015
1153
  wireFormat,
1016
1154
  applyMode,
1017
1155
  startSetupWizard,
1018
- isRunning,
1019
1156
  ]);
1020
1157
  const normalizedInput = input.trim().toLowerCase();
1021
1158
  const isSlashMode = input.startsWith('/');
@@ -1035,6 +1172,9 @@ function XibeCodeChatApp(props) {
1035
1172
  const visibleModelOptions = filteredModels.slice(modelPickerStart, modelPickerStart + MODEL_PICKER_WINDOW);
1036
1173
  const setupModelPickerStart = computeWindowStart(setupModels.length, setupSelectedModelIndex, MODEL_PICKER_WINDOW);
1037
1174
  const visibleSetupModelOptions = setupModels.slice(setupModelPickerStart, setupModelPickerStart + MODEL_PICKER_WINDOW);
1175
+ const FILE_PICKER_WINDOW = 14;
1176
+ const filePickerStart = computeWindowStart(filteredFileEntries.length, selectedFileIndex, FILE_PICKER_WINDOW);
1177
+ const visibleFileEntries = filteredFileEntries.slice(filePickerStart, filePickerStart + FILE_PICKER_WINDOW);
1038
1178
  useInput((inputKey, key) => {
1039
1179
  if (key.ctrl && inputKey === 'c') {
1040
1180
  exit();
@@ -1424,6 +1564,69 @@ function XibeCodeChatApp(props) {
1424
1564
  return;
1425
1565
  }
1426
1566
  }
1567
+ // File picker arrow navigation (@-triggered)
1568
+ if (filePickerOpen && !isRunning && !isSlashMode) {
1569
+ if (key.escape) {
1570
+ setFilePickerOpen(false);
1571
+ setSelectedFileIndex(0);
1572
+ return;
1573
+ }
1574
+ if (key.upArrow) {
1575
+ setSelectedFileIndex((prev) => prev === 0 ? filteredFileEntries.length - 1 : prev - 1);
1576
+ return;
1577
+ }
1578
+ if (key.downArrow) {
1579
+ setSelectedFileIndex((prev) => prev >= filteredFileEntries.length - 1 ? 0 : prev + 1);
1580
+ return;
1581
+ }
1582
+ if (key.return) {
1583
+ const selected = filteredFileEntries[selectedFileIndex];
1584
+ if (selected) {
1585
+ const beforeAt = input.slice(0, atPos);
1586
+ const afterAtPart = input.slice(atPos + 1);
1587
+ const spaceIdx = afterAtPart.indexOf(' ');
1588
+ const rest = spaceIdx === -1 ? '' : afterAtPart.slice(spaceIdx);
1589
+ const pickedPath = mentionPathForPick(selected);
1590
+ const tag = `${AT_MENTION_OPEN}@${pickedPath}${AT_MENTION_CLOSE}`;
1591
+ lockedPickTagsRef.current.add(tag);
1592
+ const newInput = `${beforeAt}${tag}${rest} `;
1593
+ setInput(reconcileTaggedAtMentions(newInput, lockedPickTagsRef));
1594
+ setChatInputMountKey((k) => k + 1);
1595
+ }
1596
+ setFilePickerOpen(false);
1597
+ setSelectedFileIndex(0);
1598
+ return;
1599
+ }
1600
+ }
1601
+ // Prompt history browsing with UP/DOWN arrows (only in normal chat input)
1602
+ if (!isSlashMode && !isRunning && !questionsState && !configMenuOpen && !modePickerOpen && !modelPickerOpen && !setupModelPickerOpen && !setupProviderPickerOpen && !configProviderPickerOpen && !configCostModePickerOpen) {
1603
+ const hist = promptHistoryRef.current;
1604
+ if (key.upArrow) {
1605
+ if (hist.length === 0)
1606
+ return;
1607
+ // Save current input before browsing (only the first time we press up)
1608
+ if (historyIndexRef.current >= hist.length) {
1609
+ savedInputRef.current = input;
1610
+ }
1611
+ if (historyIndexRef.current > 0) {
1612
+ historyIndexRef.current -= 1;
1613
+ setInput(hist[historyIndexRef.current]);
1614
+ }
1615
+ return;
1616
+ }
1617
+ if (key.downArrow) {
1618
+ if (historyIndexRef.current < hist.length) {
1619
+ historyIndexRef.current += 1;
1620
+ if (historyIndexRef.current >= hist.length) {
1621
+ setInput(savedInputRef.current);
1622
+ }
1623
+ else {
1624
+ setInput(hist[historyIndexRef.current]);
1625
+ }
1626
+ }
1627
+ return;
1628
+ }
1629
+ }
1427
1630
  if (!isSlashMode || isRunning || filteredCommands.length === 0) {
1428
1631
  return;
1429
1632
  }
@@ -1504,11 +1707,15 @@ function XibeCodeChatApp(props) {
1504
1707
  const isSelected = otherIdx === selectedOptionIndex;
1505
1708
  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
1709
  })()] }), _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
1710
+ })(), _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: handleChatInputChange, onSubmit: onSubmit, placeholder: questionsState
1508
1711
  ? questionsState.isTypingCustom
1509
1712
  ? `Type your answer for Q${questionsState.currentIndex + 1} and press Enter`
1510
1713
  : `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) => {
1714
+ : isRunning ? 'Waiting for response…' : 'Message XibeCode…' }, chatInputMountKey)] }), filePickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "suggestion", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "suggestion", 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) => {
1715
+ const absoluteIndex = filePickerStart + index;
1716
+ const isSelected = absoluteIndex === selectedFileIndex;
1717
+ return (_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? 'claude' : 'inactive', children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { color: isSelected ? 'claude' : (entry.isDirectory ? 'yellow' : 'text'), children: mentionPathForPick(entry) })] }, entry.relativePath));
1718
+ })), _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: "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) => {
1512
1719
  const absoluteIndex = modelPickerStart + index;
1513
1720
  const isSelected = absoluteIndex === selectedModelIndex;
1514
1721
  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));
@@ -1542,6 +1749,11 @@ function XibeCodeChatApp(props) {
1542
1749
  ].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" })] }))] }));
1543
1750
  }
1544
1751
  export async function launchClaudeStyleChat(options) {
1752
+ if (options.forceLocalRuntime) {
1753
+ process.env.XIBECODE_SANDBOX_MODE = 'local';
1754
+ delete process.env.XIBECODE_SANDBOX_SESSION_ID;
1755
+ delete process.env.XIBECODE_SANDBOX_SKIP_SYNC;
1756
+ }
1545
1757
  const config = new ConfigManager(options.profile);
1546
1758
  const apiKey = options.apiKey || config.getApiKey() || '';
1547
1759
  const needsFirstRunSetup = !apiKey;
@@ -2079,6 +2291,7 @@ export async function launchClaudeStyleChat(options) {
2079
2291
  const tokens = argsRaw ? argsRaw.split(/\s+/).filter(Boolean) : [];
2080
2292
  let apply = false;
2081
2293
  let force = false;
2294
+ let full = false;
2082
2295
  let output;
2083
2296
  for (let i = 0; i < tokens.length; i += 1) {
2084
2297
  const token = tokens[i];
@@ -2086,6 +2299,10 @@ export async function launchClaudeStyleChat(options) {
2086
2299
  apply = true;
2087
2300
  continue;
2088
2301
  }
2302
+ if (token === '--full') {
2303
+ full = true;
2304
+ continue;
2305
+ }
2089
2306
  if (token === '--force') {
2090
2307
  force = true;
2091
2308
  continue;
@@ -2093,18 +2310,19 @@ export async function launchClaudeStyleChat(options) {
2093
2310
  if (token === '--output') {
2094
2311
  const next = tokens[i + 1];
2095
2312
  if (!next || next.startsWith('--')) {
2096
- throw new Error('Usage: /cpull [--apply] [--force] [--output <path>]');
2313
+ throw new Error('Usage: /cpull [--apply] [--full] [--force] [--output <path>]');
2097
2314
  }
2098
2315
  output = next;
2099
2316
  i += 1;
2100
2317
  continue;
2101
2318
  }
2102
- throw new Error(`Unknown /cpull option "${token}". Usage: /cpull [--apply] [--force] [--output <path>]`);
2319
+ throw new Error(`Unknown /cpull option "${token}". Usage: /cpull [--apply] [--full] [--force] [--output <path>]`);
2103
2320
  }
2104
2321
  await cloudPullCommand({
2105
2322
  profile: options.profile,
2106
2323
  session: sessionId,
2107
2324
  apply,
2325
+ full,
2108
2326
  force,
2109
2327
  output,
2110
2328
  onStatus: (text) => pushLine({ type: 'info', text }),