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.
- package/dist/commands/cloud-pull.d.ts +5 -0
- package/dist/commands/cloud-pull.d.ts.map +1 -1
- package/dist/commands/cloud-pull.js +98 -4
- package/dist/commands/cloud-pull.js.map +1 -1
- package/dist/commands/resume.js +1 -0
- package/dist/commands/resume.js.map +1 -1
- package/dist/index.js +41 -17
- package/dist/index.js.map +1 -1
- package/dist/ui/claude-style-chat.d.ts +2 -0
- package/dist/ui/claude-style-chat.d.ts.map +1 -1
- package/dist/ui/claude-style-chat.js +293 -20
- package/dist/ui/claude-style-chat.js.map +1 -1
- package/dist/utils/cloud-sync-feedback.js +1 -1
- package/dist/utils/cloud-sync-feedback.js.map +1 -1
- package/dist/utils/list-files.d.ts +10 -0
- package/dist/utils/list-files.d.ts.map +1 -0
- package/dist/utils/list-files.js +118 -0
- package/dist/utils/list-files.js.map +1 -0
- package/dist/utils/tui-theme.d.ts +2 -0
- package/dist/utils/tui-theme.d.ts.map +1 -1
- package/dist/utils/tui-theme.js +2 -0
- package/dist/utils/tui-theme.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
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 =
|
|
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(
|
|
613
|
-
const exactMatch = CHAT_COMMANDS.some((command) => command.name.toLowerCase() ===
|
|
614
|
-
const resolvedInput =
|
|
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
|
-
:
|
|
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:
|
|
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') : '
|
|
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
|
-
:
|
|
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 ? '
|
|
1515
|
-
})), _jsx(Text, { color: "subtle", children: "\u2191/\u2193 navigate \u2022 Enter apply \u2022 Esc close" })] })), setupModelPickerOpen && (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor:
|
|
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 ? '
|
|
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:
|
|
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 ? '
|
|
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 ? '
|
|
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 }),
|