sanook-cli 0.5.5 → 0.5.8
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/CHANGELOG.md +55 -0
- package/README.md +392 -42
- package/README.th.md +15 -8
- package/dist/auto-maintain.js +113 -0
- package/dist/bin.js +77 -15
- package/dist/brain-final.js +8 -4
- package/dist/brain-link.js +73 -0
- package/dist/brain-new.js +9 -5
- package/dist/brain-repair.js +7 -4
- package/dist/brand.js +21 -0
- package/dist/commands.js +7 -1
- package/dist/config.js +40 -29
- package/dist/cost.js +20 -0
- package/dist/dashboard/api-helpers.js +112 -3
- package/dist/dashboard/server.js +85 -1
- package/dist/dashboard/static/app.js +381 -0
- package/dist/dashboard/static/styles.css +36 -0
- package/dist/dashboard/terminal.js +214 -0
- package/dist/diff.js +22 -8
- package/dist/gateway/session.js +4 -0
- package/dist/install-info.js +91 -0
- package/dist/loop.js +31 -4
- package/dist/memory.js +236 -16
- package/dist/persona.js +300 -0
- package/dist/project-scaffold.js +4 -2
- package/dist/providers/registry.js +11 -1
- package/dist/self-improve-synth.js +86 -0
- package/dist/self-improve.js +203 -0
- package/dist/session-brain.js +112 -0
- package/dist/slash-completion.js +1 -0
- package/dist/ui/app.js +154 -30
- package/dist/ui/input-view.js +104 -0
- package/dist/ui/persona-wizard.js +89 -0
- package/dist/ui/render.js +87 -5
- package/dist/ui/tool-activity.js +118 -0
- package/dist/ui/tool-trail.js +15 -3
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/package.json +11 -2
- package/scripts/postinstall.mjs +4 -4
package/dist/ui/app.js
CHANGED
|
@@ -6,9 +6,10 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { BUILTIN_COMMANDS, HELP_TEXT, expandCustomCommand, loadCustomCommands, parseCommand, parseSlashInvocation, } from '../commands.js';
|
|
8
8
|
import { runAgent } from '../loop.js';
|
|
9
|
+
import { finalizeReplSession, formatFinalizeMessage } from '../session-brain.js';
|
|
9
10
|
import { saveSession, newSessionId, listSessions, removeSession, renameSession } from '../session.js';
|
|
10
11
|
import { TOOL_CATALOG } from '../tool-catalog.js';
|
|
11
|
-
import { getBrainPath, appendBrainWorklog } from '../memory.js';
|
|
12
|
+
import { getBrainPath, appendBrainWorklog, appendBrainTranscript } from '../memory.js';
|
|
12
13
|
import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
|
|
13
14
|
import { makeSummarizer } from '../summarize.js';
|
|
14
15
|
import { agentTuning, patchGlobalConfig } from '../config.js';
|
|
@@ -18,7 +19,9 @@ import { loadMcpHubEntries } from '../mcp-hub.js';
|
|
|
18
19
|
import { probeMcpServer } from '../mcp.js';
|
|
19
20
|
import { filterModelPickerOptions, initialModelPickerIndex, modelPickerOptions, modelProviderEntries, } from '../model-picker.js';
|
|
20
21
|
import { clampCompletionIndex, completionForInput, completionReplaceValue } from '../slash-completion.js';
|
|
21
|
-
import { loadSkills } from '../skills.js';
|
|
22
|
+
import { loadSkills, saveSkill } from '../skills.js';
|
|
23
|
+
import { maybeAutoSkill } from '../self-improve.js';
|
|
24
|
+
import { defaultSkillSynthesizer } from '../self-improve-synth.js';
|
|
22
25
|
import { copyTextToClipboard } from '../clipboard.js';
|
|
23
26
|
import { useEditor } from './useEditor.js';
|
|
24
27
|
import { useBusyElapsedSeconds } from './useBusyElapsed.js';
|
|
@@ -34,8 +37,10 @@ import { MarkdownText, StreamingMarkdownText } from './markdown.js';
|
|
|
34
37
|
import { SessionPanel } from './session-panel.js';
|
|
35
38
|
import { getTranscriptWindow, transcriptScrollStep, transcriptWindowSize } from './transcript.js';
|
|
36
39
|
import { footerStatus } from './status.js';
|
|
40
|
+
import { inputViewport, graphemesOf, cursorGraphemeIndex, SCROLL_LEAD, SCROLL_TAIL } from './input-view.js';
|
|
41
|
+
import { PersonaOverlay } from './persona-wizard.js';
|
|
37
42
|
import { thinkingPanelLines, snapshotThinking } from './thinking-panel.js';
|
|
38
|
-
import { toolTrailLines, updateToolTrailOnEvent } from './tool-trail.js';
|
|
43
|
+
import { toolTrailLines, toolTrailHeader, toolTrailWidth, updateToolTrailOnEvent } from './tool-trail.js';
|
|
39
44
|
const execFileP = promisify(execFile);
|
|
40
45
|
const PRE_TURN_COMPACT_TOKENS = 100_000; // session ยาวมากเท่านั้นถึง summarize ก่อน turn (mode summarize)
|
|
41
46
|
const startupCount = (value) => value === 'checking' ? 'checking' : value.count ? `${value.count}` : 'none';
|
|
@@ -53,6 +58,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
53
58
|
});
|
|
54
59
|
const [streaming, setStreaming] = useState('');
|
|
55
60
|
const [thinking, setThinking] = useState('');
|
|
61
|
+
const [agentStatus, setAgentStatus] = useState('');
|
|
56
62
|
const [toolTrail, setToolTrail] = useState([]);
|
|
57
63
|
const [busy, setBusy] = useState(false);
|
|
58
64
|
const [model, setModel] = useState(initialModel);
|
|
@@ -65,6 +71,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
65
71
|
const [thinkingMode, setThinkingMode] = useState('collapsed');
|
|
66
72
|
const [contextCompression, setContextCompression] = useState();
|
|
67
73
|
const [transcriptScroll, setTranscriptScroll] = useState(0);
|
|
74
|
+
const [personaOpen, setPersonaOpen] = useState(false);
|
|
68
75
|
const idRef = useRef(0);
|
|
69
76
|
const lastCost = useRef('');
|
|
70
77
|
const nextToolTrailId = useRef(0);
|
|
@@ -74,6 +81,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
74
81
|
const msgsRef = useRef(initialHistory ?? []); // conversation จริงสำหรับ LLM (สะสมข้ามรอบ)
|
|
75
82
|
const sessionId = useRef(newSessionId());
|
|
76
83
|
const sessionCreated = useRef(new Date().toISOString());
|
|
84
|
+
const exitingRef = useRef(false);
|
|
77
85
|
const approvalResolve = useRef(null);
|
|
78
86
|
const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
|
|
79
87
|
const checkpoints = useRef([]);
|
|
@@ -259,6 +267,26 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
259
267
|
setCompletionIndex(0);
|
|
260
268
|
return true;
|
|
261
269
|
};
|
|
270
|
+
const requestExit = () => {
|
|
271
|
+
if (exitingRef.current)
|
|
272
|
+
return;
|
|
273
|
+
exitingRef.current = true;
|
|
274
|
+
void finalizeReplSession({
|
|
275
|
+
sessionId: sessionId.current,
|
|
276
|
+
sessionCreated: sessionCreated.current,
|
|
277
|
+
model,
|
|
278
|
+
cwd,
|
|
279
|
+
messages: msgsRef.current,
|
|
280
|
+
history: history.map((turn) => ({ role: turn.role, text: turn.text })),
|
|
281
|
+
})
|
|
282
|
+
.then((result) => {
|
|
283
|
+
const note = formatFinalizeMessage(result);
|
|
284
|
+
if (note)
|
|
285
|
+
process.stderr.write(`\n${note}\n`);
|
|
286
|
+
exit();
|
|
287
|
+
})
|
|
288
|
+
.catch(() => exit());
|
|
289
|
+
};
|
|
262
290
|
// /diff /undo — git-backed (execFile ไม่ผ่าน shell)
|
|
263
291
|
async function runGit(args, label) {
|
|
264
292
|
try {
|
|
@@ -817,7 +845,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
817
845
|
if (editor.value)
|
|
818
846
|
editor.reset(); // Ctrl+C ครั้งแรก = ล้างบรรทัด, ว่างแล้ว = ออก
|
|
819
847
|
else
|
|
820
|
-
|
|
848
|
+
requestExit();
|
|
821
849
|
}
|
|
822
850
|
});
|
|
823
851
|
/** ย้อน 1 turn — คืนไฟล์ (git, recoverable) + ตัดบทสนทนากลับ */
|
|
@@ -910,7 +938,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
910
938
|
if (cmd.handled) {
|
|
911
939
|
addTurn('user', displayText);
|
|
912
940
|
if (cmd.action === 'quit')
|
|
913
|
-
return
|
|
941
|
+
return requestExit();
|
|
914
942
|
if (cmd.action === 'clear') {
|
|
915
943
|
msgsRef.current = [];
|
|
916
944
|
checkpoints.current = [];
|
|
@@ -938,6 +966,10 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
938
966
|
.catch((e) => addTurn('system', `personality: ${e.message}`));
|
|
939
967
|
return;
|
|
940
968
|
}
|
|
969
|
+
if (cmd.action === 'personaSetup') {
|
|
970
|
+
setPersonaOpen(true);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
941
973
|
if (cmd.action === 'insights') {
|
|
942
974
|
void renderInsights({ days: cmd.insightsDays, cwd: cmd.insightsAll ? null : undefined })
|
|
943
975
|
.then((msg) => addTurn('system', msg))
|
|
@@ -1023,11 +1055,13 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1023
1055
|
resetLiveToolTrail();
|
|
1024
1056
|
resetLiveThinking();
|
|
1025
1057
|
setStreaming('');
|
|
1058
|
+
setAgentStatus('Starting…');
|
|
1026
1059
|
setBusy(true);
|
|
1027
1060
|
let buf = '';
|
|
1028
1061
|
let reasoningBuf = '';
|
|
1029
1062
|
let lastFlush = 0;
|
|
1030
1063
|
let lastThinkingFlush = 0;
|
|
1064
|
+
const rememberedFacts = []; // 🧠 ที่ user สั่งจำใน turn นี้ → โชว์ indicator ใน terminal
|
|
1031
1065
|
try {
|
|
1032
1066
|
const { cost, messages, text } = await runAgent({
|
|
1033
1067
|
model,
|
|
@@ -1039,8 +1073,13 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1039
1073
|
permissionMode,
|
|
1040
1074
|
approve: requestApproval,
|
|
1041
1075
|
signal: ac.signal,
|
|
1076
|
+
usageMeta: { sessionId: sessionId.current, source: 'repl' },
|
|
1042
1077
|
onEvent: (e) => {
|
|
1043
|
-
if (e.type === '
|
|
1078
|
+
if (e.type === 'status' && typeof e.detail === 'string') {
|
|
1079
|
+
setAgentStatus(e.detail);
|
|
1080
|
+
}
|
|
1081
|
+
else if (e.type === 'text') {
|
|
1082
|
+
setAgentStatus((prev) => (prev.startsWith('Codex') || prev.startsWith('Agent') ? 'Writing…' : prev));
|
|
1044
1083
|
buf += e.text ?? '';
|
|
1045
1084
|
const now = Date.now();
|
|
1046
1085
|
if (now - lastFlush > 80) {
|
|
@@ -1049,6 +1088,11 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1049
1088
|
}
|
|
1050
1089
|
}
|
|
1051
1090
|
else if (e.type === 'tool-call') {
|
|
1091
|
+
if (e.tool === 'remember') {
|
|
1092
|
+
const fact = e.detail?.fact;
|
|
1093
|
+
if (typeof fact === 'string' && fact.trim())
|
|
1094
|
+
rememberedFacts.push(fact.trim());
|
|
1095
|
+
}
|
|
1052
1096
|
recordToolTrailEvent(e);
|
|
1053
1097
|
}
|
|
1054
1098
|
else if (e.type === 'tool-result' || e.type === 'error') {
|
|
@@ -1067,7 +1111,11 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1067
1111
|
});
|
|
1068
1112
|
msgsRef.current = messages;
|
|
1069
1113
|
lastCost.current = cost.summary();
|
|
1070
|
-
|
|
1114
|
+
const answerText = buf.trim() || text.trim();
|
|
1115
|
+
addTurn('assistant', answerText, { thinking: snapshotThinking(reasoningBuf), toolTrail: snapshotToolTrail() });
|
|
1116
|
+
// 🧠 indicator: ผู้ใช้สั่งให้จำ → บันทึกถาวรแล้ว (memory + second-brain ถ้าตั้งไว้)
|
|
1117
|
+
for (const fact of rememberedFacts)
|
|
1118
|
+
addTurn('system', `🧠 จำไว้แล้ว: ${fact}`);
|
|
1071
1119
|
// เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
|
|
1072
1120
|
void saveSession({
|
|
1073
1121
|
id: sessionId.current,
|
|
@@ -1077,7 +1125,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1077
1125
|
cwd: process.cwd(),
|
|
1078
1126
|
messages,
|
|
1079
1127
|
});
|
|
1080
|
-
// worklog
|
|
1128
|
+
// worklog (ย่อ) + บทสนทนาเต็ม (ถ้าเปิด brainTranscript) เข้า second-brain
|
|
1081
1129
|
void (async () => {
|
|
1082
1130
|
const brain = await getBrainPath();
|
|
1083
1131
|
if (brain) {
|
|
@@ -1087,6 +1135,31 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1087
1135
|
model,
|
|
1088
1136
|
today: new Date().toISOString().slice(0, 10),
|
|
1089
1137
|
}).catch(() => { });
|
|
1138
|
+
await appendBrainTranscript(brain, {
|
|
1139
|
+
sessionId: sessionId.current,
|
|
1140
|
+
prompt: promptText,
|
|
1141
|
+
answer: answerText,
|
|
1142
|
+
model,
|
|
1143
|
+
// per-turn timestamp → correct HH:MM heading + correct day file (matches the worklog
|
|
1144
|
+
// above); sessionCreated is frozen at session start so it stamped every turn the same.
|
|
1145
|
+
createdIso: new Date().toISOString(),
|
|
1146
|
+
}).catch(() => { });
|
|
1147
|
+
}
|
|
1148
|
+
})();
|
|
1149
|
+
// self-improvement: งานเดิมที่สั่งซ้ำถึง threshold → สร้าง skill อัตโนมัติ + แจ้งใน terminal
|
|
1150
|
+
void (async () => {
|
|
1151
|
+
try {
|
|
1152
|
+
const existing = new Set((await loadSkills()).map((s) => s.name));
|
|
1153
|
+
const result = await maybeAutoSkill(promptText, {
|
|
1154
|
+
synthesize: defaultSkillSynthesizer(model),
|
|
1155
|
+
saveSkill,
|
|
1156
|
+
existingSkillNames: existing,
|
|
1157
|
+
});
|
|
1158
|
+
if (result.created && result.announcement)
|
|
1159
|
+
addTurn('system', result.announcement);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
/* self-improvement เป็น best-effort — ไม่ให้ล้ม turn */
|
|
1090
1163
|
}
|
|
1091
1164
|
})();
|
|
1092
1165
|
}
|
|
@@ -1103,6 +1176,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1103
1176
|
}
|
|
1104
1177
|
finally {
|
|
1105
1178
|
setStreaming('');
|
|
1179
|
+
setAgentStatus('');
|
|
1106
1180
|
resetLiveThinking();
|
|
1107
1181
|
resetLiveToolTrail();
|
|
1108
1182
|
setBusy(false);
|
|
@@ -1117,12 +1191,24 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1117
1191
|
const contextTokens = estimateTokens(msgsRef.current);
|
|
1118
1192
|
const activeQueueIndex = clampQueueActiveIndex(queueActiveIndex, queued.length);
|
|
1119
1193
|
const queueWindow = getQueueWindow(queued.length, activeQueueIndex);
|
|
1120
|
-
|
|
1194
|
+
// gate only — the expanded view renders via ToolTrailView/ActivityRow and the compact view
|
|
1195
|
+
// recomputes its own strings; equivalent to toolTrailLines(...).length without building the array each render.
|
|
1196
|
+
const showToolTrail = toolTrailMode !== 'hidden' && toolTrail.length > 0;
|
|
1197
|
+
// id of the most recent turn that has a tool trail — only it keeps full expanded diffs in
|
|
1198
|
+
// scrollback (older tool turns downgrade to compact). Tracks the trail, not just the last turn,
|
|
1199
|
+
// so trailing text/command turns don't strip detail off the latest tool work.
|
|
1200
|
+
let latestTrailTurnId;
|
|
1201
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
1202
|
+
if (history[i].toolTrail) {
|
|
1203
|
+
latestTrailTurnId = history[i].id;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1121
1207
|
const thinkingView = thinkingPanelLines(thinking, columns, thinkingMode);
|
|
1122
1208
|
const transcriptLimit = transcriptWindowSize(stdout?.rows);
|
|
1123
1209
|
const transcriptView = getTranscriptWindow(history.length, transcriptLimit, transcriptScroll);
|
|
1124
1210
|
const visibleHistory = history.slice(transcriptView.start, transcriptView.end);
|
|
1125
|
-
return (_jsxs(Box, { flexDirection: "column", children: [history.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Banner, { columns: columns, model: model, mode: permissionMode === 'ask' ? 'ask' : 'auto', signals: bannerSignals }), _jsx(SessionPanel, { columns: columns, cwd: cwd, mcp: startupReadiness.mcp, model: model, mode: permissionMode === 'ask' ? 'ask' : 'auto', skills: startupReadiness.skills })] })) : null, transcriptView.showOlder ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", transcriptView.start, " older turns \u00B7 PgUp/Ctrl+U scroll \u00B7 PgDn/Ctrl+D newer"] })) : null, visibleHistory.map((turn) => (_jsx(TurnView, { columns: columns, thinkingMode: thinkingMode, toolTrailMode: toolTrailMode, turn: turn }, `${historyResetKey}-${turn.id}`))), transcriptView.showNewer ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", transcriptView.scrollFromBottom, " newer turns hidden \u00B7 PgDn/Ctrl+D to catch up"] })) : null, thinkingView.length ? _jsx(ThinkingView, { columns: columns, mode: thinkingMode, text: thinking }) : null, streaming ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(StreamingMarkdownText, { columns: columns, text: streaming }) })) : null,
|
|
1211
|
+
return (_jsxs(Box, { flexDirection: "column", children: [history.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Banner, { columns: columns, model: model, mode: permissionMode === 'ask' ? 'ask' : 'auto', signals: bannerSignals }), _jsx(SessionPanel, { columns: columns, cwd: cwd, mcp: startupReadiness.mcp, model: model, mode: permissionMode === 'ask' ? 'ask' : 'auto', skills: startupReadiness.skills })] })) : null, transcriptView.showOlder ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", transcriptView.start, " older turns \u00B7 PgUp/Ctrl+U scroll \u00B7 PgDn/Ctrl+D newer"] })) : null, visibleHistory.map((turn) => (_jsx(TurnView, { columns: columns, isLatest: turn.id === latestTrailTurnId, thinkingMode: thinkingMode, toolTrailMode: toolTrailMode, turn: turn }, `${historyResetKey}-${turn.id}`))), transcriptView.showNewer ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", transcriptView.scrollFromBottom, " newer turns hidden \u00B7 PgDn/Ctrl+D to catch up"] })) : null, thinkingView.length ? _jsx(ThinkingView, { columns: columns, mode: thinkingMode, text: thinking }) : null, streaming ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(StreamingMarkdownText, { columns: columns, text: streaming }) })) : null, showToolTrail ? (_jsx(ToolTrailView, { columns: columns, items: toolTrail, mode: toolTrailMode })) : null, _jsx(FloatingOverlay, { columns: columns, overlay: overlay, pageSize: pagerPageSize }), _jsx(CompletionOverlay, { columns: columns, items: completions, selected: selectedCompletion }), queued.length ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["queued (", queued.length, ") \u00B7 \u2191\u2193 select \u00B7 Ctrl+X delete \u00B7 Esc clears"] }), queueWindow.showLead ? _jsx(Text, { dimColor: true, children: " \u2026" }) : null, queued.slice(queueWindow.start, queueWindow.end).map((q, i) => (_jsxs(Text, { color: queueWindow.start + i === activeQueueIndex ? 'yellow' : undefined, dimColor: queueWindow.start + i !== activeQueueIndex, children: [queueWindow.start + i === activeQueueIndex ? '›' : ' ', " ", queueWindow.start + i + 1, ".", ' ', compactPreview(q, Math.max(16, columns - 10))] }, `${queueWindow.start + i}-${q.slice(0, 16)}`))), queueWindow.showTail ? _jsxs(Text, { dimColor: true, children: [" \u2026and ", queued.length - queueWindow.end, " more"] }) : null] })) : null, approvalReq ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34\u0E23\u0E31\u0E19 ", approvalReq.tool, "?"] }), _jsx(Text, { dimColor: true, children: approvalReq.summary }), _jsx(Text, { dimColor: true, children: "y = \u0E23\u0E31\u0E19 \u00B7 n = \u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18" })] })) : personaOpen ? (_jsx(PersonaOverlay, { onDone: (msg) => { setPersonaOpen(false); addTurn('system', msg); } })) : (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? 'gray' : 'blue', paddingX: 1, children: [_jsx(Text, { color: busy ? 'gray' : 'cyan', children: busy ? '· ' : '› ' }), _jsx(InputView, { value: editor.value, cursor: editor.cursor, busy: busy, agentStatus: agentStatus, toolTrail: toolTrail, columns: columns })] })), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: footerStatus({
|
|
1126
1212
|
branch: gitBranch,
|
|
1127
1213
|
backgroundTaskCount: bgTaskCount,
|
|
1128
1214
|
busy,
|
|
@@ -1138,27 +1224,65 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
1138
1224
|
}) })] }));
|
|
1139
1225
|
}
|
|
1140
1226
|
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
1141
|
-
function InputView({ value, cursor, busy }) {
|
|
1142
|
-
if (busy && !value)
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1227
|
+
function InputView({ value, cursor, busy, agentStatus, toolTrail, columns = 80, }) {
|
|
1228
|
+
if (busy && !value) {
|
|
1229
|
+
const runningTool = toolTrail?.find((item) => item.status === 'running');
|
|
1230
|
+
// prefer the running tool's friendly activity title (e.g. "📖 อ่านไฟล์ src/x.ts", "$ npm test") over
|
|
1231
|
+
// the raw "Tool · read_file" status — same detail the tool trail shows, so the one-line status during a
|
|
1232
|
+
// turn says specifically what's happening; falls back to agentStatus (Thinking…/Writing…/Agent · model).
|
|
1233
|
+
const detail = runningTool?.activity?.title || agentStatus || (runningTool ? `Tool · ${runningTool.name}` : 'Working…');
|
|
1234
|
+
// wrap="truncate-end": status ต้องอยู่ 1 บรรทัดเสมอ — กัน timer/elapsed ทำบรรทัดเด้งหลังส่ง prompt
|
|
1235
|
+
return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [detail, " \u00B7 Esc/Ctrl+C \u0E2B\u0E22\u0E38\u0E14 \u00B7 \u0E1E\u0E34\u0E21\u0E1E\u0E4C\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E15\u0E48\u0E2D\u0E04\u0E34\u0E27 (\u23CE)"] }));
|
|
1236
|
+
}
|
|
1237
|
+
if (!busy && !value) {
|
|
1238
|
+
return (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: "\u0E16\u0E32\u0E21\u0E2D\u0E30\u0E44\u0E23\u0E01\u0E47\u0E44\u0E14\u0E49 \u2014 /help \u0E14\u0E39\u0E04\u0E33\u0E2A\u0E31\u0E48\u0E07 \u00B7 /tools \u0E14\u0E39 tools \u00B7 @\u0E44\u0E1F\u0E25\u0E4C \u0E41\u0E19\u0E1A context/\u0E23\u0E39\u0E1B" }));
|
|
1239
|
+
}
|
|
1240
|
+
// multiline (กด Alt+Enter / ลงท้าย \) — สูงหลายบรรทัดตั้งใจอยู่แล้ว: render grapheme-cursor แบบ wrap ปกติ
|
|
1241
|
+
if (value.includes('\n')) {
|
|
1242
|
+
const ci = cursorGraphemeIndex(value, cursor);
|
|
1243
|
+
const graphemes = graphemesOf(value);
|
|
1244
|
+
const before = graphemes.slice(0, ci).join('');
|
|
1245
|
+
const at = ci < graphemes.length ? graphemes[ci] : ' ';
|
|
1246
|
+
const after = ci < graphemes.length ? graphemes.slice(ci + 1).join('') : '';
|
|
1247
|
+
return (_jsxs(Text, { children: [before, _jsx(Text, { inverse: true, children: at }), after, busy ? _jsxs(Text, { dimColor: true, children: [' ', "(\u23CE \u0E15\u0E48\u0E2D\u0E04\u0E34\u0E27)"] }) : null] }));
|
|
1248
|
+
}
|
|
1249
|
+
// บรรทัดเดียว: viewport กว้างคงที่ (เลื่อนแนวนอนแทน wrap) → กล่อง input สูง 1 บรรทัดเสมอ ไม่เด้งตอนพิมพ์ไทย
|
|
1250
|
+
// เผื่อ overhead: border(2) + paddingX(2) + prefix "› "(2) + ช่อง cursor/suffix ~2
|
|
1251
|
+
const queueHint = busy ? ' (⏎ ต่อคิว)' : '';
|
|
1252
|
+
const reserved = 8 + queueHint.length;
|
|
1253
|
+
const vp = inputViewport(value, cursor, Math.max(8, columns - reserved));
|
|
1254
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [vp.lead ? _jsx(Text, { dimColor: true, children: SCROLL_LEAD }) : null, vp.before, _jsx(Text, { inverse: true, children: vp.at }), vp.after, vp.tail ? _jsx(Text, { dimColor: true, children: SCROLL_TAIL }) : null, queueHint ? _jsx(Text, { dimColor: true, children: queueHint }) : null] }));
|
|
1255
|
+
}
|
|
1256
|
+
function statusColor(status) {
|
|
1257
|
+
return status === 'error' ? 'red' : status === 'running' ? 'yellow' : 'green';
|
|
1258
|
+
}
|
|
1259
|
+
function statusMarker(status) {
|
|
1260
|
+
return status === 'error' ? '✗' : status === 'running' ? '›' : '✓';
|
|
1261
|
+
}
|
|
1262
|
+
// total diff rows rendered per tool row — a hard per-item height bound. diffLines caps each SIDE at
|
|
1263
|
+
// MAX_DIFF_LINES, so a two-sided edit can exceed this; we cap the combined rows here and show a plain
|
|
1264
|
+
// "…" (no count — the inner per-side "…(+N)" sentinels already carry the numbers, so we don't double-count).
|
|
1265
|
+
const MAX_ROW_DIFF_LINES = 14;
|
|
1266
|
+
/** one tool's activity: a friendly title line + colored diff (green +, red -), height-bounded. */
|
|
1267
|
+
function ActivityRow({ item, width }) {
|
|
1268
|
+
const title = item.activity?.title ?? item.name;
|
|
1269
|
+
const fullDiff = item.activity?.diff;
|
|
1270
|
+
const diff = fullDiff?.slice(0, MAX_ROW_DIFF_LINES);
|
|
1271
|
+
const diffClipped = (fullDiff?.length ?? 0) > MAX_ROW_DIFF_LINES;
|
|
1272
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: statusColor(item.status), wrap: "truncate-end", children: [statusMarker(item.status), " ", title] }), diff?.map((line, idx) => (_jsxs(Text, { color: line.sign === '+' ? 'green' : line.sign === '-' ? 'red' : undefined, dimColor: line.sign === ' ', wrap: "truncate-end", children: [' ', line.sign === ' ' ? ' ' : line.sign, " ", line.text.slice(0, Math.max(0, width - 4))] }, `d-${item.id}-${idx}`))), diffClipped ? _jsx(Text, { dimColor: true, children: ' …' }) : null, item.status !== 'running' && item.detail ? (_jsxs(Text, { color: item.status === 'error' ? 'red' : undefined, dimColor: true, wrap: "truncate-end", children: [' ↳ ', item.detail.slice(0, Math.max(0, width - 6))] })) : null] }));
|
|
1150
1273
|
}
|
|
1151
1274
|
function ToolTrailView({ columns, items, mode }) {
|
|
1152
|
-
|
|
1153
|
-
if (!lines.length)
|
|
1275
|
+
if (mode === 'hidden' || !items.length)
|
|
1154
1276
|
return null;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1277
|
+
const header = toolTrailHeader(items, mode);
|
|
1278
|
+
// compact mode keeps the terse one-line string rendering
|
|
1279
|
+
if (mode === 'compact') {
|
|
1280
|
+
const lines = toolTrailLines(items, columns, mode);
|
|
1281
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'cyan' : undefined, dimColor: index > 0, wrap: "truncate-end", children: line }, `${index}-${line}`))) }));
|
|
1282
|
+
}
|
|
1283
|
+
// expanded: rich, colored per-tool activity with diffs
|
|
1284
|
+
const width = toolTrailWidth(columns);
|
|
1285
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "cyan", wrap: "truncate-end", children: header[0] }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: header[1] }), items.map((item) => (_jsx(ActivityRow, { item: item, width: width }, item.id)))] }));
|
|
1162
1286
|
}
|
|
1163
1287
|
function ThinkingView({ columns, mode, text }) {
|
|
1164
1288
|
const lines = thinkingPanelLines(text, columns, mode);
|
|
@@ -1166,10 +1290,10 @@ function ThinkingView({ columns, mode, text }) {
|
|
|
1166
1290
|
return null;
|
|
1167
1291
|
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'cyan' : undefined, dimColor: index > 0, wrap: "truncate-end", children: line }, `${index}-${line}`))) }));
|
|
1168
1292
|
}
|
|
1169
|
-
function TurnView({ columns, thinkingMode, toolTrailMode, turn, }) {
|
|
1293
|
+
function TurnView({ columns, isLatest, thinkingMode, toolTrailMode, turn, }) {
|
|
1170
1294
|
if (turn.role === 'system')
|
|
1171
1295
|
return _jsx(Text, { dimColor: true, children: turn.text });
|
|
1172
1296
|
if (turn.role === 'user')
|
|
1173
1297
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { color: "cyan", children: turn.text })] }));
|
|
1174
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [turn.thinking ? _jsx(ThinkingView, { columns: columns, mode: thinkingMode, text: turn.thinking }) : null, _jsx(MarkdownText, { columns: columns, text: turn.text }), turn.toolTrail ? _jsx(ToolTrailView, { columns: columns, items: turn.toolTrail, mode: toolTrailMode }) : null] }));
|
|
1298
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [turn.thinking ? _jsx(ThinkingView, { columns: columns, mode: thinkingMode, text: turn.thinking }) : null, _jsx(MarkdownText, { columns: columns, text: turn.text }), turn.toolTrail ? (_jsx(ToolTrailView, { columns: columns, items: turn.toolTrail, mode: isLatest || toolTrailMode !== 'expanded' ? toolTrailMode : 'compact' })) : null] }));
|
|
1175
1299
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
import { graphemeBoundaries } from './useEditor.js';
|
|
3
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Stable, Thai-safe rendering of the REPL input line.
|
|
5
|
+
//
|
|
6
|
+
// Two bugs this fixes (เทียบกับ CLI เจ้าอื่นที่ "นิ่ง"):
|
|
7
|
+
// 1) cursor split a grapheme cluster — the old code did value.slice(cursor, cursor+1),
|
|
8
|
+
// which on Thai cuts a base char away from its combining vowel/tone mark (สระ/วรรณยุกต์
|
|
9
|
+
// เป็น zero-width). The orphaned mark then renders on its own cell → "อักษรห่างเกินไป".
|
|
10
|
+
// Fix: the cursor highlights a WHOLE grapheme cluster (base + all its marks).
|
|
11
|
+
// 2) the line bounced between 1 and 2 rows while typing — a wrapping <Text> grows the box
|
|
12
|
+
// vertically the moment content crosses the right edge, shoving the footer down on every
|
|
13
|
+
// keystroke. Fix: a fixed-width horizontal viewport (readline-style) so the input box is
|
|
14
|
+
// always exactly one row; long lines scroll left with ‹ / › markers instead of wrapping.
|
|
15
|
+
//
|
|
16
|
+
// Display width is measured with string-width (the same lib Ink wraps with), so Thai combining
|
|
17
|
+
// marks count as 0 and the window math matches what the terminal actually paints.
|
|
18
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
export const SCROLL_LEAD = '‹';
|
|
20
|
+
export const SCROLL_TAIL = '›';
|
|
21
|
+
/** split a string into grapheme clusters (base char + its combining marks stay together) */
|
|
22
|
+
export function graphemesOf(value) {
|
|
23
|
+
const bounds = graphemeBoundaries(value);
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let i = 0; i < bounds.length - 1; i += 1)
|
|
26
|
+
out.push(value.slice(bounds[i], bounds[i + 1]));
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/** grapheme-cluster index that a code-unit cursor sits at (0..graphemeCount) */
|
|
30
|
+
export function cursorGraphemeIndex(value, cursor) {
|
|
31
|
+
const bounds = graphemeBoundaries(value);
|
|
32
|
+
let index = 0;
|
|
33
|
+
for (let i = 0; i < bounds.length; i += 1) {
|
|
34
|
+
if (bounds[i] <= cursor)
|
|
35
|
+
index = i;
|
|
36
|
+
else
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return index;
|
|
40
|
+
}
|
|
41
|
+
/** display width of one grapheme, never less than 1 cell (so the cursor always has a cell) */
|
|
42
|
+
function cellWidth(grapheme) {
|
|
43
|
+
return Math.max(1, stringWidth(grapheme));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Compute the visible window for a single physical line.
|
|
47
|
+
* `width` = number of terminal columns available to the value (caller subtracts prefix/border/padding).
|
|
48
|
+
* Returns the slice around the cursor that fits, plus whether truncation markers are needed.
|
|
49
|
+
*/
|
|
50
|
+
export function inputViewport(value, cursor, width) {
|
|
51
|
+
const w = Math.max(4, Math.floor(width));
|
|
52
|
+
const graphemes = graphemesOf(value);
|
|
53
|
+
const ci = cursorGraphemeIndex(value, cursor);
|
|
54
|
+
// a trailing sentinel cell so a cursor parked at end-of-line still has somewhere to sit
|
|
55
|
+
const units = [...graphemes.map((g) => ({ text: g, width: cellWidth(g) })), { text: ' ', width: 1 }];
|
|
56
|
+
const cursorUnit = Math.min(ci, units.length - 1);
|
|
57
|
+
const totalWidth = units.reduce((sum, u) => sum + u.width, 0);
|
|
58
|
+
if (totalWidth <= w) {
|
|
59
|
+
return {
|
|
60
|
+
lead: false,
|
|
61
|
+
before: graphemes.slice(0, cursorUnit).join(''),
|
|
62
|
+
at: cursorUnit < graphemes.length ? graphemes[cursorUnit] : ' ',
|
|
63
|
+
after: cursorUnit < graphemes.length ? graphemes.slice(cursorUnit + 1).join('') : '',
|
|
64
|
+
tail: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// overflow → slide a window that always contains the cursor unit; reserve 1 cell for each
|
|
68
|
+
// truncation marker that will actually be shown.
|
|
69
|
+
let start = cursorUnit;
|
|
70
|
+
let end = cursorUnit + 1;
|
|
71
|
+
let used = units[cursorUnit].width;
|
|
72
|
+
// extend right first (so typing at end keeps the tail in view), then backfill left context.
|
|
73
|
+
// the marker reservations (start>0 ⇒ ‹, end<len ⇒ ›) are folded into each fit check.
|
|
74
|
+
while (end < units.length) {
|
|
75
|
+
const next = units[end].width;
|
|
76
|
+
if (used + next + (start > 0 ? 1 : 0) + (end + 1 < units.length ? 1 : 0) <= w) {
|
|
77
|
+
used += next;
|
|
78
|
+
end += 1;
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
while (start > 0) {
|
|
84
|
+
const prev = units[start - 1].width;
|
|
85
|
+
if (used + prev + (start - 1 > 0 ? 1 : 0) + (end < units.length ? 1 : 0) <= w) {
|
|
86
|
+
used += prev;
|
|
87
|
+
start -= 1;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const slice = (from, to) => units
|
|
93
|
+
.slice(from, to)
|
|
94
|
+
.map((u) => u.text)
|
|
95
|
+
.join('');
|
|
96
|
+
const atUnit = units[cursorUnit];
|
|
97
|
+
return {
|
|
98
|
+
lead: start > 0,
|
|
99
|
+
before: slice(start, cursorUnit),
|
|
100
|
+
at: cursorUnit === units.length - 1 ? ' ' : atUnit.text,
|
|
101
|
+
after: slice(cursorUnit + 1, end),
|
|
102
|
+
tail: end < units.length,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { TextInput, Select } from '@inkjs/ui';
|
|
5
|
+
import { PERSONA_QUESTIONS, PERSONA_OTHER } from '../persona.js';
|
|
6
|
+
/**
|
|
7
|
+
* Interactive persona questionnaire (`sanook persona`). Walks PERSONA_QUESTIONS one at a
|
|
8
|
+
* time — A/B/C/D Selects and free-text inputs. A select "อื่นๆ (พิมพ์เอง)" drops into a
|
|
9
|
+
* free-text follow-up for that same question. Esc goes back one step. Calls onComplete with
|
|
10
|
+
* the full answers map when finished.
|
|
11
|
+
*/
|
|
12
|
+
export function PersonaWizard({ onComplete, initialAnswers = {}, }) {
|
|
13
|
+
const total = PERSONA_QUESTIONS.length;
|
|
14
|
+
const [index, setIndex] = useState(0);
|
|
15
|
+
const [answers, setAnswers] = useState(initialAnswers);
|
|
16
|
+
const [otherMode, setOtherMode] = useState(false);
|
|
17
|
+
const prefilled = Object.keys(initialAnswers).filter((k) => initialAnswers[k]?.trim()).length;
|
|
18
|
+
const q = PERSONA_QUESTIONS[index];
|
|
19
|
+
const goBack = () => {
|
|
20
|
+
if (otherMode) {
|
|
21
|
+
setOtherMode(false);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (index > 0)
|
|
25
|
+
setIndex(index - 1);
|
|
26
|
+
};
|
|
27
|
+
useInput((_input, key) => {
|
|
28
|
+
if (key.escape)
|
|
29
|
+
goBack();
|
|
30
|
+
});
|
|
31
|
+
const advance = (value) => {
|
|
32
|
+
const next = { ...answers, [q.id]: value };
|
|
33
|
+
setAnswers(next);
|
|
34
|
+
setOtherMode(false);
|
|
35
|
+
if (index + 1 >= total) {
|
|
36
|
+
onComplete(next);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
setIndex(index + 1);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const showTextInput = q.type === 'text' || otherMode;
|
|
43
|
+
// On Esc-back, surface the previously-chosen option FIRST so the highlight lands on it. @inkjs/ui
|
|
44
|
+
// Select always focuses the first option on (re)mount and ignores defaultValue for the highlight, so
|
|
45
|
+
// with no defaultValue pressing Enter selects the focused (first) option — making that the prior
|
|
46
|
+
// answer keeps "Esc-back then Enter to re-confirm" from silently overwriting it with the first option.
|
|
47
|
+
// If the prior answer was a custom "อื่นๆ" value (not a literal option), highlight the PERSONA_OTHER
|
|
48
|
+
// option so Enter re-enters other-mode (where the TextInput restores the typed text via defaultValue).
|
|
49
|
+
const selectOptions = (() => {
|
|
50
|
+
const opts = (q.options ?? []).map((o) => ({ label: o.label, value: o.value }));
|
|
51
|
+
const prior = answers[q.id];
|
|
52
|
+
if (!prior)
|
|
53
|
+
return opts;
|
|
54
|
+
let i = opts.findIndex((o) => o.value === prior);
|
|
55
|
+
if (i < 0)
|
|
56
|
+
i = opts.findIndex((o) => o.value === PERSONA_OTHER); // custom typed value → highlight "อื่นๆ"
|
|
57
|
+
return i > 0 ? [opts[i], ...opts.slice(0, i), ...opts.slice(i + 1)] : opts;
|
|
58
|
+
})();
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["\uD83E\uDEAA \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 Persona ", _jsx(Text, { color: "gray", children: "\u2014 \u0E1A\u0E2D\u0E01 AI \u0E27\u0E48\u0E32\u0E04\u0E38\u0E13\u0E40\u0E1B\u0E47\u0E19\u0E43\u0E04\u0E23 + \u0E2D\u0E22\u0E32\u0E01\u0E43\u0E2B\u0E49\u0E17\u0E33\u0E07\u0E32\u0E19\u0E22\u0E31\u0E07\u0E44\u0E07" })] }), _jsxs(Text, { color: "gray", children: ["\u0E02\u0E49\u0E2D ", index + 1, "/", total, " \u00B7 Esc = \u0E22\u0E49\u0E2D\u0E19\u0E01\u0E25\u0E31\u0E1A \u00B7 Ctrl+C = \u0E2D\u0E2D\u0E01", prefilled ? ` · โหลดค่าเดิม ${prefilled} ข้อ` : ''] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: q.prompt }), showTextInput ? (_jsx(TextInput, { defaultValue: answers[q.id] ?? '', placeholder: otherMode ? 'พิมพ์คำตอบของคุณ…' : (q.placeholder ?? ''), onSubmit: (v) => advance(v.trim()) }, `text-${index}-${otherMode ? 'other' : 'main'}`)) : (_jsx(Select, { options: selectOptions, onChange: (v) => {
|
|
60
|
+
if (v === PERSONA_OTHER)
|
|
61
|
+
setOtherMode(true);
|
|
62
|
+
else
|
|
63
|
+
advance(v);
|
|
64
|
+
} }, `select-${index}`))] })] }));
|
|
65
|
+
}
|
|
66
|
+
/** REPL overlay — `/persona` loads existing answers then runs the same wizard inline */
|
|
67
|
+
export function PersonaOverlay({ onDone }) {
|
|
68
|
+
const [initialAnswers, setInitialAnswers] = useState(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
void import('../memory.js')
|
|
71
|
+
.then((m) => m.loadPersonaAnswers())
|
|
72
|
+
.then(setInitialAnswers)
|
|
73
|
+
.catch(() => setInitialAnswers({}));
|
|
74
|
+
}, []);
|
|
75
|
+
if (!initialAnswers) {
|
|
76
|
+
return (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "gray", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14 persona\u2026" }) }));
|
|
77
|
+
}
|
|
78
|
+
return (_jsx(PersonaWizard, { initialAnswers: initialAnswers, onComplete: (answers) => {
|
|
79
|
+
void (async () => {
|
|
80
|
+
const { persistPersonaAnswers } = await import('../memory.js');
|
|
81
|
+
const { memoryWritten, vaultWritten, brainPath } = await persistPersonaAnswers(answers);
|
|
82
|
+
const memLine = memoryWritten > 0
|
|
83
|
+
? `จำเข้า memory ${memoryWritten} ข้อ`
|
|
84
|
+
: 'ไม่มีข้อมูลใหม่ (ตรงกับของเดิม)';
|
|
85
|
+
const vaultLine = vaultWritten ? ` · vault: ${brainPath}/Shared/User-Persona/persona.md` : '';
|
|
86
|
+
onDone(`✅ บันทึก Persona — ${memLine}${vaultLine}`);
|
|
87
|
+
})();
|
|
88
|
+
} }));
|
|
89
|
+
}
|