sanook-cli 0.5.1 → 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/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -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-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- 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/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- 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 +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- 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 +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- 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 +172 -46
- 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 +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- 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 +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -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 +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -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/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/ui/app.js
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef
|
|
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
|
-
import { agentTuning } from '../config.js';
|
|
14
|
+
import { agentTuning, patchGlobalConfig } from '../config.js';
|
|
13
15
|
import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
|
|
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';
|
|
14
23
|
import { useEditor } from './useEditor.js';
|
|
24
|
+
import { useBusyElapsedSeconds } from './useBusyElapsed.js';
|
|
25
|
+
import { useGitBranch } from './useGitBranch.js';
|
|
15
26
|
import { loadHistory, appendHistory } from './history.js';
|
|
16
27
|
import { expandMentions } from './mentions.js';
|
|
17
28
|
import { BRAND } from '../brand.js';
|
|
29
|
+
import { backgroundTaskRunningCount, listBackgroundTasks } from '../tools/task.js';
|
|
18
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';
|
|
19
39
|
const execFileP = promisify(execFile);
|
|
20
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;
|
|
21
43
|
export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory, initialNote }) {
|
|
22
44
|
const { exit } = useApp();
|
|
45
|
+
const { stdout } = useStdout();
|
|
23
46
|
const [history, setHistory] = useState(() => {
|
|
24
47
|
const seed = [];
|
|
25
48
|
if (initialNote)
|
|
@@ -29,36 +52,213 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
29
52
|
return seed;
|
|
30
53
|
});
|
|
31
54
|
const [streaming, setStreaming] = useState('');
|
|
55
|
+
const [thinking, setThinking] = useState('');
|
|
56
|
+
const [toolTrail, setToolTrail] = useState([]);
|
|
32
57
|
const [busy, setBusy] = useState(false);
|
|
33
58
|
const [model, setModel] = useState(initialModel);
|
|
34
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);
|
|
35
68
|
const idRef = useRef(0);
|
|
36
69
|
const lastCost = useRef('');
|
|
70
|
+
const nextToolTrailId = useRef(0);
|
|
71
|
+
const toolTrailRef = useRef([]);
|
|
72
|
+
const toolTrailModeRef = useRef('expanded');
|
|
73
|
+
const thinkingRef = useRef('');
|
|
37
74
|
const msgsRef = useRef(initialHistory ?? []); // conversation จริงสำหรับ LLM (สะสมข้ามรอบ)
|
|
38
75
|
const sessionId = useRef(newSessionId());
|
|
39
76
|
const sessionCreated = useRef(new Date().toISOString());
|
|
40
77
|
const approvalResolve = useRef(null);
|
|
41
78
|
const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
|
|
42
79
|
const checkpoints = useRef([]);
|
|
80
|
+
const lastRun = useRef(null);
|
|
43
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
|
+
});
|
|
44
88
|
// real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
|
|
45
89
|
const abortRef = useRef(null);
|
|
46
90
|
const queueRef = useRef([]);
|
|
47
91
|
const [queued, setQueued] = useState([]);
|
|
92
|
+
const [bgTaskCount, setBgTaskCount] = useState(0);
|
|
48
93
|
const enqueue = (msg) => {
|
|
49
94
|
queueRef.current.push(msg);
|
|
50
95
|
setQueued([...queueRef.current]);
|
|
96
|
+
setQueueActiveIndex((index) => clampQueueActiveIndex(index, queueRef.current.length));
|
|
51
97
|
};
|
|
52
98
|
const dequeue = () => {
|
|
53
99
|
const m = queueRef.current.shift();
|
|
54
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
|
+
});
|
|
55
108
|
return m;
|
|
56
109
|
};
|
|
57
110
|
const clearQueue = () => {
|
|
58
111
|
queueRef.current = [];
|
|
59
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;
|
|
60
261
|
};
|
|
61
|
-
const addTurn = (role, text) => setHistory((h) => [...h, { id: idRef.current++, role, text }]);
|
|
62
262
|
// /diff /undo — git-backed (execFile ไม่ผ่าน shell)
|
|
63
263
|
async function runGit(args, label) {
|
|
64
264
|
try {
|
|
@@ -74,7 +274,460 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
74
274
|
approvalResolve.current = resolve;
|
|
75
275
|
setApprovalReq({ tool, summary });
|
|
76
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
|
+
};
|
|
77
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
|
+
}
|
|
78
731
|
// มี approval ค้าง → จับ y/n ก่อน (แม้ agent กำลังรัน/busy)
|
|
79
732
|
if (approvalReq) {
|
|
80
733
|
if (input === 'y' || input === 'Y' || key.return) {
|
|
@@ -94,16 +747,69 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
94
747
|
clearQueue();
|
|
95
748
|
return;
|
|
96
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
|
+
}
|
|
97
762
|
// พิมพ์ระหว่าง busy ได้ — Enter = ต่อคิว (รันอัตโนมัติหลัง turn นี้จบ)
|
|
98
763
|
const a = editor.handleKey(input, key);
|
|
99
764
|
if (a === 'submit') {
|
|
100
765
|
const v = editor.value.trim();
|
|
766
|
+
const expanded = editor.expandValue(v).trim();
|
|
101
767
|
editor.reset();
|
|
102
|
-
|
|
103
|
-
|
|
768
|
+
const slash = parseSlashInvocation(v);
|
|
769
|
+
if (slash?.name === 'stop') {
|
|
770
|
+
addTurn('user', v);
|
|
771
|
+
abortRef.current?.abort();
|
|
772
|
+
clearQueue();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
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;
|
|
784
|
+
}
|
|
785
|
+
if (key.downArrow) {
|
|
786
|
+
setCompletionIndex((index) => clampCompletionIndex(index + 1, completions.length));
|
|
787
|
+
return;
|
|
104
788
|
}
|
|
789
|
+
if (key.tab || key.return) {
|
|
790
|
+
if (applyCompletion())
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (key.ctrl && input === 't') {
|
|
795
|
+
noteToolTrailMode(changeToolTrailMode());
|
|
105
796
|
return;
|
|
106
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
|
+
}
|
|
107
813
|
const action = editor.handleKey(input, key);
|
|
108
814
|
if (action === 'submit')
|
|
109
815
|
void submit(editor.value);
|
|
@@ -131,9 +837,26 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
131
837
|
: ` · ไฟล์: ${r.reason}`;
|
|
132
838
|
}
|
|
133
839
|
msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
|
|
134
|
-
|
|
840
|
+
lastRun.current = null;
|
|
841
|
+
filterHistory((t) => t.id < cp.turnId);
|
|
135
842
|
addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
|
|
136
843
|
}
|
|
844
|
+
async function retryLastTurn() {
|
|
845
|
+
const previous = lastRun.current;
|
|
846
|
+
if (!previous) {
|
|
847
|
+
addTurn('user', '/retry');
|
|
848
|
+
addTurn('system', 'ยังไม่มี turn ให้ retry');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
msgsRef.current = msgsRef.current.slice(0, previous.msgLen);
|
|
852
|
+
checkpoints.current = checkpoints.current.filter((cp) => cp.turnId < previous.turnId);
|
|
853
|
+
filterHistory((t) => t.id < previous.turnId);
|
|
854
|
+
const mark = { turnId: idRef.current, msgLen: previous.msgLen };
|
|
855
|
+
const preview = previous.userText.length > 120 ? `${previous.userText.slice(0, 117)}...` : previous.userText;
|
|
856
|
+
addTurn('user', '/retry');
|
|
857
|
+
addTurn('system', `retry: ${preview}`);
|
|
858
|
+
await runAssistantTurn(previous.promptText, previous.images, mark, previous.userText);
|
|
859
|
+
}
|
|
137
860
|
/** บีบ context: 'summarize' (ใช้ model ถูกย่อ) ถ้าตั้งไว้ ไม่งั้น 'truncate' (zero-LLM) */
|
|
138
861
|
async function compactHistory(targetTokens, label) {
|
|
139
862
|
const before = estimateTokens(msgsRef.current);
|
|
@@ -142,6 +865,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
142
865
|
return;
|
|
143
866
|
}
|
|
144
867
|
const tuning = await agentTuning().catch(() => null);
|
|
868
|
+
if (tuning?.contextCompression)
|
|
869
|
+
setContextCompression(tuning.contextCompression);
|
|
145
870
|
if (tuning?.compaction === 'summarize') {
|
|
146
871
|
addTurn('system', '⏳ กำลังย่อ context ด้วย model ถูก…');
|
|
147
872
|
msgsRef.current = await summarizeCompact(msgsRef.current, targetTokens, makeSummarizer(model, tuning.summaryModel), 20).catch(() => autoCompact(msgsRef.current, targetTokens, 20));
|
|
@@ -153,13 +878,14 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
153
878
|
}
|
|
154
879
|
}
|
|
155
880
|
async function submit(raw) {
|
|
156
|
-
const
|
|
881
|
+
const displayText = raw.trim();
|
|
882
|
+
const text = editor.expandValue(displayText).trim();
|
|
157
883
|
editor.reset();
|
|
158
|
-
if (!
|
|
884
|
+
if (!displayText)
|
|
159
885
|
return;
|
|
160
|
-
appendHistory(
|
|
161
|
-
replHistory.current.push(
|
|
162
|
-
const slash = parseSlashInvocation(
|
|
886
|
+
appendHistory(displayText, replHistory.current[replHistory.current.length - 1]);
|
|
887
|
+
replHistory.current.push(displayText);
|
|
888
|
+
const slash = parseSlashInvocation(displayText);
|
|
163
889
|
if (slash) {
|
|
164
890
|
if (slash.name === 'rewind') {
|
|
165
891
|
await rewind();
|
|
@@ -170,36 +896,98 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
170
896
|
if (custom) {
|
|
171
897
|
const expanded = expandCustomCommand(custom, slash.args);
|
|
172
898
|
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
173
|
-
addTurn('user',
|
|
899
|
+
addTurn('user', displayText);
|
|
174
900
|
if (!expanded.trim()) {
|
|
175
901
|
addTurn('system', `custom command /${slash.name} ว่าง`);
|
|
176
902
|
return;
|
|
177
903
|
}
|
|
178
|
-
await runAssistantTurn(expanded, [], mark);
|
|
904
|
+
await runAssistantTurn(expanded, [], mark, displayText);
|
|
179
905
|
return;
|
|
180
906
|
}
|
|
181
907
|
}
|
|
182
908
|
}
|
|
183
|
-
const cmd = parseCommand(
|
|
909
|
+
const cmd = parseCommand(displayText, { model, costSummary: lastCost.current });
|
|
184
910
|
if (cmd.handled) {
|
|
185
|
-
addTurn('user',
|
|
911
|
+
addTurn('user', displayText);
|
|
186
912
|
if (cmd.action === 'quit')
|
|
187
913
|
return exit();
|
|
188
914
|
if (cmd.action === 'clear') {
|
|
189
915
|
msgsRef.current = [];
|
|
190
916
|
checkpoints.current = [];
|
|
191
|
-
|
|
917
|
+
lastRun.current = null;
|
|
918
|
+
setStreaming('');
|
|
919
|
+
resetLiveToolTrail();
|
|
920
|
+
replaceHistory([]);
|
|
921
|
+
return;
|
|
192
922
|
}
|
|
193
923
|
if (cmd.action === 'compact') {
|
|
194
924
|
void compactHistory(40_000, 'บีบ context');
|
|
195
925
|
return;
|
|
196
926
|
}
|
|
927
|
+
if (cmd.action === 'copyLast') {
|
|
928
|
+
void copyLatestAssistant();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
197
931
|
if (cmd.action === 'diff')
|
|
198
932
|
return void runGit(['diff', '--stat'], 'diff');
|
|
933
|
+
if (cmd.action === 'retry')
|
|
934
|
+
return void retryLastTurn();
|
|
935
|
+
if (cmd.action === 'personality') {
|
|
936
|
+
void patchGlobalConfig({ personality: cmd.personalityChange || undefined })
|
|
937
|
+
.then(() => addTurn('system', cmd.message ?? 'ตั้ง personality แล้ว'))
|
|
938
|
+
.catch((e) => addTurn('system', `personality: ${e.message}`));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (cmd.action === 'insights') {
|
|
942
|
+
void renderInsights({ days: cmd.insightsDays, cwd: cmd.insightsAll ? null : undefined })
|
|
943
|
+
.then((msg) => addTurn('system', msg))
|
|
944
|
+
.catch((e) => addTurn('system', `insights: ${e.message}`));
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
199
947
|
if (cmd.action === 'undo') {
|
|
200
948
|
void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
|
|
201
949
|
return;
|
|
202
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
|
+
}
|
|
203
991
|
if (cmd.modelChange)
|
|
204
992
|
setModel(cmd.modelChange);
|
|
205
993
|
if (cmd.message)
|
|
@@ -208,17 +996,20 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
208
996
|
}
|
|
209
997
|
// prompt ปกติ → expand @mentions (inline ไฟล์ text + เก็บ path รูป)
|
|
210
998
|
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
211
|
-
addTurn('user',
|
|
999
|
+
addTurn('user', displayText);
|
|
212
1000
|
const { text: expanded, images, errors } = await expandMentions(text);
|
|
213
1001
|
if (errors.length)
|
|
214
1002
|
addTurn('system', `@mention: ${errors.join(' · ')}`);
|
|
215
|
-
await runAssistantTurn(expanded, images, mark);
|
|
1003
|
+
await runAssistantTurn(expanded, images, mark, displayText);
|
|
216
1004
|
}
|
|
217
|
-
async function runAssistantTurn(promptText, images, mark) {
|
|
1005
|
+
async function runAssistantTurn(promptText, images, mark, userText = promptText) {
|
|
1006
|
+
lastRun.current = { ...mark, userText, promptText, images };
|
|
218
1007
|
// proactive summarize-compaction สำหรับ session ยาวมาก (เฉพาะ mode summarize) — เริ่ม turn ให้ context lean
|
|
219
1008
|
// (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
|
|
220
1009
|
if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
|
|
221
1010
|
const t = await agentTuning().catch(() => null);
|
|
1011
|
+
if (t?.contextCompression)
|
|
1012
|
+
setContextCompression(t.contextCompression);
|
|
222
1013
|
if (t?.compaction === 'summarize') {
|
|
223
1014
|
addTurn('system', '⏳ context ยาว — ย่ออัตโนมัติก่อนรอบนี้…');
|
|
224
1015
|
msgsRef.current = await summarizeCompact(msgsRef.current, PRE_TURN_COMPACT_TOKENS, makeSummarizer(model, t.summaryModel), 20).catch(() => msgsRef.current);
|
|
@@ -229,9 +1020,14 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
229
1020
|
checkpoints.current.push({ ref, turnId: mark.turnId, msgLen: mark.msgLen });
|
|
230
1021
|
const ac = new AbortController(); // steering: ให้ Esc/Ctrl+C หยุด stream กลางทางได้
|
|
231
1022
|
abortRef.current = ac;
|
|
1023
|
+
resetLiveToolTrail();
|
|
1024
|
+
resetLiveThinking();
|
|
1025
|
+
setStreaming('');
|
|
232
1026
|
setBusy(true);
|
|
233
1027
|
let buf = '';
|
|
1028
|
+
let reasoningBuf = '';
|
|
234
1029
|
let lastFlush = 0;
|
|
1030
|
+
let lastThinkingFlush = 0;
|
|
235
1031
|
try {
|
|
236
1032
|
const { cost, messages, text } = await runAgent({
|
|
237
1033
|
model,
|
|
@@ -253,14 +1049,25 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
253
1049
|
}
|
|
254
1050
|
}
|
|
255
1051
|
else if (e.type === 'tool-call') {
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}
|
|
258
1065
|
}
|
|
259
1066
|
},
|
|
260
1067
|
});
|
|
261
1068
|
msgsRef.current = messages;
|
|
262
1069
|
lastCost.current = cost.summary();
|
|
263
|
-
addTurn('assistant', buf.trim() || text.trim());
|
|
1070
|
+
addTurn('assistant', buf.trim() || text.trim(), { thinking: snapshotThinking(reasoningBuf), toolTrail: snapshotToolTrail() });
|
|
264
1071
|
// เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
|
|
265
1072
|
void saveSession({
|
|
266
1073
|
id: sessionId.current,
|
|
@@ -287,7 +1094,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
287
1094
|
if (ac.signal.aborted) {
|
|
288
1095
|
// หยุดเอง — เก็บ partial output ไว้ดู, ทิ้ง turn นี้ออกจาก LLM history (msgsRef ไม่อัปเดต)
|
|
289
1096
|
if (buf.trim())
|
|
290
|
-
addTurn('assistant', buf.trim());
|
|
1097
|
+
addTurn('assistant', buf.trim(), { thinking: snapshotThinking(reasoningBuf), toolTrail: snapshotToolTrail() });
|
|
291
1098
|
addTurn('system', '⊘ หยุด turn แล้ว (ไฟล์ที่ tool แก้ไปแล้วคืนด้วย /rewind ได้)');
|
|
292
1099
|
}
|
|
293
1100
|
else {
|
|
@@ -296,6 +1103,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
296
1103
|
}
|
|
297
1104
|
finally {
|
|
298
1105
|
setStreaming('');
|
|
1106
|
+
resetLiveThinking();
|
|
1107
|
+
resetLiveToolTrail();
|
|
299
1108
|
setBusy(false);
|
|
300
1109
|
abortRef.current = null;
|
|
301
1110
|
}
|
|
@@ -304,10 +1113,29 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
304
1113
|
if (next)
|
|
305
1114
|
void submit(next);
|
|
306
1115
|
}
|
|
307
|
-
// banner ผูกกับ live `model` (ไม่ใช่ initialModel) → /model เปลี่ยนแล้ว banner อัปเดตตาม ไม่ค้าง model เก่า
|
|
308
|
-
const banner = useMemo(() => _jsx(Banner, { model: model }), [model]);
|
|
309
1116
|
const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
|
|
310
|
-
|
|
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
|
+
}) })] }));
|
|
311
1139
|
}
|
|
312
1140
|
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
313
1141
|
function InputView({ value, cursor, busy }) {
|
|
@@ -320,10 +1148,28 @@ function InputView({ value, cursor, busy }) {
|
|
|
320
1148
|
const after = value.slice(cursor + 1);
|
|
321
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] }));
|
|
322
1150
|
}
|
|
323
|
-
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, }) {
|
|
324
1170
|
if (turn.role === 'system')
|
|
325
1171
|
return _jsx(Text, { dimColor: true, children: turn.text });
|
|
326
1172
|
if (turn.role === 'user')
|
|
327
1173
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { color: "cyan", children: turn.text })] }));
|
|
328
|
-
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] }));
|
|
329
1175
|
}
|