sanook-cli 0.5.2 → 0.5.5
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 +91 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +623 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain.js +3 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +94 -14
- package/dist/config.js +31 -5
- package/dist/context-pack.js +145 -0
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +30 -11
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +34 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +835 -29
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +163 -50
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +32 -6
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +2 -2
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
package/dist/ui/app.js
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef } from 'react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useRef } from 'react';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
-
import { Box, Text,
|
|
6
|
-
import {
|
|
5
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { BUILTIN_COMMANDS, HELP_TEXT, expandCustomCommand, loadCustomCommands, parseCommand, parseSlashInvocation, } from '../commands.js';
|
|
7
8
|
import { runAgent } from '../loop.js';
|
|
8
|
-
import { saveSession, newSessionId } from '../session.js';
|
|
9
|
+
import { saveSession, newSessionId, listSessions, removeSession, renameSession } from '../session.js';
|
|
10
|
+
import { TOOL_CATALOG } from '../tool-catalog.js';
|
|
9
11
|
import { getBrainPath, appendBrainWorklog } from '../memory.js';
|
|
10
12
|
import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
|
|
11
13
|
import { makeSummarizer } from '../summarize.js';
|
|
12
14
|
import { agentTuning, patchGlobalConfig } from '../config.js';
|
|
13
15
|
import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
|
|
14
16
|
import { renderInsights } from '../insights.js';
|
|
17
|
+
import { loadMcpHubEntries } from '../mcp-hub.js';
|
|
18
|
+
import { probeMcpServer } from '../mcp.js';
|
|
19
|
+
import { filterModelPickerOptions, initialModelPickerIndex, modelPickerOptions, modelProviderEntries, } from '../model-picker.js';
|
|
20
|
+
import { clampCompletionIndex, completionForInput, completionReplaceValue } from '../slash-completion.js';
|
|
21
|
+
import { loadSkills } from '../skills.js';
|
|
22
|
+
import { copyTextToClipboard } from '../clipboard.js';
|
|
15
23
|
import { useEditor } from './useEditor.js';
|
|
24
|
+
import { useBusyElapsedSeconds } from './useBusyElapsed.js';
|
|
25
|
+
import { useGitBranch } from './useGitBranch.js';
|
|
16
26
|
import { loadHistory, appendHistory } from './history.js';
|
|
17
27
|
import { expandMentions } from './mentions.js';
|
|
18
28
|
import { BRAND } from '../brand.js';
|
|
29
|
+
import { backgroundTaskRunningCount, listBackgroundTasks } from '../tools/task.js';
|
|
19
30
|
import { Banner } from './banner.js';
|
|
31
|
+
import { CompletionOverlay, FloatingOverlay, firstUserSummary } from './overlay.js';
|
|
32
|
+
import { clampQueueActiveIndex, compactPreview, getQueueWindow, queueActiveIndexAfterDelete } from './queue.js';
|
|
33
|
+
import { MarkdownText, StreamingMarkdownText } from './markdown.js';
|
|
34
|
+
import { SessionPanel } from './session-panel.js';
|
|
35
|
+
import { getTranscriptWindow, transcriptScrollStep, transcriptWindowSize } from './transcript.js';
|
|
36
|
+
import { footerStatus } from './status.js';
|
|
37
|
+
import { thinkingPanelLines, snapshotThinking } from './thinking-panel.js';
|
|
38
|
+
import { toolTrailLines, updateToolTrailOnEvent } from './tool-trail.js';
|
|
20
39
|
const execFileP = promisify(execFile);
|
|
21
40
|
const PRE_TURN_COMPACT_TOKENS = 100_000; // session ยาวมากเท่านั้นถึง summarize ก่อน turn (mode summarize)
|
|
41
|
+
const startupCount = (value) => value === 'checking' ? 'checking' : value.count ? `${value.count}` : 'none';
|
|
42
|
+
const shortSignal = (value, max = 18) => value.length > max ? `…${value.slice(Math.max(0, value.length - max + 1))}` : value;
|
|
22
43
|
export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory, initialNote }) {
|
|
23
44
|
const { exit } = useApp();
|
|
45
|
+
const { stdout } = useStdout();
|
|
24
46
|
const [history, setHistory] = useState(() => {
|
|
25
47
|
const seed = [];
|
|
26
48
|
if (initialNote)
|
|
@@ -30,11 +52,25 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
30
52
|
return seed;
|
|
31
53
|
});
|
|
32
54
|
const [streaming, setStreaming] = useState('');
|
|
55
|
+
const [thinking, setThinking] = useState('');
|
|
56
|
+
const [toolTrail, setToolTrail] = useState([]);
|
|
33
57
|
const [busy, setBusy] = useState(false);
|
|
34
58
|
const [model, setModel] = useState(initialModel);
|
|
35
59
|
const [approvalReq, setApprovalReq] = useState(null);
|
|
60
|
+
const [overlay, setOverlay] = useState(null);
|
|
61
|
+
const [completionIndex, setCompletionIndex] = useState(0);
|
|
62
|
+
const [historyResetKey, setHistoryResetKey] = useState(0);
|
|
63
|
+
const [queueActiveIndex, setQueueActiveIndex] = useState(null);
|
|
64
|
+
const [toolTrailMode, setToolTrailModeState] = useState('expanded');
|
|
65
|
+
const [thinkingMode, setThinkingMode] = useState('collapsed');
|
|
66
|
+
const [contextCompression, setContextCompression] = useState();
|
|
67
|
+
const [transcriptScroll, setTranscriptScroll] = useState(0);
|
|
36
68
|
const idRef = useRef(0);
|
|
37
69
|
const lastCost = useRef('');
|
|
70
|
+
const nextToolTrailId = useRef(0);
|
|
71
|
+
const toolTrailRef = useRef([]);
|
|
72
|
+
const toolTrailModeRef = useRef('expanded');
|
|
73
|
+
const thinkingRef = useRef('');
|
|
38
74
|
const msgsRef = useRef(initialHistory ?? []); // conversation จริงสำหรับ LLM (สะสมข้ามรอบ)
|
|
39
75
|
const sessionId = useRef(newSessionId());
|
|
40
76
|
const sessionCreated = useRef(new Date().toISOString());
|
|
@@ -43,24 +79,186 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
43
79
|
const checkpoints = useRef([]);
|
|
44
80
|
const lastRun = useRef(null);
|
|
45
81
|
const editor = useEditor(replHistory.current);
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
const [startupReadiness, setStartupReadiness] = useState({
|
|
84
|
+
brain: 'checking',
|
|
85
|
+
mcp: 'checking',
|
|
86
|
+
skills: 'checking',
|
|
87
|
+
});
|
|
46
88
|
// real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
|
|
47
89
|
const abortRef = useRef(null);
|
|
48
90
|
const queueRef = useRef([]);
|
|
49
91
|
const [queued, setQueued] = useState([]);
|
|
92
|
+
const [bgTaskCount, setBgTaskCount] = useState(0);
|
|
50
93
|
const enqueue = (msg) => {
|
|
51
94
|
queueRef.current.push(msg);
|
|
52
95
|
setQueued([...queueRef.current]);
|
|
96
|
+
setQueueActiveIndex((index) => clampQueueActiveIndex(index, queueRef.current.length));
|
|
53
97
|
};
|
|
54
98
|
const dequeue = () => {
|
|
55
99
|
const m = queueRef.current.shift();
|
|
56
100
|
setQueued([...queueRef.current]);
|
|
101
|
+
setQueueActiveIndex((index) => {
|
|
102
|
+
if (!queueRef.current.length)
|
|
103
|
+
return null;
|
|
104
|
+
if (index === null)
|
|
105
|
+
return 0;
|
|
106
|
+
return clampQueueActiveIndex(index - 1, queueRef.current.length);
|
|
107
|
+
});
|
|
57
108
|
return m;
|
|
58
109
|
};
|
|
59
110
|
const clearQueue = () => {
|
|
60
111
|
queueRef.current = [];
|
|
61
112
|
setQueued([]);
|
|
113
|
+
setQueueActiveIndex(null);
|
|
114
|
+
};
|
|
115
|
+
const moveQueueActive = (delta) => {
|
|
116
|
+
setQueueActiveIndex((index) => {
|
|
117
|
+
const active = clampQueueActiveIndex(index, queueRef.current.length);
|
|
118
|
+
return active === null ? null : clampQueueActiveIndex(active + delta, queueRef.current.length);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const removeActiveQueued = () => {
|
|
122
|
+
const length = queueRef.current.length;
|
|
123
|
+
const active = clampQueueActiveIndex(queueActiveIndex, length);
|
|
124
|
+
if (active === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
const [removed] = queueRef.current.splice(active, 1);
|
|
127
|
+
setQueued([...queueRef.current]);
|
|
128
|
+
setQueueActiveIndex(queueActiveIndexAfterDelete(active, length));
|
|
129
|
+
return removed;
|
|
130
|
+
};
|
|
131
|
+
const resetLiveToolTrail = () => {
|
|
132
|
+
nextToolTrailId.current = 0;
|
|
133
|
+
toolTrailRef.current = [];
|
|
134
|
+
setToolTrail([]);
|
|
135
|
+
};
|
|
136
|
+
const resetLiveThinking = () => {
|
|
137
|
+
thinkingRef.current = '';
|
|
138
|
+
setThinking('');
|
|
139
|
+
};
|
|
140
|
+
const setToolTrailMode = (mode) => {
|
|
141
|
+
toolTrailModeRef.current = mode;
|
|
142
|
+
setToolTrailModeState(mode);
|
|
143
|
+
// NOTE: this remount is load-bearing — the transcript lives in <Static>, which freezes already-
|
|
144
|
+
// emitted turns; bumping the key is what re-renders past turns in the new mode. (Cost: a full
|
|
145
|
+
// scrollback re-emit on toggle — a known <Static> trade-off, not removable without a rewrite.)
|
|
146
|
+
setHistoryResetKey((key) => key + 1);
|
|
147
|
+
};
|
|
148
|
+
const changeToolTrailMode = (mode) => {
|
|
149
|
+
const next = mode ?? (toolTrailModeRef.current === 'expanded' ? 'compact' : 'expanded');
|
|
150
|
+
setToolTrailMode(next);
|
|
151
|
+
return next;
|
|
152
|
+
};
|
|
153
|
+
const noteToolTrailMode = (mode) => {
|
|
154
|
+
addTurn('system', `tool trail → ${mode} (${mode === 'compact' ? 'สรุปสั้น' : mode === 'hidden' ? 'ซ่อน' : 'แสดงรายละเอียด'})`);
|
|
155
|
+
};
|
|
156
|
+
const snapshotToolTrail = () => toolTrailRef.current.length ? toolTrailRef.current.map((item) => ({ ...item })) : undefined;
|
|
157
|
+
const applyDetailsMode = (section, mode) => {
|
|
158
|
+
if (!section || !mode)
|
|
159
|
+
return;
|
|
160
|
+
if (section === 'thinking') {
|
|
161
|
+
setThinkingMode(mode);
|
|
162
|
+
setHistoryResetKey((key) => key + 1); // remount needed to restyle frozen <Static> turns (see setToolTrailMode)
|
|
163
|
+
addTurn('system', `details thinking → ${mode}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const nextToolMode = mode === 'expanded' ? 'expanded' : mode === 'hidden' ? 'hidden' : 'compact';
|
|
167
|
+
noteToolTrailMode(changeToolTrailMode(nextToolMode));
|
|
168
|
+
};
|
|
169
|
+
const addTurn = (role, text, extras) => {
|
|
170
|
+
setTranscriptScroll(0);
|
|
171
|
+
setHistory((h) => [
|
|
172
|
+
...h,
|
|
173
|
+
{
|
|
174
|
+
id: idRef.current++,
|
|
175
|
+
role,
|
|
176
|
+
thinking: extras?.thinking,
|
|
177
|
+
text,
|
|
178
|
+
toolTrail: extras?.toolTrail?.length ? extras.toolTrail.map((item) => ({ ...item })) : undefined,
|
|
179
|
+
},
|
|
180
|
+
]);
|
|
181
|
+
};
|
|
182
|
+
const recordToolTrailEvent = (event) => {
|
|
183
|
+
if (event.type !== 'tool-call' && event.type !== 'tool-result' && event.type !== 'error')
|
|
184
|
+
return;
|
|
185
|
+
const type = event.type === 'tool-call' ? 'tool-call' : event.type === 'tool-result' ? 'tool-result' : 'error';
|
|
186
|
+
const next = updateToolTrailOnEvent(toolTrailRef.current, { detail: event.detail, text: event.text, tool: event.tool, type }, nextToolTrailId.current);
|
|
187
|
+
nextToolTrailId.current = next.nextId;
|
|
188
|
+
toolTrailRef.current = next.items;
|
|
189
|
+
setToolTrail(next.items);
|
|
190
|
+
};
|
|
191
|
+
const replaceHistory = (next) => {
|
|
192
|
+
setHistoryResetKey((key) => key + 1);
|
|
193
|
+
setTranscriptScroll(0);
|
|
194
|
+
setHistory(next);
|
|
195
|
+
};
|
|
196
|
+
const filterHistory = (predicate) => {
|
|
197
|
+
setHistoryResetKey((key) => key + 1);
|
|
198
|
+
setTranscriptScroll(0);
|
|
199
|
+
setHistory((h) => h.filter(predicate));
|
|
200
|
+
};
|
|
201
|
+
const gitBranch = useGitBranch(cwd);
|
|
202
|
+
const busyElapsedSeconds = useBusyElapsedSeconds(busy);
|
|
203
|
+
const columns = Math.max(20, stdout?.columns ?? 80);
|
|
204
|
+
const pagerPageSize = Math.max(5, Math.min(18, (stdout?.rows ?? 24) - 10));
|
|
205
|
+
const completion = !overlay && !busy ? completionForInput(editor.value, cwd) : { items: [], replaceFrom: 0 };
|
|
206
|
+
const completions = completion.items;
|
|
207
|
+
const selectedCompletion = clampCompletionIndex(completionIndex, completions.length);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
let alive = true;
|
|
210
|
+
void agentTuning()
|
|
211
|
+
.then((tuning) => {
|
|
212
|
+
if (alive)
|
|
213
|
+
setContextCompression(tuning.contextCompression);
|
|
214
|
+
})
|
|
215
|
+
.catch(() => {
|
|
216
|
+
if (alive)
|
|
217
|
+
setContextCompression(undefined);
|
|
218
|
+
});
|
|
219
|
+
return () => {
|
|
220
|
+
alive = false;
|
|
221
|
+
};
|
|
222
|
+
}, []);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
let alive = true;
|
|
225
|
+
void Promise.allSettled([getBrainPath(), loadMcpHubEntries(cwd), loadSkills(cwd)]).then(([brain, mcp, skills]) => {
|
|
226
|
+
if (!alive)
|
|
227
|
+
return;
|
|
228
|
+
setStartupReadiness({
|
|
229
|
+
brain: brain.status === 'fulfilled' && brain.value ? 'ready' : 'missing',
|
|
230
|
+
mcp: mcp.status === 'fulfilled'
|
|
231
|
+
? { count: mcp.value.entries.length, names: mcp.value.entries.map((entry) => entry.name) }
|
|
232
|
+
: { count: 0, names: [] },
|
|
233
|
+
skills: skills.status === 'fulfilled'
|
|
234
|
+
? { count: skills.value.length, names: skills.value.map((skill) => skill.name) }
|
|
235
|
+
: { count: 0, names: [] },
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
return () => {
|
|
239
|
+
alive = false;
|
|
240
|
+
};
|
|
241
|
+
}, [cwd]);
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const refresh = () => setBgTaskCount(backgroundTaskRunningCount());
|
|
244
|
+
refresh();
|
|
245
|
+
const timer = setInterval(refresh, 2000);
|
|
246
|
+
return () => clearInterval(timer);
|
|
247
|
+
}, []);
|
|
248
|
+
const bannerSignals = [
|
|
249
|
+
{ label: 'brain', tone: startupReadiness.brain === 'ready' ? 'ready' : startupReadiness.brain === 'checking' ? 'muted' : 'warn', value: startupReadiness.brain },
|
|
250
|
+
{ label: 'mcp', tone: startupReadiness.mcp === 'checking' ? 'muted' : startupReadiness.mcp.count ? 'ready' : 'warn', value: startupCount(startupReadiness.mcp) },
|
|
251
|
+
{ label: 'skills', tone: startupReadiness.skills === 'checking' ? 'muted' : startupReadiness.skills.count ? 'ready' : 'warn', value: startupCount(startupReadiness.skills) },
|
|
252
|
+
...(gitBranch ? [{ label: 'git', tone: 'ready', value: shortSignal(gitBranch) }] : []),
|
|
253
|
+
];
|
|
254
|
+
const applyCompletion = () => {
|
|
255
|
+
const next = completionReplaceValue(editor.value, completions[selectedCompletion], completion.replaceFrom);
|
|
256
|
+
if (!next)
|
|
257
|
+
return false;
|
|
258
|
+
editor.setValue(next);
|
|
259
|
+
setCompletionIndex(0);
|
|
260
|
+
return true;
|
|
62
261
|
};
|
|
63
|
-
const addTurn = (role, text) => setHistory((h) => [...h, { id: idRef.current++, role, text }]);
|
|
64
262
|
// /diff /undo — git-backed (execFile ไม่ผ่าน shell)
|
|
65
263
|
async function runGit(args, label) {
|
|
66
264
|
try {
|
|
@@ -76,7 +274,460 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
76
274
|
approvalResolve.current = resolve;
|
|
77
275
|
setApprovalReq({ tool, summary });
|
|
78
276
|
});
|
|
277
|
+
const openModelPicker = () => {
|
|
278
|
+
const providers = modelProviderEntries();
|
|
279
|
+
const options = modelPickerOptions(model);
|
|
280
|
+
setOverlay({
|
|
281
|
+
kind: 'model',
|
|
282
|
+
phase: 'provider',
|
|
283
|
+
providers,
|
|
284
|
+
options,
|
|
285
|
+
selected: 0,
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
const openMcpHub = async () => {
|
|
289
|
+
try {
|
|
290
|
+
const state = await loadMcpHubEntries(process.cwd());
|
|
291
|
+
setOverlay({ detail: false, kind: 'mcp', notes: state.notes, selected: 0, servers: state.entries });
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
addTurn('system', `mcp: ${e.message}`);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const moveMcpHub = (delta) => {
|
|
298
|
+
setOverlay((current) => {
|
|
299
|
+
if (current?.kind !== 'mcp' || current.detail)
|
|
300
|
+
return current;
|
|
301
|
+
const last = Math.max(0, current.servers.length - 1);
|
|
302
|
+
return { ...current, probe: undefined, selected: Math.max(0, Math.min(last, current.selected + delta)), toolSelected: 0 };
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
const moveMcpToolCatalog = (delta) => {
|
|
306
|
+
setOverlay((current) => {
|
|
307
|
+
if (current?.kind !== 'mcp' || !current.detail)
|
|
308
|
+
return current;
|
|
309
|
+
const tools = current.probe?.status === 'pass' ? (current.probe.tools ?? []) : [];
|
|
310
|
+
if (!tools.length)
|
|
311
|
+
return current;
|
|
312
|
+
const last = tools.length - 1;
|
|
313
|
+
const selected = Math.max(0, Math.min(last, (current.toolSelected ?? 0) + delta));
|
|
314
|
+
return { ...current, toolSelected: selected };
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
const testMcpServerFromOverlay = (current) => {
|
|
318
|
+
if (current.kind !== 'mcp')
|
|
319
|
+
return;
|
|
320
|
+
const server = current.servers[current.selected];
|
|
321
|
+
if (!server)
|
|
322
|
+
return;
|
|
323
|
+
setOverlay({ ...current, detail: true, probe: { serverName: server.name, status: 'running' }, toolSelected: 0 });
|
|
324
|
+
void probeMcpServer(server.config, 8_000)
|
|
325
|
+
.then((result) => {
|
|
326
|
+
setOverlay((latest) => {
|
|
327
|
+
if (latest?.kind !== 'mcp' || !latest.detail || latest.probe?.serverName !== server.name)
|
|
328
|
+
return latest;
|
|
329
|
+
return {
|
|
330
|
+
...latest,
|
|
331
|
+
detail: true,
|
|
332
|
+
probe: result.ok
|
|
333
|
+
? { serverName: server.name, status: 'pass', tools: result.tools, transport: result.transport }
|
|
334
|
+
: {
|
|
335
|
+
error: result.error ?? 'unknown error',
|
|
336
|
+
serverName: server.name,
|
|
337
|
+
status: 'fail',
|
|
338
|
+
transport: result.transport,
|
|
339
|
+
},
|
|
340
|
+
toolSelected: 0,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
})
|
|
344
|
+
.catch((e) => {
|
|
345
|
+
setOverlay((latest) => {
|
|
346
|
+
if (latest?.kind !== 'mcp' || !latest.detail || latest.probe?.serverName !== server.name)
|
|
347
|
+
return latest;
|
|
348
|
+
return {
|
|
349
|
+
...latest,
|
|
350
|
+
detail: true,
|
|
351
|
+
probe: { error: e.message, serverName: server.name, status: 'fail' },
|
|
352
|
+
toolSelected: 0,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
const moveModelPicker = (delta) => {
|
|
358
|
+
setOverlay((current) => {
|
|
359
|
+
if (current?.kind !== 'model')
|
|
360
|
+
return current;
|
|
361
|
+
const list = current.phase === 'provider' ? current.providers : current.options;
|
|
362
|
+
const last = Math.max(0, list.length - 1);
|
|
363
|
+
return { ...current, selected: Math.max(0, Math.min(last, current.selected + delta)) };
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
const selectModelFromOverlay = (current) => {
|
|
367
|
+
if (current.kind !== 'model')
|
|
368
|
+
return;
|
|
369
|
+
if (current.phase === 'provider') {
|
|
370
|
+
const provider = current.providers[current.selected];
|
|
371
|
+
if (!provider)
|
|
372
|
+
return;
|
|
373
|
+
const options = filterModelPickerOptions(modelPickerOptions(model), provider.id);
|
|
374
|
+
setOverlay({
|
|
375
|
+
kind: 'model',
|
|
376
|
+
phase: 'model',
|
|
377
|
+
providerFilter: provider.id,
|
|
378
|
+
providers: current.providers,
|
|
379
|
+
options,
|
|
380
|
+
selected: initialModelPickerIndex(options),
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const selectedSpec = current.options[current.selected]?.spec ?? '';
|
|
385
|
+
setOverlay(null);
|
|
386
|
+
if (!selectedSpec)
|
|
387
|
+
return;
|
|
388
|
+
const result = parseCommand(`/model ${selectedSpec}`, { model, costSummary: lastCost.current });
|
|
389
|
+
if (result.modelChange)
|
|
390
|
+
setModel(result.modelChange);
|
|
391
|
+
if (result.message)
|
|
392
|
+
addTurn('system', result.message);
|
|
393
|
+
};
|
|
394
|
+
const openHelpPager = (text = HELP_TEXT) => {
|
|
395
|
+
setOverlay({ kind: 'pager', lines: text.split('\n'), offset: 0, title: 'Sanook help' });
|
|
396
|
+
};
|
|
397
|
+
const movePager = (delta) => {
|
|
398
|
+
setOverlay((current) => {
|
|
399
|
+
if (current?.kind !== 'pager')
|
|
400
|
+
return current;
|
|
401
|
+
const max = Math.max(0, current.lines.length - pagerPageSize);
|
|
402
|
+
const step = delta === 'top' ? -current.lines.length : delta === 'bottom' ? current.lines.length : delta;
|
|
403
|
+
const next = Math.max(0, Math.min(current.offset + step, max));
|
|
404
|
+
return next === current.offset ? current : { ...current, offset: next };
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
const pagePagerForward = () => {
|
|
408
|
+
setOverlay((current) => {
|
|
409
|
+
if (current?.kind !== 'pager')
|
|
410
|
+
return current;
|
|
411
|
+
const max = Math.max(0, current.lines.length - pagerPageSize);
|
|
412
|
+
if (current.offset >= max)
|
|
413
|
+
return null;
|
|
414
|
+
return { ...current, offset: Math.min(current.offset + pagerPageSize, max) };
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
const openSkillsHub = async () => {
|
|
418
|
+
try {
|
|
419
|
+
const skills = (await loadSkills()).sort((a, b) => a.name.localeCompare(b.name));
|
|
420
|
+
setOverlay({ detail: false, kind: 'skills', selected: 0, skills });
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
addTurn('system', `skills: ${e.message}`);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const openToolsHub = () => {
|
|
427
|
+
setOverlay({ detail: false, kind: 'tools', selected: 0, tools: TOOL_CATALOG });
|
|
428
|
+
};
|
|
429
|
+
const openTasksHub = () => {
|
|
430
|
+
const tasks = listBackgroundTasks().sort((a, b) => b.startedMs - a.startedMs);
|
|
431
|
+
setOverlay({ detail: false, kind: 'tasks', selected: 0, tasks });
|
|
432
|
+
};
|
|
433
|
+
const moveTasksHub = (delta) => {
|
|
434
|
+
setOverlay((current) => {
|
|
435
|
+
if (current?.kind !== 'tasks' || current.detail)
|
|
436
|
+
return current;
|
|
437
|
+
const last = Math.max(0, current.tasks.length - 1);
|
|
438
|
+
return { ...current, selected: Math.max(0, Math.min(last, current.selected + delta)) };
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const copyLatestAssistant = async () => {
|
|
442
|
+
const latest = [...history].reverse().find((turn) => turn.role === 'assistant' && turn.text.trim());
|
|
443
|
+
if (!latest) {
|
|
444
|
+
addTurn('system', 'copy: ยังไม่มีคำตอบ assistant ให้คัดลอก');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const result = await copyTextToClipboard(latest.text, { writeOsc52: (sequence) => stdout?.write(sequence) });
|
|
449
|
+
addTurn('system', `copy: copied latest assistant (${latest.text.length} chars) via ${result.detail}`);
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
addTurn('system', `copy: ${e.message}`);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const moveToolsHub = (delta) => {
|
|
456
|
+
setOverlay((current) => {
|
|
457
|
+
if (current?.kind !== 'tools' || current.detail)
|
|
458
|
+
return current;
|
|
459
|
+
const last = Math.max(0, current.tools.length - 1);
|
|
460
|
+
return { ...current, selected: Math.max(0, Math.min(last, current.selected + delta)) };
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
const moveSkillsHub = (delta) => {
|
|
464
|
+
setOverlay((current) => {
|
|
465
|
+
if (current?.kind !== 'skills' || current.detail)
|
|
466
|
+
return current;
|
|
467
|
+
const last = Math.max(0, current.skills.length - 1);
|
|
468
|
+
return { ...current, selected: Math.max(0, Math.min(last, current.selected + delta)) };
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
const openSessionsHub = async () => {
|
|
472
|
+
try {
|
|
473
|
+
const sessions = await listSessions({ cwd: null, limit: 20 });
|
|
474
|
+
setOverlay({ currentCwd: cwd, kind: 'sessions', selected: 0, sessions });
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
addTurn('system', `sessions: ${e.message}`);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const moveSessionsHub = (delta) => {
|
|
481
|
+
setOverlay((current) => {
|
|
482
|
+
if (current?.kind !== 'sessions')
|
|
483
|
+
return current;
|
|
484
|
+
const last = Math.max(0, current.sessions.length - 1);
|
|
485
|
+
return { ...current, notice: undefined, pendingDeleteId: undefined, selected: Math.max(0, Math.min(last, current.selected + delta)) };
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
const inspectSessionFromOverlay = (current) => {
|
|
489
|
+
if (current.kind !== 'sessions' || !current.sessions[current.selected])
|
|
490
|
+
return;
|
|
491
|
+
setOverlay({ ...current, detail: true, notice: undefined, pendingDeleteId: undefined });
|
|
492
|
+
};
|
|
493
|
+
const resumeSessionFromOverlay = (current) => {
|
|
494
|
+
if (current.kind !== 'sessions')
|
|
495
|
+
return;
|
|
496
|
+
const session = current.sessions[current.selected];
|
|
497
|
+
setOverlay(null);
|
|
498
|
+
if (!session)
|
|
499
|
+
return;
|
|
500
|
+
restoreSession(session);
|
|
501
|
+
};
|
|
502
|
+
const restoreSession = (session) => {
|
|
503
|
+
msgsRef.current = session.messages;
|
|
504
|
+
checkpoints.current = [];
|
|
505
|
+
lastRun.current = null;
|
|
506
|
+
lastCost.current = '';
|
|
507
|
+
sessionId.current = session.id;
|
|
508
|
+
sessionCreated.current = session.created;
|
|
509
|
+
setModel(session.model);
|
|
510
|
+
resetLiveToolTrail();
|
|
511
|
+
resetLiveThinking();
|
|
512
|
+
const crossProject = session.cwd !== cwd;
|
|
513
|
+
const cwdNote = crossProject ? ` · cwd ${session.cwd.replace(homedir(), '~')}` : '';
|
|
514
|
+
replaceHistory([
|
|
515
|
+
{
|
|
516
|
+
id: idRef.current++,
|
|
517
|
+
role: 'system',
|
|
518
|
+
text: `↻ เปิด session ${session.id} (${session.messages.length} messages)${cwdNote}${crossProject ? ' · --continue-any' : ''}`,
|
|
519
|
+
},
|
|
520
|
+
]);
|
|
521
|
+
};
|
|
522
|
+
const startSessionRename = (current) => {
|
|
523
|
+
if (current.kind !== 'sessions')
|
|
524
|
+
return;
|
|
525
|
+
const session = current.sessions[current.selected];
|
|
526
|
+
if (!session)
|
|
527
|
+
return;
|
|
528
|
+
const draft = session.title || firstUserSummary(session) || '';
|
|
529
|
+
setOverlay({
|
|
530
|
+
...current,
|
|
531
|
+
detail: false,
|
|
532
|
+
notice: undefined,
|
|
533
|
+
pendingDeleteId: undefined,
|
|
534
|
+
renaming: draft,
|
|
535
|
+
});
|
|
536
|
+
};
|
|
537
|
+
const confirmSessionRename = async (current) => {
|
|
538
|
+
if (current.kind !== 'sessions' || current.renaming === undefined)
|
|
539
|
+
return;
|
|
540
|
+
const session = current.sessions[current.selected];
|
|
541
|
+
if (!session)
|
|
542
|
+
return;
|
|
543
|
+
const title = current.renaming.trim();
|
|
544
|
+
if (!title) {
|
|
545
|
+
setOverlay({ ...current, notice: 'rename: title cannot be empty' });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const updated = await renameSession(session.id, title);
|
|
550
|
+
if (!updated) {
|
|
551
|
+
setOverlay({ ...current, notice: `rename failed: ${session.id} not found` });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const sessions = current.sessions.map((item) => (item.id === session.id ? updated : item));
|
|
555
|
+
setOverlay({
|
|
556
|
+
...current,
|
|
557
|
+
notice: `renamed → ${title}`,
|
|
558
|
+
renaming: undefined,
|
|
559
|
+
sessions,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
setOverlay({ ...current, notice: `rename failed: ${e.message}` });
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
const deleteSessionFromOverlay = async (current) => {
|
|
567
|
+
if (current.kind !== 'sessions')
|
|
568
|
+
return;
|
|
569
|
+
const session = current.sessions[current.selected];
|
|
570
|
+
if (!session)
|
|
571
|
+
return;
|
|
572
|
+
if (current.pendingDeleteId !== session.id) {
|
|
573
|
+
setOverlay({ ...current, notice: `delete? press d again: ${session.id}`, pendingDeleteId: session.id });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const removed = await removeSession(session.id);
|
|
578
|
+
const sessions = current.sessions.filter((item) => item.id !== session.id);
|
|
579
|
+
const selected = Math.max(0, Math.min(current.selected, sessions.length - 1));
|
|
580
|
+
setOverlay({
|
|
581
|
+
detail: false,
|
|
582
|
+
kind: 'sessions',
|
|
583
|
+
notice: removed ? `deleted ${session.id}` : `already removed ${session.id}`,
|
|
584
|
+
selected,
|
|
585
|
+
sessions,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
setOverlay({ ...current, notice: `delete failed: ${e.message}`, pendingDeleteId: undefined });
|
|
590
|
+
}
|
|
591
|
+
};
|
|
79
592
|
useInput((input, key) => {
|
|
593
|
+
if (overlay) {
|
|
594
|
+
if (overlay.kind === 'model') {
|
|
595
|
+
if (input === 'q' || input === 'Q')
|
|
596
|
+
setOverlay(null);
|
|
597
|
+
else if (key.escape) {
|
|
598
|
+
if (overlay.phase === 'model') {
|
|
599
|
+
setOverlay({ ...overlay, phase: 'provider', providerFilter: undefined, selected: 0 });
|
|
600
|
+
}
|
|
601
|
+
else
|
|
602
|
+
setOverlay(null);
|
|
603
|
+
}
|
|
604
|
+
else if (key.return)
|
|
605
|
+
selectModelFromOverlay(overlay);
|
|
606
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
607
|
+
moveModelPicker(1);
|
|
608
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
609
|
+
moveModelPicker(-1);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (overlay.kind === 'mcp') {
|
|
613
|
+
if (input === 'q' || input === 'Q')
|
|
614
|
+
setOverlay(null);
|
|
615
|
+
else if (input === 't' || input === 'T')
|
|
616
|
+
testMcpServerFromOverlay(overlay);
|
|
617
|
+
else if (overlay.detail && (key.escape || key.return))
|
|
618
|
+
setOverlay({ ...overlay, detail: false, toolSelected: 0 });
|
|
619
|
+
else if (key.escape)
|
|
620
|
+
setOverlay(null);
|
|
621
|
+
else if (key.return && overlay.servers.length)
|
|
622
|
+
setOverlay({ ...overlay, detail: true, toolSelected: 0 });
|
|
623
|
+
else if (overlay.detail && (key.downArrow || input === 'j' || input === 'J'))
|
|
624
|
+
moveMcpToolCatalog(1);
|
|
625
|
+
else if (overlay.detail && (key.upArrow || input === 'k' || input === 'K'))
|
|
626
|
+
moveMcpToolCatalog(-1);
|
|
627
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
628
|
+
moveMcpHub(1);
|
|
629
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
630
|
+
moveMcpHub(-1);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (overlay.kind === 'pager') {
|
|
634
|
+
if (key.escape || input === 'q' || input === 'Q')
|
|
635
|
+
setOverlay(null);
|
|
636
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
637
|
+
movePager(-1);
|
|
638
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
639
|
+
movePager(1);
|
|
640
|
+
else if (key.pageUp || input === 'b' || input === 'B')
|
|
641
|
+
movePager(-pagerPageSize);
|
|
642
|
+
else if (input === 'g')
|
|
643
|
+
movePager('top');
|
|
644
|
+
else if (input === 'G')
|
|
645
|
+
movePager('bottom');
|
|
646
|
+
else if (key.return || key.pageDown || input === ' ')
|
|
647
|
+
pagePagerForward();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (overlay.kind === 'skills') {
|
|
651
|
+
if (input === 'q' || input === 'Q')
|
|
652
|
+
setOverlay(null);
|
|
653
|
+
else if (overlay.detail && (key.escape || key.return))
|
|
654
|
+
setOverlay({ ...overlay, detail: false });
|
|
655
|
+
else if (key.escape)
|
|
656
|
+
setOverlay(null);
|
|
657
|
+
else if (key.return && overlay.skills.length)
|
|
658
|
+
setOverlay({ ...overlay, detail: true });
|
|
659
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
660
|
+
moveSkillsHub(1);
|
|
661
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
662
|
+
moveSkillsHub(-1);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (overlay.kind === 'sessions') {
|
|
666
|
+
if (overlay.renaming !== undefined) {
|
|
667
|
+
if (key.escape)
|
|
668
|
+
setOverlay({ ...overlay, notice: undefined, renaming: undefined });
|
|
669
|
+
else if (key.return)
|
|
670
|
+
void confirmSessionRename(overlay);
|
|
671
|
+
else if (key.backspace || key.delete)
|
|
672
|
+
setOverlay({ ...overlay, renaming: overlay.renaming.slice(0, -1) });
|
|
673
|
+
else if (input && !key.ctrl && !key.meta)
|
|
674
|
+
setOverlay({ ...overlay, renaming: overlay.renaming + input });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (input === 'q' || input === 'Q')
|
|
678
|
+
setOverlay(null);
|
|
679
|
+
else if (overlay.detail && key.escape)
|
|
680
|
+
setOverlay({ ...overlay, detail: false, notice: undefined, pendingDeleteId: undefined });
|
|
681
|
+
else if (key.escape)
|
|
682
|
+
setOverlay(null);
|
|
683
|
+
else if (input === 'd' || input === 'D')
|
|
684
|
+
void deleteSessionFromOverlay(overlay);
|
|
685
|
+
else if (input === 'r' || input === 'R')
|
|
686
|
+
startSessionRename(overlay);
|
|
687
|
+
else if (input === 'i' || input === 'I')
|
|
688
|
+
inspectSessionFromOverlay(overlay);
|
|
689
|
+
else if (key.return)
|
|
690
|
+
resumeSessionFromOverlay(overlay);
|
|
691
|
+
else if (!overlay.detail && (key.downArrow || input === 'j' || input === 'J'))
|
|
692
|
+
moveSessionsHub(1);
|
|
693
|
+
else if (!overlay.detail && (key.upArrow || input === 'k' || input === 'K'))
|
|
694
|
+
moveSessionsHub(-1);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (overlay.kind === 'tools') {
|
|
698
|
+
if (input === 'q' || input === 'Q')
|
|
699
|
+
setOverlay(null);
|
|
700
|
+
else if (overlay.detail && (key.escape || key.return))
|
|
701
|
+
setOverlay({ ...overlay, detail: false });
|
|
702
|
+
else if (key.escape)
|
|
703
|
+
setOverlay(null);
|
|
704
|
+
else if (key.return && overlay.tools.length)
|
|
705
|
+
setOverlay({ ...overlay, detail: true });
|
|
706
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
707
|
+
moveToolsHub(1);
|
|
708
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
709
|
+
moveToolsHub(-1);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (overlay.kind === 'tasks') {
|
|
713
|
+
if (input === 'q' || input === 'Q')
|
|
714
|
+
setOverlay(null);
|
|
715
|
+
else if (overlay.detail && (key.escape || key.return))
|
|
716
|
+
setOverlay({ ...overlay, detail: false, tasks: listBackgroundTasks().sort((a, b) => b.startedMs - a.startedMs) });
|
|
717
|
+
else if (key.escape)
|
|
718
|
+
setOverlay(null);
|
|
719
|
+
else if (key.return && overlay.tasks.length)
|
|
720
|
+
setOverlay({ ...overlay, detail: true });
|
|
721
|
+
else if (key.downArrow || input === 'j' || input === 'J')
|
|
722
|
+
moveTasksHub(1);
|
|
723
|
+
else if (key.upArrow || input === 'k' || input === 'K')
|
|
724
|
+
moveTasksHub(-1);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (key.escape || key.return || input === 'q' || input === 'Q')
|
|
728
|
+
setOverlay(null);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
80
731
|
// มี approval ค้าง → จับ y/n ก่อน (แม้ agent กำลังรัน/busy)
|
|
81
732
|
if (approvalReq) {
|
|
82
733
|
if (input === 'y' || input === 'Y' || key.return) {
|
|
@@ -96,10 +747,23 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
96
747
|
clearQueue();
|
|
97
748
|
return;
|
|
98
749
|
}
|
|
750
|
+
if (key.ctrl && input === 't') {
|
|
751
|
+
noteToolTrailMode(changeToolTrailMode());
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (key.ctrl && input === 'x') {
|
|
755
|
+
removeActiveQueued();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (!editor.value && queueRef.current.length && (key.upArrow || key.downArrow)) {
|
|
759
|
+
moveQueueActive(key.upArrow ? -1 : 1);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
99
762
|
// พิมพ์ระหว่าง busy ได้ — Enter = ต่อคิว (รันอัตโนมัติหลัง turn นี้จบ)
|
|
100
763
|
const a = editor.handleKey(input, key);
|
|
101
764
|
if (a === 'submit') {
|
|
102
765
|
const v = editor.value.trim();
|
|
766
|
+
const expanded = editor.expandValue(v).trim();
|
|
103
767
|
editor.reset();
|
|
104
768
|
const slash = parseSlashInvocation(v);
|
|
105
769
|
if (slash?.name === 'stop') {
|
|
@@ -108,11 +772,44 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
108
772
|
clearQueue();
|
|
109
773
|
return;
|
|
110
774
|
}
|
|
111
|
-
if (
|
|
112
|
-
enqueue(
|
|
775
|
+
if (expanded)
|
|
776
|
+
enqueue(expanded);
|
|
777
|
+
}
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (completions.length) {
|
|
781
|
+
if (key.upArrow) {
|
|
782
|
+
setCompletionIndex((index) => clampCompletionIndex(index - 1, completions.length));
|
|
783
|
+
return;
|
|
113
784
|
}
|
|
785
|
+
if (key.downArrow) {
|
|
786
|
+
setCompletionIndex((index) => clampCompletionIndex(index + 1, completions.length));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (key.tab || key.return) {
|
|
790
|
+
if (applyCompletion())
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (key.ctrl && input === 't') {
|
|
795
|
+
noteToolTrailMode(changeToolTrailMode());
|
|
114
796
|
return;
|
|
115
797
|
}
|
|
798
|
+
const transcriptLimit = transcriptWindowSize(stdout?.rows);
|
|
799
|
+
const transcriptStep = transcriptScrollStep(transcriptLimit);
|
|
800
|
+
if (history.length > transcriptLimit) {
|
|
801
|
+
if (key.pageUp || (key.ctrl && input === 'u')) {
|
|
802
|
+
setTranscriptScroll((scroll) => {
|
|
803
|
+
const max = Math.max(0, history.length - transcriptLimit);
|
|
804
|
+
return Math.min(max, scroll + transcriptStep);
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (key.pageDown || (key.ctrl && input === 'd')) {
|
|
809
|
+
setTranscriptScroll((scroll) => Math.max(0, scroll - transcriptStep));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
116
813
|
const action = editor.handleKey(input, key);
|
|
117
814
|
if (action === 'submit')
|
|
118
815
|
void submit(editor.value);
|
|
@@ -141,7 +838,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
141
838
|
}
|
|
142
839
|
msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
|
|
143
840
|
lastRun.current = null;
|
|
144
|
-
|
|
841
|
+
filterHistory((t) => t.id < cp.turnId);
|
|
145
842
|
addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
|
|
146
843
|
}
|
|
147
844
|
async function retryLastTurn() {
|
|
@@ -153,7 +850,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
153
850
|
}
|
|
154
851
|
msgsRef.current = msgsRef.current.slice(0, previous.msgLen);
|
|
155
852
|
checkpoints.current = checkpoints.current.filter((cp) => cp.turnId < previous.turnId);
|
|
156
|
-
|
|
853
|
+
filterHistory((t) => t.id < previous.turnId);
|
|
157
854
|
const mark = { turnId: idRef.current, msgLen: previous.msgLen };
|
|
158
855
|
const preview = previous.userText.length > 120 ? `${previous.userText.slice(0, 117)}...` : previous.userText;
|
|
159
856
|
addTurn('user', '/retry');
|
|
@@ -168,6 +865,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
168
865
|
return;
|
|
169
866
|
}
|
|
170
867
|
const tuning = await agentTuning().catch(() => null);
|
|
868
|
+
if (tuning?.contextCompression)
|
|
869
|
+
setContextCompression(tuning.contextCompression);
|
|
171
870
|
if (tuning?.compaction === 'summarize') {
|
|
172
871
|
addTurn('system', '⏳ กำลังย่อ context ด้วย model ถูก…');
|
|
173
872
|
msgsRef.current = await summarizeCompact(msgsRef.current, targetTokens, makeSummarizer(model, tuning.summaryModel), 20).catch(() => autoCompact(msgsRef.current, targetTokens, 20));
|
|
@@ -179,13 +878,14 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
179
878
|
}
|
|
180
879
|
}
|
|
181
880
|
async function submit(raw) {
|
|
182
|
-
const
|
|
881
|
+
const displayText = raw.trim();
|
|
882
|
+
const text = editor.expandValue(displayText).trim();
|
|
183
883
|
editor.reset();
|
|
184
|
-
if (!
|
|
884
|
+
if (!displayText)
|
|
185
885
|
return;
|
|
186
|
-
appendHistory(
|
|
187
|
-
replHistory.current.push(
|
|
188
|
-
const slash = parseSlashInvocation(
|
|
886
|
+
appendHistory(displayText, replHistory.current[replHistory.current.length - 1]);
|
|
887
|
+
replHistory.current.push(displayText);
|
|
888
|
+
const slash = parseSlashInvocation(displayText);
|
|
189
889
|
if (slash) {
|
|
190
890
|
if (slash.name === 'rewind') {
|
|
191
891
|
await rewind();
|
|
@@ -196,31 +896,38 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
196
896
|
if (custom) {
|
|
197
897
|
const expanded = expandCustomCommand(custom, slash.args);
|
|
198
898
|
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
199
|
-
addTurn('user',
|
|
899
|
+
addTurn('user', displayText);
|
|
200
900
|
if (!expanded.trim()) {
|
|
201
901
|
addTurn('system', `custom command /${slash.name} ว่าง`);
|
|
202
902
|
return;
|
|
203
903
|
}
|
|
204
|
-
await runAssistantTurn(expanded, [], mark,
|
|
904
|
+
await runAssistantTurn(expanded, [], mark, displayText);
|
|
205
905
|
return;
|
|
206
906
|
}
|
|
207
907
|
}
|
|
208
908
|
}
|
|
209
|
-
const cmd = parseCommand(
|
|
909
|
+
const cmd = parseCommand(displayText, { model, costSummary: lastCost.current });
|
|
210
910
|
if (cmd.handled) {
|
|
211
|
-
addTurn('user',
|
|
911
|
+
addTurn('user', displayText);
|
|
212
912
|
if (cmd.action === 'quit')
|
|
213
913
|
return exit();
|
|
214
914
|
if (cmd.action === 'clear') {
|
|
215
915
|
msgsRef.current = [];
|
|
216
916
|
checkpoints.current = [];
|
|
217
917
|
lastRun.current = null;
|
|
218
|
-
|
|
918
|
+
setStreaming('');
|
|
919
|
+
resetLiveToolTrail();
|
|
920
|
+
replaceHistory([]);
|
|
921
|
+
return;
|
|
219
922
|
}
|
|
220
923
|
if (cmd.action === 'compact') {
|
|
221
924
|
void compactHistory(40_000, 'บีบ context');
|
|
222
925
|
return;
|
|
223
926
|
}
|
|
927
|
+
if (cmd.action === 'copyLast') {
|
|
928
|
+
void copyLatestAssistant();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
224
931
|
if (cmd.action === 'diff')
|
|
225
932
|
return void runGit(['diff', '--stat'], 'diff');
|
|
226
933
|
if (cmd.action === 'retry')
|
|
@@ -241,6 +948,46 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
241
948
|
void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
|
|
242
949
|
return;
|
|
243
950
|
}
|
|
951
|
+
if (cmd.action === 'help') {
|
|
952
|
+
openHelpPager(cmd.message);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (cmd.action === 'mcpHub') {
|
|
956
|
+
void openMcpHub();
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (cmd.action === 'hotkeys') {
|
|
960
|
+
setOverlay({ kind: 'hotkeys' });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (cmd.action === 'modelPicker') {
|
|
964
|
+
openModelPicker();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (cmd.action === 'skillsHub') {
|
|
968
|
+
void openSkillsHub();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (cmd.action === 'toolTrail') {
|
|
972
|
+
noteToolTrailMode(changeToolTrailMode(cmd.toolTrailMode));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (cmd.action === 'details') {
|
|
976
|
+
applyDetailsMode(cmd.detailSection, cmd.detailMode);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (cmd.action === 'toolsHub') {
|
|
980
|
+
openToolsHub();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (cmd.action === 'sessionsHub') {
|
|
984
|
+
void openSessionsHub();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (cmd.action === 'tasksHub') {
|
|
988
|
+
openTasksHub();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
244
991
|
if (cmd.modelChange)
|
|
245
992
|
setModel(cmd.modelChange);
|
|
246
993
|
if (cmd.message)
|
|
@@ -249,11 +996,11 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
249
996
|
}
|
|
250
997
|
// prompt ปกติ → expand @mentions (inline ไฟล์ text + เก็บ path รูป)
|
|
251
998
|
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
252
|
-
addTurn('user',
|
|
999
|
+
addTurn('user', displayText);
|
|
253
1000
|
const { text: expanded, images, errors } = await expandMentions(text);
|
|
254
1001
|
if (errors.length)
|
|
255
1002
|
addTurn('system', `@mention: ${errors.join(' · ')}`);
|
|
256
|
-
await runAssistantTurn(expanded, images, mark,
|
|
1003
|
+
await runAssistantTurn(expanded, images, mark, displayText);
|
|
257
1004
|
}
|
|
258
1005
|
async function runAssistantTurn(promptText, images, mark, userText = promptText) {
|
|
259
1006
|
lastRun.current = { ...mark, userText, promptText, images };
|
|
@@ -261,6 +1008,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
261
1008
|
// (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
|
|
262
1009
|
if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
|
|
263
1010
|
const t = await agentTuning().catch(() => null);
|
|
1011
|
+
if (t?.contextCompression)
|
|
1012
|
+
setContextCompression(t.contextCompression);
|
|
264
1013
|
if (t?.compaction === 'summarize') {
|
|
265
1014
|
addTurn('system', '⏳ context ยาว — ย่ออัตโนมัติก่อนรอบนี้…');
|
|
266
1015
|
msgsRef.current = await summarizeCompact(msgsRef.current, PRE_TURN_COMPACT_TOKENS, makeSummarizer(model, t.summaryModel), 20).catch(() => msgsRef.current);
|
|
@@ -271,9 +1020,14 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
271
1020
|
checkpoints.current.push({ ref, turnId: mark.turnId, msgLen: mark.msgLen });
|
|
272
1021
|
const ac = new AbortController(); // steering: ให้ Esc/Ctrl+C หยุด stream กลางทางได้
|
|
273
1022
|
abortRef.current = ac;
|
|
1023
|
+
resetLiveToolTrail();
|
|
1024
|
+
resetLiveThinking();
|
|
1025
|
+
setStreaming('');
|
|
274
1026
|
setBusy(true);
|
|
275
1027
|
let buf = '';
|
|
1028
|
+
let reasoningBuf = '';
|
|
276
1029
|
let lastFlush = 0;
|
|
1030
|
+
let lastThinkingFlush = 0;
|
|
277
1031
|
try {
|
|
278
1032
|
const { cost, messages, text } = await runAgent({
|
|
279
1033
|
model,
|
|
@@ -295,14 +1049,25 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
295
1049
|
}
|
|
296
1050
|
}
|
|
297
1051
|
else if (e.type === 'tool-call') {
|
|
298
|
-
|
|
299
|
-
|
|
1052
|
+
recordToolTrailEvent(e);
|
|
1053
|
+
}
|
|
1054
|
+
else if (e.type === 'tool-result' || e.type === 'error') {
|
|
1055
|
+
recordToolTrailEvent(e);
|
|
1056
|
+
}
|
|
1057
|
+
else if (e.type === 'reasoning') {
|
|
1058
|
+
reasoningBuf += e.text ?? '';
|
|
1059
|
+
thinkingRef.current = reasoningBuf;
|
|
1060
|
+
const now = Date.now();
|
|
1061
|
+
if (now - lastThinkingFlush > 120) {
|
|
1062
|
+
setThinking(reasoningBuf);
|
|
1063
|
+
lastThinkingFlush = now;
|
|
1064
|
+
}
|
|
300
1065
|
}
|
|
301
1066
|
},
|
|
302
1067
|
});
|
|
303
1068
|
msgsRef.current = messages;
|
|
304
1069
|
lastCost.current = cost.summary();
|
|
305
|
-
addTurn('assistant', buf.trim() || text.trim());
|
|
1070
|
+
addTurn('assistant', buf.trim() || text.trim(), { thinking: snapshotThinking(reasoningBuf), toolTrail: snapshotToolTrail() });
|
|
306
1071
|
// เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
|
|
307
1072
|
void saveSession({
|
|
308
1073
|
id: sessionId.current,
|
|
@@ -329,7 +1094,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
329
1094
|
if (ac.signal.aborted) {
|
|
330
1095
|
// หยุดเอง — เก็บ partial output ไว้ดู, ทิ้ง turn นี้ออกจาก LLM history (msgsRef ไม่อัปเดต)
|
|
331
1096
|
if (buf.trim())
|
|
332
|
-
addTurn('assistant', buf.trim());
|
|
1097
|
+
addTurn('assistant', buf.trim(), { thinking: snapshotThinking(reasoningBuf), toolTrail: snapshotToolTrail() });
|
|
333
1098
|
addTurn('system', '⊘ หยุด turn แล้ว (ไฟล์ที่ tool แก้ไปแล้วคืนด้วย /rewind ได้)');
|
|
334
1099
|
}
|
|
335
1100
|
else {
|
|
@@ -338,6 +1103,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
338
1103
|
}
|
|
339
1104
|
finally {
|
|
340
1105
|
setStreaming('');
|
|
1106
|
+
resetLiveThinking();
|
|
1107
|
+
resetLiveToolTrail();
|
|
341
1108
|
setBusy(false);
|
|
342
1109
|
abortRef.current = null;
|
|
343
1110
|
}
|
|
@@ -347,7 +1114,28 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
347
1114
|
void submit(next);
|
|
348
1115
|
}
|
|
349
1116
|
const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
|
|
350
|
-
|
|
1117
|
+
const contextTokens = estimateTokens(msgsRef.current);
|
|
1118
|
+
const activeQueueIndex = clampQueueActiveIndex(queueActiveIndex, queued.length);
|
|
1119
|
+
const queueWindow = getQueueWindow(queued.length, activeQueueIndex);
|
|
1120
|
+
const toolTrailView = toolTrailLines(toolTrail, columns, toolTrailMode);
|
|
1121
|
+
const thinkingView = thinkingPanelLines(thinking, columns, thinkingMode);
|
|
1122
|
+
const transcriptLimit = transcriptWindowSize(stdout?.rows);
|
|
1123
|
+
const transcriptView = getTranscriptWindow(history.length, transcriptLimit, transcriptScroll);
|
|
1124
|
+
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, toolTrailView.length ? (_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" })] })) : (_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 })] })), _jsx(Text, { dimColor: true, children: footerStatus({
|
|
1126
|
+
branch: gitBranch,
|
|
1127
|
+
backgroundTaskCount: bgTaskCount,
|
|
1128
|
+
busy,
|
|
1129
|
+
columns,
|
|
1130
|
+
contextCompression,
|
|
1131
|
+
contextTokens,
|
|
1132
|
+
costHint,
|
|
1133
|
+
cwd,
|
|
1134
|
+
elapsedSeconds: busyElapsedSeconds,
|
|
1135
|
+
model,
|
|
1136
|
+
mode: permissionMode === 'ask' ? 'ask' : 'auto',
|
|
1137
|
+
queuedCount: queued.length,
|
|
1138
|
+
}) })] }));
|
|
351
1139
|
}
|
|
352
1140
|
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
353
1141
|
function InputView({ value, cursor, busy }) {
|
|
@@ -360,10 +1148,28 @@ function InputView({ value, cursor, busy }) {
|
|
|
360
1148
|
const after = value.slice(cursor + 1);
|
|
361
1149
|
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] }));
|
|
362
1150
|
}
|
|
363
|
-
function
|
|
1151
|
+
function ToolTrailView({ columns, items, mode }) {
|
|
1152
|
+
const lines = toolTrailLines(items, columns, mode);
|
|
1153
|
+
if (!lines.length)
|
|
1154
|
+
return null;
|
|
1155
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: lines.map((line, index) => {
|
|
1156
|
+
const isRunning = line.startsWith('>');
|
|
1157
|
+
const isError = line.startsWith('!');
|
|
1158
|
+
const isDone = line.startsWith('+');
|
|
1159
|
+
const isMeta = line.startsWith('view:') || line.startsWith('tools:');
|
|
1160
|
+
return (_jsx(Text, { color: index === 0 ? 'cyan' : isError ? 'red' : isRunning ? 'yellow' : undefined, dimColor: isDone || isMeta, wrap: "truncate-end", children: line }, `${index}-${line}`));
|
|
1161
|
+
}) }));
|
|
1162
|
+
}
|
|
1163
|
+
function ThinkingView({ columns, mode, text }) {
|
|
1164
|
+
const lines = thinkingPanelLines(text, columns, mode);
|
|
1165
|
+
if (!lines.length)
|
|
1166
|
+
return null;
|
|
1167
|
+
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
|
+
}
|
|
1169
|
+
function TurnView({ columns, thinkingMode, toolTrailMode, turn, }) {
|
|
364
1170
|
if (turn.role === 'system')
|
|
365
1171
|
return _jsx(Text, { dimColor: true, children: turn.text });
|
|
366
1172
|
if (turn.role === 'user')
|
|
367
1173
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { color: "cyan", children: turn.text })] }));
|
|
368
|
-
return (
|
|
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] }));
|
|
369
1175
|
}
|