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