xibecode 1.0.8 → 1.2.2
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/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +33 -35
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/hooks.d.ts +13 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +134 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/memory.d.ts +13 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +88 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/resume.d.ts +3 -0
- package/dist/commands/resume.d.ts.map +1 -1
- package/dist/commands/resume.js +36 -9
- package/dist/commands/resume.js.map +1 -1
- package/dist/commands/settings.d.ts +14 -0
- package/dist/commands/settings.d.ts.map +1 -0
- package/dist/commands/settings.js +89 -0
- package/dist/commands/settings.js.map +1 -0
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/claude-style-chat.d.ts.map +1 -1
- package/dist/ui/claude-style-chat.js +355 -11
- package/dist/ui/claude-style-chat.js.map +1 -1
- package/package.json +2 -2
|
@@ -20,6 +20,7 @@ import { SPINNER_VERBS } from '../constants/spinnerVerbs.js';
|
|
|
20
20
|
import { extractAtReferences, splitAtReferences } from 'xibecode-core';
|
|
21
21
|
import { loadImageAttachment, mimeFromExtension } from '../utils/image-attachments.js';
|
|
22
22
|
import { SessionManager } from 'xibecode-core';
|
|
23
|
+
import { AutoMemoryManager, HooksManager, SettingsManager as CoreSettingsManager } from 'xibecode-core';
|
|
23
24
|
function isAbortLikeError(err) {
|
|
24
25
|
if (!err || typeof err !== 'object')
|
|
25
26
|
return false;
|
|
@@ -101,7 +102,7 @@ const HERO_LOGO = [
|
|
|
101
102
|
const WORK_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
102
103
|
/** How fast to advance OpenClaude-style spinner verbs (ms) */
|
|
103
104
|
const WORK_VERB_ROTATE_MS = 2400;
|
|
104
|
-
const QUICK_HELP = ['/help', '/mode', '/format', '/model', '/setup', '/config', '/donate', '/sponsor', '/clear', '/exit'];
|
|
105
|
+
const QUICK_HELP = ['/help', '/mode', '/format', '/model', '/setup', '/config', '/memory', '/hooks', '/donate', '/sponsor', '/clear', '/exit'];
|
|
105
106
|
const CHAT_COMMANDS = [
|
|
106
107
|
{ name: '/help', description: 'Show available shortcuts and usage hints' },
|
|
107
108
|
{ name: '/mode', description: 'Switch agent mode from an interactive picker' },
|
|
@@ -110,6 +111,8 @@ const CHAT_COMMANDS = [
|
|
|
110
111
|
{ name: '/model', description: 'Fetch and switch available models for this provider' },
|
|
111
112
|
{ name: '/setup', description: 'Guided setup (set API key, then pick provider/model)' },
|
|
112
113
|
{ name: '/config', description: 'Show current config and quick config hints' },
|
|
114
|
+
{ name: '/memory', description: 'Show auto-memories for this project' },
|
|
115
|
+
{ name: '/hooks', description: 'Show registered lifecycle hooks' },
|
|
113
116
|
{ name: '/donate', description: 'Open the donation page in your browser' },
|
|
114
117
|
{ name: '/sponsor', description: 'Open the sponsorship page in your browser' },
|
|
115
118
|
{ name: '/exit', description: 'Exit the interactive chat session' },
|
|
@@ -232,6 +235,7 @@ function XibeCodeChatApp(props) {
|
|
|
232
235
|
const [configProviderIndex, setConfigProviderIndex] = useState(0);
|
|
233
236
|
const [configCostModePickerOpen, setConfigCostModePickerOpen] = useState(false);
|
|
234
237
|
const [configCostModeIndex, setConfigCostModeIndex] = useState(0);
|
|
238
|
+
const [questionsState, setQuestionsState] = useState(null);
|
|
235
239
|
const [workSpinnerFrame, setWorkSpinnerFrame] = useState(0);
|
|
236
240
|
const [workVerbIndex, setWorkVerbIndex] = useState(0);
|
|
237
241
|
const nextLineIdRef = useRef(1);
|
|
@@ -279,6 +283,11 @@ function XibeCodeChatApp(props) {
|
|
|
279
283
|
setActiveMode(nextMode);
|
|
280
284
|
});
|
|
281
285
|
}, [props]);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
props.registerQuestionsSink?.((qs) => {
|
|
288
|
+
setQuestionsState({ questions: qs, currentIndex: 0, answers: {}, selectedOptionIndex: 0, isTypingCustom: false });
|
|
289
|
+
});
|
|
290
|
+
}, [props]);
|
|
282
291
|
const abortControllerRef = useRef(null);
|
|
283
292
|
const abortReasonRef = useRef('none');
|
|
284
293
|
const queuedPromptRef = useRef(null);
|
|
@@ -336,13 +345,14 @@ function XibeCodeChatApp(props) {
|
|
|
336
345
|
if (!isRunning)
|
|
337
346
|
return;
|
|
338
347
|
const id = setInterval(() => {
|
|
339
|
-
// Silent means: no visible output (tool_call/tool_result/stream_text/response) for
|
|
340
|
-
|
|
348
|
+
// Silent means: no visible output (tool_call/tool_result/stream_text/response) for 180s.
|
|
349
|
+
// Some models (DeepSeek, etc.) can take a long time for their first token during complex tasks.
|
|
350
|
+
if (Date.now() - lastVisibleOutputAtRef.current > 180_000) {
|
|
341
351
|
if (abortControllerRef.current && abortReasonRef.current === 'none') {
|
|
342
352
|
abortReasonRef.current = 'watchdog';
|
|
343
353
|
pushLine({
|
|
344
354
|
type: 'info',
|
|
345
|
-
text: `No output for
|
|
355
|
+
text: `No output for 180s — restarting (attempt ${Math.min(restartAttemptsRef.current + 1, 2)}/2)…`,
|
|
346
356
|
});
|
|
347
357
|
abortControllerRef.current.abort();
|
|
348
358
|
}
|
|
@@ -471,10 +481,79 @@ function XibeCodeChatApp(props) {
|
|
|
471
481
|
text: `Mode switched to ${nextMode}`,
|
|
472
482
|
});
|
|
473
483
|
}, [activeMode, props, pushLine]);
|
|
484
|
+
const applyQuestionAnswer = useCallback(async (state, answer) => {
|
|
485
|
+
const { questions, currentIndex, answers } = state;
|
|
486
|
+
const currentQ = questions[currentIndex];
|
|
487
|
+
const newAnswers = { ...answers, [currentQ.id]: answer };
|
|
488
|
+
const nextIndex = currentIndex + 1;
|
|
489
|
+
if (nextIndex < questions.length) {
|
|
490
|
+
// More questions - advance with reset selection
|
|
491
|
+
setQuestionsState({
|
|
492
|
+
questions,
|
|
493
|
+
currentIndex: nextIndex,
|
|
494
|
+
answers: newAnswers,
|
|
495
|
+
selectedOptionIndex: 0,
|
|
496
|
+
isTypingCustom: false,
|
|
497
|
+
});
|
|
498
|
+
setInput('');
|
|
499
|
+
pushLine({ type: 'info', text: ` ${currentIndex + 1}. ${currentQ.question} → ${answer}` });
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// All questions answered - submit
|
|
503
|
+
setQuestionsState(null);
|
|
504
|
+
setInput('');
|
|
505
|
+
pushLine({ type: 'info', text: ` ${currentIndex + 1}. ${currentQ.question} → ${answer}` });
|
|
506
|
+
const answersText = questions
|
|
507
|
+
.map((q, i) => `${i + 1}. ${q.question}\n Answer: ${newAnswers[q.id] || '(skipped)'}`)
|
|
508
|
+
.join('\n');
|
|
509
|
+
pushLine({ type: 'info', text: 'Answers submitted. Continuing...' });
|
|
510
|
+
const answersPrompt = `Here are my answers to your questions:\n\n${answersText}`;
|
|
511
|
+
currentPromptRef.current = answersPrompt;
|
|
512
|
+
abortReasonRef.current = 'none';
|
|
513
|
+
lastVisibleOutputAtRef.current = Date.now();
|
|
514
|
+
abortControllerRef.current = new AbortController();
|
|
515
|
+
pushLine({ type: 'user', text: answersPrompt });
|
|
516
|
+
sessionMessagesRef.current.push({ role: 'user', content: answersPrompt });
|
|
517
|
+
setIsRunning(true);
|
|
518
|
+
try {
|
|
519
|
+
const startedAt = Date.now();
|
|
520
|
+
const stats = await props.runPrompt(answersPrompt, pushLine, {
|
|
521
|
+
signal: abortControllerRef.current.signal,
|
|
522
|
+
onVisibleOutput: () => {
|
|
523
|
+
lastVisibleOutputAtRef.current = Date.now();
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
const elapsedMs = Date.now() - startedAt;
|
|
527
|
+
const seconds = (elapsedMs / 1000).toFixed(1);
|
|
528
|
+
pushLine({
|
|
529
|
+
type: 'info',
|
|
530
|
+
text: `Done in ${seconds}s` + (stats.costLabel ? ` · cost ${stats.costLabel}` : ''),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
abortControllerRef.current = null;
|
|
535
|
+
currentPromptRef.current = null;
|
|
536
|
+
setIsRunning(false);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}, [props, pushLine]);
|
|
540
|
+
// Called from useInput when a question option is selected via arrows+Enter
|
|
541
|
+
const handleQuestionAnswer = useCallback((state, answer) => {
|
|
542
|
+
applyQuestionAnswer(state, answer);
|
|
543
|
+
}, [applyQuestionAnswer]);
|
|
474
544
|
const onSubmit = useCallback(async (value) => {
|
|
475
545
|
const trimmed = value.trim();
|
|
476
546
|
if (!trimmed)
|
|
477
547
|
return;
|
|
548
|
+
// Handle custom-typing mode for questions (Enter after typing custom answer)
|
|
549
|
+
if (questionsState && questionsState.isTypingCustom) {
|
|
550
|
+
await applyQuestionAnswer(questionsState, trimmed);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// If questions are active but not in typing mode, ignore (use arrows + Enter)
|
|
554
|
+
if (questionsState) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
478
557
|
if (isRunning) {
|
|
479
558
|
queuedPromptRef.current = trimmed;
|
|
480
559
|
setInput('');
|
|
@@ -632,6 +711,16 @@ function XibeCodeChatApp(props) {
|
|
|
632
711
|
startSetupWizard();
|
|
633
712
|
return;
|
|
634
713
|
}
|
|
714
|
+
if (resolvedInput === '/memory' || resolvedInput.startsWith('/memory ')) {
|
|
715
|
+
const subcmd = resolvedInput.replace('/memory', '').trim().toLowerCase();
|
|
716
|
+
props.onMemoryCommand?.(subcmd || 'list', pushLine);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (resolvedInput === '/hooks' || resolvedInput.startsWith('/hooks ')) {
|
|
720
|
+
const subcmd = resolvedInput.replace('/hooks', '').trim().toLowerCase();
|
|
721
|
+
props.onHooksCommand?.(subcmd || 'list', pushLine);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
635
724
|
if (resolvedInput === '/format' || resolvedInput.startsWith('/format ')) {
|
|
636
725
|
const arg = resolvedInput.replace(/^\/format\s*/i, '').trim().toLowerCase();
|
|
637
726
|
if (!arg) {
|
|
@@ -857,6 +946,72 @@ function XibeCodeChatApp(props) {
|
|
|
857
946
|
exit();
|
|
858
947
|
return;
|
|
859
948
|
}
|
|
949
|
+
// Interactive questions: arrow key navigation
|
|
950
|
+
if (questionsState && !questionsState.isTypingCustom) {
|
|
951
|
+
const q = questionsState.questions[questionsState.currentIndex];
|
|
952
|
+
const totalOptions = q.options.length + (q.hasOther !== false ? 1 : 0);
|
|
953
|
+
if (key.upArrow) {
|
|
954
|
+
setQuestionsState({
|
|
955
|
+
...questionsState,
|
|
956
|
+
selectedOptionIndex: questionsState.selectedOptionIndex <= 0
|
|
957
|
+
? totalOptions - 1
|
|
958
|
+
: questionsState.selectedOptionIndex - 1,
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (key.downArrow) {
|
|
963
|
+
setQuestionsState({
|
|
964
|
+
...questionsState,
|
|
965
|
+
selectedOptionIndex: questionsState.selectedOptionIndex >= totalOptions - 1
|
|
966
|
+
? 0
|
|
967
|
+
: questionsState.selectedOptionIndex + 1,
|
|
968
|
+
});
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// Enter on a highlighted option: if "type yourself" is selected, switch to typing mode
|
|
972
|
+
if (key.return) {
|
|
973
|
+
const idx = questionsState.selectedOptionIndex;
|
|
974
|
+
const isOtherOption = idx === q.options.length && q.hasOther !== false;
|
|
975
|
+
if (isOtherOption) {
|
|
976
|
+
setQuestionsState({ ...questionsState, isTypingCustom: true });
|
|
977
|
+
setInput('');
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
// Picked a concrete option — resolve and submit
|
|
981
|
+
if (idx < q.options.length) {
|
|
982
|
+
const answer = q.options[idx].label;
|
|
983
|
+
handleQuestionAnswer(questionsState, answer);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// Escape to cancel questions
|
|
988
|
+
if (key.escape) {
|
|
989
|
+
setQuestionsState(null);
|
|
990
|
+
pushLine({ type: 'info', text: 'Questions cancelled.' });
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
// If not a special key and not typing custom, ignore (user must use arrows + Enter)
|
|
994
|
+
if (!key.return && !key.escape && !key.upArrow && !key.downArrow && !key.ctrl && !key.meta) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// If in custom-typing mode for a question, handle Enter to submit
|
|
999
|
+
if (questionsState && questionsState.isTypingCustom) {
|
|
1000
|
+
if (key.return) {
|
|
1001
|
+
const answer = input.trim();
|
|
1002
|
+
if (!answer)
|
|
1003
|
+
return;
|
|
1004
|
+
handleQuestionAnswer(questionsState, answer);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (key.escape) {
|
|
1008
|
+
// Go back to option selection
|
|
1009
|
+
setQuestionsState({ ...questionsState, isTypingCustom: false });
|
|
1010
|
+
setInput('');
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
// Let normal TextInput handle typing — don't intercept
|
|
1014
|
+
}
|
|
860
1015
|
if (isRunning && key.escape) {
|
|
861
1016
|
if (abortControllerRef.current && abortReasonRef.current === 'none') {
|
|
862
1017
|
abortReasonRef.current = 'user';
|
|
@@ -1235,7 +1390,27 @@ function XibeCodeChatApp(props) {
|
|
|
1235
1390
|
: 'OpenAI chat', ")"] })] })] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "suggestion", children: "\u25C8 cloud" }), _jsx(Text, { color: "inactive", children: " Ready \u2014 type " }), _jsx(Text, { color: "claude", children: "/help" }), _jsx(Text, { color: "inactive", children: " to begin" })] }), _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));
|
|
1236
1391
|
}
|
|
1237
1392
|
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));
|
|
1238
|
-
} }), !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 })] }) })),
|
|
1393
|
+
} }), !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 && (() => {
|
|
1394
|
+
const { questions, currentIndex, selectedOptionIndex, isTypingCustom } = questionsState;
|
|
1395
|
+
const q = questions[currentIndex];
|
|
1396
|
+
const optLetters = 'abcdefghij';
|
|
1397
|
+
const totalOptions = q.options.length + (q.hasOther !== false ? 1 : 0);
|
|
1398
|
+
if (isTypingCustom) {
|
|
1399
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "green", children: ["Question ", currentIndex + 1, "/", questions.length, " \u2014 Type your answer"] }), _jsx(Text, { color: "text", children: q.question }), _jsx(Text, { color: "inactive", dimColor: true, children: "Press Esc to go back to options" })] }));
|
|
1400
|
+
}
|
|
1401
|
+
return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Question ", currentIndex + 1, "/", questions.length] }), _jsxs(Text, { color: "text", children: [q.question, q.allowMultiple ? ' (select all that apply)' : ''] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [q.options.map((o, j) => {
|
|
1402
|
+
const isSelected = j === selectedOptionIndex;
|
|
1403
|
+
return (_jsxs(Text, { children: [_jsx(Text, { bold: true, color: isSelected ? 'green' : 'inactive', children: isSelected ? ' ▸ ' : ' ' }), _jsxs(Text, { bold: true, color: isSelected ? 'green' : 'yellow', children: [optLetters[j], ")"] }), _jsxs(Text, { color: isSelected ? 'green' : 'text', children: [" ", o.label] })] }, o.id));
|
|
1404
|
+
}), q.hasOther !== false && (() => {
|
|
1405
|
+
const otherIdx = q.options.length;
|
|
1406
|
+
const isSelected = otherIdx === selectedOptionIndex;
|
|
1407
|
+
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" })] }));
|
|
1408
|
+
})()] }), _jsx(Text, { color: "inactive", dimColor: true, children: "\u2191/\u2193 to navigate, Enter to select, Esc to cancel" })] }));
|
|
1409
|
+
})(), _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
|
|
1410
|
+
? questionsState.isTypingCustom
|
|
1411
|
+
? `Type your answer for Q${questionsState.currentIndex + 1} and press Enter`
|
|
1412
|
+
: `Use ↑/↓ and Enter to pick an option (Q${questionsState.currentIndex + 1}/${questionsState.questions.length})`
|
|
1413
|
+
: 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) => {
|
|
1239
1414
|
const absoluteIndex = modelPickerStart + index;
|
|
1240
1415
|
const isSelected = absoluteIndex === selectedModelIndex;
|
|
1241
1416
|
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));
|
|
@@ -1397,15 +1572,36 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1397
1572
|
}
|
|
1398
1573
|
activeAgent.removeAllListeners('event');
|
|
1399
1574
|
let streamedBuffer = '';
|
|
1575
|
+
let streamFlushTimer = null;
|
|
1576
|
+
const flushStreamedBuffer = () => {
|
|
1577
|
+
if (streamedBuffer.trim()) {
|
|
1578
|
+
onLine({ type: 'assistant', text: streamedBuffer.trim() });
|
|
1579
|
+
streamedBuffer = '';
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1400
1582
|
activeAgent.on('event', (event) => {
|
|
1401
1583
|
switch (event.type) {
|
|
1402
1584
|
case 'thinking':
|
|
1585
|
+
// Flush any pending streamed text before showing thinking indicator
|
|
1586
|
+
flushStreamedBuffer();
|
|
1587
|
+
if (streamFlushTimer) {
|
|
1588
|
+
clearTimeout(streamFlushTimer);
|
|
1589
|
+
streamFlushTimer = null;
|
|
1590
|
+
}
|
|
1591
|
+
// Thinking messages are visible output — reset the watchdog timer
|
|
1592
|
+
opts?.onVisibleOutput?.();
|
|
1403
1593
|
onLine({
|
|
1404
1594
|
type: 'info',
|
|
1405
1595
|
text: event.data?.message || 'Thinking…',
|
|
1406
1596
|
});
|
|
1407
1597
|
break;
|
|
1408
1598
|
case 'tool_call': {
|
|
1599
|
+
// Flush any pending streamed text before showing tool call
|
|
1600
|
+
flushStreamedBuffer();
|
|
1601
|
+
if (streamFlushTimer) {
|
|
1602
|
+
clearTimeout(streamFlushTimer);
|
|
1603
|
+
streamFlushTimer = null;
|
|
1604
|
+
}
|
|
1409
1605
|
opts?.onVisibleOutput?.();
|
|
1410
1606
|
const name = String(event.data?.name ?? 'tool');
|
|
1411
1607
|
const input = event.data?.input;
|
|
@@ -1417,6 +1613,12 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1417
1613
|
break;
|
|
1418
1614
|
}
|
|
1419
1615
|
case 'tool_result': {
|
|
1616
|
+
// Flush any pending streamed text before showing tool result
|
|
1617
|
+
flushStreamedBuffer();
|
|
1618
|
+
if (streamFlushTimer) {
|
|
1619
|
+
clearTimeout(streamFlushTimer);
|
|
1620
|
+
streamFlushTimer = null;
|
|
1621
|
+
}
|
|
1420
1622
|
opts?.onVisibleOutput?.();
|
|
1421
1623
|
const name = String(event.data?.name ?? 'tool');
|
|
1422
1624
|
const result = event.data?.result;
|
|
@@ -1441,12 +1643,30 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1441
1643
|
case 'stream_text':
|
|
1442
1644
|
opts?.onVisibleOutput?.();
|
|
1443
1645
|
streamedBuffer += event.data?.text || '';
|
|
1646
|
+
// Accumulate streamed text and flush periodically or when buffer is large.
|
|
1647
|
+
// Use a longer interval (2s) to batch more text into fewer "XibeCode:" blocks,
|
|
1648
|
+
// reducing the fragmented multi-prefix display seen with fast models.
|
|
1649
|
+
if (streamedBuffer.length > 1500) {
|
|
1650
|
+
// Large buffer — flush immediately to avoid memory buildup
|
|
1651
|
+
if (streamFlushTimer) {
|
|
1652
|
+
clearTimeout(streamFlushTimer);
|
|
1653
|
+
streamFlushTimer = null;
|
|
1654
|
+
}
|
|
1655
|
+
flushStreamedBuffer();
|
|
1656
|
+
}
|
|
1657
|
+
else if (!streamFlushTimer) {
|
|
1658
|
+
streamFlushTimer = setTimeout(() => {
|
|
1659
|
+
streamFlushTimer = null;
|
|
1660
|
+
flushStreamedBuffer();
|
|
1661
|
+
}, 2000);
|
|
1662
|
+
}
|
|
1444
1663
|
break;
|
|
1445
1664
|
case 'stream_end':
|
|
1446
|
-
if (
|
|
1447
|
-
|
|
1665
|
+
if (streamFlushTimer) {
|
|
1666
|
+
clearTimeout(streamFlushTimer);
|
|
1667
|
+
streamFlushTimer = null;
|
|
1448
1668
|
}
|
|
1449
|
-
|
|
1669
|
+
flushStreamedBuffer();
|
|
1450
1670
|
break;
|
|
1451
1671
|
case 'response':
|
|
1452
1672
|
opts?.onVisibleOutput?.();
|
|
@@ -1473,6 +1693,20 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1473
1693
|
}
|
|
1474
1694
|
break;
|
|
1475
1695
|
}
|
|
1696
|
+
case 'plan_ready':
|
|
1697
|
+
onLine({
|
|
1698
|
+
type: 'info',
|
|
1699
|
+
text: 'Plan written to implementations.md. Say "build" to implement it, or ask for edits.',
|
|
1700
|
+
});
|
|
1701
|
+
break;
|
|
1702
|
+
case 'questions': {
|
|
1703
|
+
const qs = event.data?.questions;
|
|
1704
|
+
if (qs && qs.length > 0) {
|
|
1705
|
+
// Push questions to the UI component via the sink
|
|
1706
|
+
questionsSink?.(qs);
|
|
1707
|
+
}
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1476
1710
|
default:
|
|
1477
1711
|
break;
|
|
1478
1712
|
}
|
|
@@ -1481,6 +1715,22 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1481
1715
|
images: opts?.images,
|
|
1482
1716
|
signal: opts?.signal,
|
|
1483
1717
|
});
|
|
1718
|
+
// Write the latest messages to the transcript incrementally.
|
|
1719
|
+
// The agent's run() method appends to this.messages; we persist
|
|
1720
|
+
// the user prompt and assistant response as separate transcript entries.
|
|
1721
|
+
const latestMessages = activeAgent.getMessages();
|
|
1722
|
+
if (latestMessages.length > 0) {
|
|
1723
|
+
// Write the user prompt that triggered this turn
|
|
1724
|
+
const userMsg = latestMessages[latestMessages.length - 2];
|
|
1725
|
+
if (userMsg?.role === 'user') {
|
|
1726
|
+
await activeAgent.transcriptUserMessage(userMsg);
|
|
1727
|
+
}
|
|
1728
|
+
// Write the assistant response
|
|
1729
|
+
const assistantMsg = latestMessages[latestMessages.length - 1];
|
|
1730
|
+
if (assistantMsg?.role === 'assistant') {
|
|
1731
|
+
await activeAgent.transcriptAssistantMessage(assistantMsg);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1484
1734
|
await onMessagesUpdate(activeAgent.getMessages());
|
|
1485
1735
|
activeMode = activeAgent.getMode();
|
|
1486
1736
|
toolExecutor.setMode(activeMode);
|
|
@@ -1527,6 +1777,11 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1527
1777
|
const registerModeSink = (sink) => {
|
|
1528
1778
|
modeSink = sink;
|
|
1529
1779
|
};
|
|
1780
|
+
// Sink for pushing interactive questions from the agent to the UI component.
|
|
1781
|
+
let questionsSink = null;
|
|
1782
|
+
const registerQuestionsSink = (sink) => {
|
|
1783
|
+
questionsSink = sink;
|
|
1784
|
+
};
|
|
1530
1785
|
const formatUnknownError = (err) => {
|
|
1531
1786
|
if (err instanceof Error)
|
|
1532
1787
|
return err.stack || err.message;
|
|
@@ -1558,13 +1813,27 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1558
1813
|
cwd: process.cwd(),
|
|
1559
1814
|
});
|
|
1560
1815
|
currentSessionId = currentSession.id;
|
|
1816
|
+
// Initialize transcript persistence for the agent
|
|
1817
|
+
activeAgent?.initTranscript(currentSessionId, process.cwd());
|
|
1561
1818
|
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
1562
1819
|
currentSession.messages = options.initialMessages;
|
|
1563
|
-
|
|
1820
|
+
// Write initial messages to transcript
|
|
1821
|
+
for (const msg of options.initialMessages) {
|
|
1822
|
+
if (msg.role === 'user') {
|
|
1823
|
+
await activeAgent?.transcriptUserMessage(msg);
|
|
1824
|
+
}
|
|
1825
|
+
else if (msg.role === 'assistant') {
|
|
1826
|
+
await activeAgent?.transcriptAssistantMessage(msg);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1564
1829
|
}
|
|
1565
1830
|
}
|
|
1566
1831
|
else {
|
|
1567
1832
|
currentSession = await sessionManager.loadSession(currentSessionId);
|
|
1833
|
+
// Initialize transcript persistence for resumed session
|
|
1834
|
+
if (currentSession) {
|
|
1835
|
+
activeAgent?.initTranscript(currentSessionId, currentSession.cwd);
|
|
1836
|
+
}
|
|
1568
1837
|
}
|
|
1569
1838
|
if (currentSession?.messages?.length) {
|
|
1570
1839
|
activeAgent?.setMessages(currentSession.messages);
|
|
@@ -1574,6 +1843,7 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1574
1843
|
}
|
|
1575
1844
|
const onSessionCreated = (sessionId) => {
|
|
1576
1845
|
currentSessionId = sessionId;
|
|
1846
|
+
activeAgent?.initTranscript(sessionId, process.cwd());
|
|
1577
1847
|
};
|
|
1578
1848
|
const onMessagesUpdate = async (messages) => {
|
|
1579
1849
|
if (currentSessionId && currentSession) {
|
|
@@ -1587,12 +1857,86 @@ export async function launchClaudeStyleChat(options) {
|
|
|
1587
1857
|
}
|
|
1588
1858
|
}
|
|
1589
1859
|
}
|
|
1590
|
-
|
|
1860
|
+
// Use JSONL transcript persistence instead of monolithic JSON save.
|
|
1861
|
+
// Each message was already written to the transcript incrementally
|
|
1862
|
+
// via transcriptUserMessage/transcriptAssistantMessage in the chat loop.
|
|
1863
|
+
// Here we just update the title metadata.
|
|
1864
|
+
try {
|
|
1865
|
+
await sessionManager.saveSession(currentSession);
|
|
1866
|
+
}
|
|
1867
|
+
catch {
|
|
1868
|
+
// Transcript writes are best-effort; don't block the chat loop
|
|
1869
|
+
}
|
|
1591
1870
|
}
|
|
1592
1871
|
};
|
|
1593
1872
|
const root = createRoot({ exitOnCtrlC: true });
|
|
1873
|
+
// ── Memory & Hooks slash-command handlers ──
|
|
1874
|
+
const autoMemManager = new AutoMemoryManager({ cwd: process.cwd() });
|
|
1875
|
+
const coreSettingsManager = new CoreSettingsManager({ cwd: process.cwd() });
|
|
1876
|
+
const hooksMgr = new HooksManager(coreSettingsManager);
|
|
1877
|
+
await hooksMgr.loadFromSettingsManager().catch(() => { });
|
|
1878
|
+
const onMemoryCommand = (subcmd, pushLine) => {
|
|
1879
|
+
void (async () => {
|
|
1880
|
+
try {
|
|
1881
|
+
if (subcmd === 'list' || subcmd === '') {
|
|
1882
|
+
const memories = await autoMemManager.listMemories();
|
|
1883
|
+
if (memories.length === 0) {
|
|
1884
|
+
pushLine({ type: 'info', text: 'No memories found for this project. Memories are auto-extracted as you chat.' });
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
pushLine({ type: 'info', text: `Found ${memories.length} memory/memories:` });
|
|
1888
|
+
for (const mem of memories.slice(0, 10)) {
|
|
1889
|
+
const tags = mem.frontmatter.tags?.length ? ` [${mem.frontmatter.tags.join(', ')}]` : '';
|
|
1890
|
+
pushLine({ type: 'info', text: ` ${mem.frontmatter.type}${tags}: ${mem.content.trim().slice(0, 80)}${mem.content.length > 80 ? '...' : ''}` });
|
|
1891
|
+
}
|
|
1892
|
+
if (memories.length > 10) {
|
|
1893
|
+
pushLine({ type: 'info', text: ` ... and ${memories.length - 10} more. Use "xc memory list" to see all.` });
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
else if (subcmd === 'dream') {
|
|
1897
|
+
pushLine({ type: 'info', text: 'Running dream consolidation...' });
|
|
1898
|
+
const result = await autoMemManager.dream();
|
|
1899
|
+
pushLine({ type: 'info', text: `Dream complete: created=${result.created}, merged=${result.merged}, pruned=${result.pruned}` });
|
|
1900
|
+
}
|
|
1901
|
+
else if (subcmd === 'path') {
|
|
1902
|
+
pushLine({ type: 'info', text: `Memory dir: ${autoMemManager.getMemoryDir()}` });
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
pushLine({ type: 'info', text: 'Usage: /memory [list|dream|path]. Default: list' });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
catch (err) {
|
|
1909
|
+
pushLine({ type: 'error', text: `Memory error: ${err?.message || err}` });
|
|
1910
|
+
}
|
|
1911
|
+
})();
|
|
1912
|
+
};
|
|
1913
|
+
const onHooksCommand = (subcmd, pushLine) => {
|
|
1914
|
+
const allHooks = hooksMgr.getAllHooks();
|
|
1915
|
+
const flatList = [];
|
|
1916
|
+
for (const [event, hooks] of allHooks) {
|
|
1917
|
+
for (const hook of hooks) {
|
|
1918
|
+
flatList.push({ event, config: hook.config, matcher: hook.matcher });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (subcmd === 'list' || subcmd === '') {
|
|
1922
|
+
if (flatList.length === 0) {
|
|
1923
|
+
pushLine({ type: 'info', text: 'No hooks configured. Use "xc hooks add" or edit ~/.xibecode/settings.json' });
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
pushLine({ type: 'info', text: `Registered hooks (${flatList.length}):` });
|
|
1927
|
+
for (const hook of flatList.slice(0, 10)) {
|
|
1928
|
+
const type = 'command' in hook.config ? 'command' : 'prompt' in hook.config ? 'prompt' : 'agent' in hook.config ? 'agent' : 'http' in hook.config ? 'http' : 'function';
|
|
1929
|
+
const value = hook.config.command || hook.config.prompt || hook.config.agent || hook.config.http || hook.config.url || '(fn)';
|
|
1930
|
+
const matcher = hook.matcher ? ` matcher="${hook.matcher}"` : '';
|
|
1931
|
+
pushLine({ type: 'info', text: ` ${hook.event}${matcher} -> ${type}: ${value}` });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
else {
|
|
1935
|
+
pushLine({ type: 'info', text: 'Usage: /hooks [list]. Default: list. Use "xc hooks" CLI for full management.' });
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1594
1938
|
try {
|
|
1595
|
-
await renderAndRun(root, _jsx(XibeCodeChatApp, { model: model, initialMode: activeMode, provider: provider, baseUrl: baseUrl, needsFirstRunSetup: needsFirstRunSetup, defaultModel: model, modeOptions: modeOptions, initialRequestFormat: wireFormat, customProviderFormat: customProviderFormat, profile: options.profile, sessionId: currentSessionId, initialMessages: options.initialMessages, runPrompt: runPrompt, listBackgroundTasks: listBackgroundTasks, checkBackgroundTask: checkBackgroundTask, onUiLine: appendLogLine, registerUiSink: registerUiSink, registerModeSink: registerModeSink, loadModels: loadModels, onModelChange: onModelChange, onModeChange: onModeChange, onWireFormatChange: onWireFormatChange, onSessionCreated: onSessionCreated, onMessagesUpdate: onMessagesUpdate, getCurrentMessages: () => activeAgent?.getMessages() ?? currentSession?.messages ?? [] }));
|
|
1939
|
+
await renderAndRun(root, _jsx(XibeCodeChatApp, { model: model, initialMode: activeMode, provider: provider, baseUrl: baseUrl, needsFirstRunSetup: needsFirstRunSetup, defaultModel: model, modeOptions: modeOptions, initialRequestFormat: wireFormat, customProviderFormat: customProviderFormat, profile: options.profile, sessionId: currentSessionId, initialMessages: options.initialMessages, runPrompt: runPrompt, listBackgroundTasks: listBackgroundTasks, checkBackgroundTask: checkBackgroundTask, onUiLine: appendLogLine, registerUiSink: registerUiSink, registerModeSink: registerModeSink, registerQuestionsSink: registerQuestionsSink, loadModels: loadModels, onModelChange: onModelChange, onModeChange: onModeChange, onWireFormatChange: onWireFormatChange, onSessionCreated: onSessionCreated, onMessagesUpdate: onMessagesUpdate, getCurrentMessages: () => activeAgent?.getMessages() ?? currentSession?.messages ?? [], onMemoryCommand: onMemoryCommand, onHooksCommand: onHooksCommand }));
|
|
1596
1940
|
}
|
|
1597
1941
|
finally {
|
|
1598
1942
|
process.off('unhandledRejection', onUnhandledRejection);
|