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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { BRAND } from '../brand.js';
|
|
6
|
+
import { TOOL_CATALOG } from '../tool-catalog.js';
|
|
7
|
+
const MIN_PANEL_COLUMNS = 48;
|
|
8
|
+
const COMPACT_PANEL_COLUMNS = 72;
|
|
9
|
+
const MAX_PANEL_COLUMNS = 100;
|
|
10
|
+
const PREVIEW_LIMIT = 4;
|
|
11
|
+
const clip = (text, width) => {
|
|
12
|
+
if (width <= 0)
|
|
13
|
+
return '';
|
|
14
|
+
return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text;
|
|
15
|
+
};
|
|
16
|
+
function displayDir(cwd) {
|
|
17
|
+
return (cwd ?? process.cwd()).replace(homedir(), '~');
|
|
18
|
+
}
|
|
19
|
+
function sectionCount(value, fallback = 0) {
|
|
20
|
+
if (value === 'checking')
|
|
21
|
+
return 'checking';
|
|
22
|
+
if (!value)
|
|
23
|
+
return `${fallback}`;
|
|
24
|
+
return value.count ? `${value.count}` : 'none';
|
|
25
|
+
}
|
|
26
|
+
function previewNames(value, fallbackNames = []) {
|
|
27
|
+
if (value === 'checking')
|
|
28
|
+
return ['checking…'];
|
|
29
|
+
if (!value?.names.length)
|
|
30
|
+
return fallbackNames.length ? fallbackNames : ['none configured'];
|
|
31
|
+
return value.names.slice(0, PREVIEW_LIMIT);
|
|
32
|
+
}
|
|
33
|
+
export function sessionPanelLines({ columns, cwd, expanded, mcp, model, mode, skills, tools, }) {
|
|
34
|
+
const width = Math.max(20, Math.floor(columns || 80));
|
|
35
|
+
if (width < MIN_PANEL_COLUMNS)
|
|
36
|
+
return [];
|
|
37
|
+
const dir = displayDir(cwd);
|
|
38
|
+
const toolPreview = tools ?? { count: TOOL_CATALOG.length, names: TOOL_CATALOG.map((tool) => tool.name) };
|
|
39
|
+
const expandedSections = expanded ?? new Set();
|
|
40
|
+
if (width < COMPACT_PANEL_COLUMNS) {
|
|
41
|
+
return [
|
|
42
|
+
'Routes: Code · Brain · Connect · Ship',
|
|
43
|
+
'Code @file · /tools · git diff/undo',
|
|
44
|
+
'Brain context · remember · /skills',
|
|
45
|
+
'Connect /mcp search/install · doctor',
|
|
46
|
+
'Ship /copy · cost guard · final proof',
|
|
47
|
+
`System ${model} · ${mode}-mode`,
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
const lines = [
|
|
51
|
+
`${BRAND.bannerWide} service routes`,
|
|
52
|
+
'Code @file mentions · read/edit/run tools · git diff/undo',
|
|
53
|
+
'Brain second-brain context · remember/recall · reusable workflows /skills',
|
|
54
|
+
'Connect MCP registry search/install · doctor · gateway serve',
|
|
55
|
+
'Ship /copy handoff · cost guard · final proof · /undo safety',
|
|
56
|
+
'System ask approvals · queued follow-ups · /hotkeys',
|
|
57
|
+
`Runtime ${model} · ${mode}-mode · BYOK · ${dir}`,
|
|
58
|
+
'Launchpad 1 tools · 2 skills · 3 MCP',
|
|
59
|
+
];
|
|
60
|
+
const sectionLine = (key, label, countLabel, preview) => {
|
|
61
|
+
const open = expandedSections.has(key);
|
|
62
|
+
lines.push(`${open ? '▾' : '▸'} ${label} (${countLabel})`);
|
|
63
|
+
if (open) {
|
|
64
|
+
for (const name of preview)
|
|
65
|
+
lines.push(` ${name}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
sectionLine('tools', 'Tools', sectionCount(toolPreview, TOOL_CATALOG.length), previewNames(toolPreview, TOOL_CATALOG.map((tool) => tool.name)));
|
|
69
|
+
sectionLine('skills', 'Skills', sectionCount(skills), previewNames(skills, ['load with /skills']));
|
|
70
|
+
sectionLine('mcp', 'MCP', sectionCount(mcp), previewNames(mcp, ['sanook mcp search/install']));
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
/** Hermes-style startup service panel, rebranded around Sanook's local-first workflow. */
|
|
74
|
+
export function SessionPanel(props) {
|
|
75
|
+
const [expanded, setExpanded] = useState(() => new Set());
|
|
76
|
+
useInput((input) => {
|
|
77
|
+
if (input === '1') {
|
|
78
|
+
setExpanded((current) => {
|
|
79
|
+
const next = new Set(current);
|
|
80
|
+
if (next.has('tools'))
|
|
81
|
+
next.delete('tools');
|
|
82
|
+
else
|
|
83
|
+
next.add('tools');
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else if (input === '2') {
|
|
88
|
+
setExpanded((current) => {
|
|
89
|
+
const next = new Set(current);
|
|
90
|
+
if (next.has('skills'))
|
|
91
|
+
next.delete('skills');
|
|
92
|
+
else
|
|
93
|
+
next.add('skills');
|
|
94
|
+
return next;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else if (input === '3') {
|
|
98
|
+
setExpanded((current) => {
|
|
99
|
+
const next = new Set(current);
|
|
100
|
+
if (next.has('mcp'))
|
|
101
|
+
next.delete('mcp');
|
|
102
|
+
else
|
|
103
|
+
next.add('mcp');
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const width = Math.max(20, Math.floor(props.columns || 80));
|
|
109
|
+
const lines = sessionPanelLines({ ...props, expanded });
|
|
110
|
+
if (!lines.length)
|
|
111
|
+
return null;
|
|
112
|
+
const panelWidth = Math.max(36, Math.min(width, MAX_PANEL_COLUMNS));
|
|
113
|
+
const innerWidth = Math.max(1, panelWidth - 4);
|
|
114
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, width: panelWidth, marginBottom: 1, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'green' : undefined, dimColor: index > 0, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`))) }));
|
|
115
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PROVIDERS, hasUsableEnvKey } from '../providers/registry.js';
|
|
2
|
+
import { resolveKeyFromEnv } from '../providers/keys.js';
|
|
3
|
+
/** Provider menu order — Codex sits right after OpenAI (ChatGPT plan vs API key). */
|
|
4
|
+
export const SETUP_PROVIDER_ORDER = [
|
|
5
|
+
'anthropic',
|
|
6
|
+
'openai',
|
|
7
|
+
'codex',
|
|
8
|
+
'google',
|
|
9
|
+
'xai',
|
|
10
|
+
'mistral',
|
|
11
|
+
'groq',
|
|
12
|
+
'ollama',
|
|
13
|
+
'lmstudio',
|
|
14
|
+
];
|
|
15
|
+
/** label + hint ต่อ provider: เจอ key ใน env / local / ChatGPT-login / ต้องมี key — ให้เลือกง่ายขึ้น */
|
|
16
|
+
export function providerOption(id) {
|
|
17
|
+
const p = PROVIDERS[id];
|
|
18
|
+
let hint;
|
|
19
|
+
if (p.kind === 'delegate')
|
|
20
|
+
hint = 'login ChatGPT · ไม่ใช้ API key';
|
|
21
|
+
else if (!p.requiresKey)
|
|
22
|
+
hint = 'local · ไม่ต้อง key';
|
|
23
|
+
else if (hasUsableEnvKey(id))
|
|
24
|
+
hint = '✓ key ใน env ใช้ได้';
|
|
25
|
+
else if (resolveKeyFromEnv(p.envVar, p.envFallbacks))
|
|
26
|
+
hint = 'key ใน env ใช้ไม่ได้';
|
|
27
|
+
else
|
|
28
|
+
hint = 'ต้องมี API key';
|
|
29
|
+
return { label: `${p.label} — ${hint}`, value: p.id };
|
|
30
|
+
}
|
|
31
|
+
export function setupProviderOptions() {
|
|
32
|
+
return SETUP_PROVIDER_ORDER.filter((id) => PROVIDERS[id]).map((id) => providerOption(id));
|
|
33
|
+
}
|
|
34
|
+
/** Static lines so every provider (incl. Codex) is visible before scrolling the Select. */
|
|
35
|
+
export function setupProviderMenuLines() {
|
|
36
|
+
return setupProviderOptions().map((option, index) => {
|
|
37
|
+
const marker = option.value === 'codex' ? '★' : '·';
|
|
38
|
+
return ` ${marker} ${index + 1}. ${option.label}`;
|
|
39
|
+
});
|
|
40
|
+
}
|
package/dist/ui/setup.js
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { Select, PasswordInput } from '@inkjs/ui';
|
|
5
|
-
import { PROVIDERS, consoleUrl
|
|
6
|
-
import {
|
|
5
|
+
import { PROVIDERS, consoleUrl } from '../providers/registry.js';
|
|
6
|
+
import { assertDirectApiKey } from '../providers/keys.js';
|
|
7
7
|
import { listRemoteModels, mergeModelOptions } from '../providers/models.js';
|
|
8
8
|
import { detectCodex } from '../providers/codex.js';
|
|
9
|
+
import { CODEX_DEVICE_VERIFY_URL, runCodexDeviceCodeLogin } from '../providers/codex-login.js';
|
|
9
10
|
import { BRAND } from '../brand.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const p = PROVIDERS[id];
|
|
15
|
-
let hint;
|
|
16
|
-
if (p.kind === 'delegate')
|
|
17
|
-
hint = 'login ChatGPT · ไม่ใช้ API key';
|
|
18
|
-
else if (!p.requiresKey)
|
|
19
|
-
hint = 'local · ไม่ต้อง key';
|
|
20
|
-
else if (hasUsableEnvKey(id))
|
|
21
|
-
hint = '✓ key ใน env ใช้ได้';
|
|
22
|
-
else if (resolveKeyFromEnv(p.envVar, p.envFallbacks))
|
|
23
|
-
hint = 'key ใน env ใช้ไม่ได้';
|
|
24
|
-
else
|
|
25
|
-
hint = 'ต้องมี API key';
|
|
26
|
-
return { label: `${p.label} — ${hint}`, value: p.id };
|
|
27
|
-
}
|
|
28
|
-
/** first-run setup wizard: เลือก provider → (codex login | API key) → เลือก model → เสนอสร้าง second-brain */
|
|
11
|
+
import { setupProviderMenuLines, setupProviderOptions } from './setup-providers.js';
|
|
12
|
+
import { detectDefaultLocale, getLocaleCatalog, normalizeLocale } from '../i18n/index.js';
|
|
13
|
+
export { providerOption } from './setup-providers.js';
|
|
14
|
+
/** first-run setup wizard: language → welcome → provider → auth → model → brain → complete */
|
|
29
15
|
export function SetupWizard({ onComplete }) {
|
|
30
|
-
const [step, setStep] = useState('
|
|
16
|
+
const [step, setStep] = useState('language');
|
|
17
|
+
const [locale, setLocale] = useState(detectDefaultLocale());
|
|
18
|
+
const m = getLocaleCatalog(locale).setup;
|
|
31
19
|
const [provider, setProvider] = useState('');
|
|
32
20
|
const [key, setKey] = useState('');
|
|
33
21
|
const [model, setModel] = useState('');
|
|
@@ -37,8 +25,22 @@ export function SetupWizard({ onComplete }) {
|
|
|
37
25
|
const [recheck, setRecheck] = useState(0);
|
|
38
26
|
const [keyDraft, setKeyDraft] = useState('');
|
|
39
27
|
const [keyError, setKeyError] = useState('');
|
|
28
|
+
const [codexDeviceCode, setCodexDeviceCode] = useState('');
|
|
29
|
+
const [codexDeviceStatus, setCodexDeviceStatus] = useState('idle');
|
|
30
|
+
const [codexDeviceError, setCodexDeviceError] = useState('');
|
|
31
|
+
const [codexDeviceAttempt, setCodexDeviceAttempt] = useState(0);
|
|
32
|
+
const [permissionMode, setPermissionMode] = useState('ask');
|
|
33
|
+
const [gatewayHint, setGatewayHint] = useState();
|
|
40
34
|
const cfg = provider ? PROVIDERS[provider] : undefined;
|
|
41
|
-
const providerOptions =
|
|
35
|
+
const providerOptions = setupProviderOptions();
|
|
36
|
+
const providerMenuLines = setupProviderMenuLines();
|
|
37
|
+
const advanceIfCodexReady = (status) => {
|
|
38
|
+
if (!status.loggedIn)
|
|
39
|
+
return;
|
|
40
|
+
setModel(`codex:${PROVIDERS.codex.models.default}`);
|
|
41
|
+
if (status.installed)
|
|
42
|
+
setStep('agent');
|
|
43
|
+
};
|
|
42
44
|
// codex-auth: เช็ก codex CLI ติดตั้ง + login ChatGPT (re-run เมื่อกด "เช็กใหม่")
|
|
43
45
|
useEffect(() => {
|
|
44
46
|
if (step !== 'codex-auth')
|
|
@@ -49,16 +51,48 @@ export function SetupWizard({ onComplete }) {
|
|
|
49
51
|
if (!alive)
|
|
50
52
|
return;
|
|
51
53
|
setCodexStatus(s);
|
|
52
|
-
|
|
53
|
-
// login แล้ว → ใช้ default model ของ codex (ChatGPT-plan เลือก model เอง) ข้ามขั้นเลือก key/model
|
|
54
|
-
setModel(`codex:${PROVIDERS.codex.models.default}`);
|
|
55
|
-
setStep('brain-offer');
|
|
56
|
-
}
|
|
54
|
+
advanceIfCodexReady(s);
|
|
57
55
|
});
|
|
58
56
|
return () => {
|
|
59
57
|
alive = false;
|
|
60
58
|
};
|
|
61
59
|
}, [step, recheck]);
|
|
60
|
+
// Hermes-style device-code login (writes ~/.codex/auth.json for the official CLI)
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (step !== 'codex-device-code')
|
|
63
|
+
return;
|
|
64
|
+
let alive = true;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
setCodexDeviceStatus('running');
|
|
67
|
+
setCodexDeviceError('');
|
|
68
|
+
setCodexDeviceCode('');
|
|
69
|
+
void runCodexDeviceCodeLogin({
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
onStatus: (message) => {
|
|
72
|
+
if (!alive)
|
|
73
|
+
return;
|
|
74
|
+
if (message.startsWith('code:'))
|
|
75
|
+
setCodexDeviceCode(message.slice('code:'.length));
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
.then(() => {
|
|
79
|
+
if (!alive)
|
|
80
|
+
return;
|
|
81
|
+
setCodexDeviceStatus('done');
|
|
82
|
+
setRecheck((n) => n + 1);
|
|
83
|
+
setStep('codex-auth');
|
|
84
|
+
})
|
|
85
|
+
.catch((e) => {
|
|
86
|
+
if (!alive)
|
|
87
|
+
return;
|
|
88
|
+
setCodexDeviceStatus('error');
|
|
89
|
+
setCodexDeviceError(e.message);
|
|
90
|
+
});
|
|
91
|
+
return () => {
|
|
92
|
+
alive = false;
|
|
93
|
+
controller.abort();
|
|
94
|
+
};
|
|
95
|
+
}, [step, codexDeviceAttempt]);
|
|
62
96
|
// ดึงรายชื่อ model จริงจาก provider (เฉพาะ provider แบบ SDK ที่ต้อง/ไม่ต้อง key)
|
|
63
97
|
useEffect(() => {
|
|
64
98
|
if (step !== 'model' || !cfg)
|
|
@@ -73,35 +107,61 @@ export function SetupWizard({ onComplete }) {
|
|
|
73
107
|
};
|
|
74
108
|
}, [step, cfg, key]);
|
|
75
109
|
const modelOptions = cfg ? mergeModelOptions(cfg, remote) : [];
|
|
76
|
-
const finish = (createBrain) =>
|
|
110
|
+
const finish = (createBrain) => {
|
|
111
|
+
if (createBrain) {
|
|
112
|
+
onComplete({
|
|
113
|
+
locale,
|
|
114
|
+
provider,
|
|
115
|
+
model,
|
|
116
|
+
envVar: cfg?.envVar ?? '',
|
|
117
|
+
key,
|
|
118
|
+
permissionMode,
|
|
119
|
+
gatewayHint,
|
|
120
|
+
createBrain: true,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setStep('complete');
|
|
125
|
+
};
|
|
126
|
+
const finishRepl = () => onComplete({
|
|
127
|
+
locale,
|
|
128
|
+
provider,
|
|
129
|
+
model,
|
|
130
|
+
envVar: cfg?.envVar ?? '',
|
|
131
|
+
key,
|
|
132
|
+
permissionMode,
|
|
133
|
+
gatewayHint,
|
|
134
|
+
createBrain: false,
|
|
135
|
+
});
|
|
77
136
|
const backToProvider = () => {
|
|
78
137
|
setProvider('');
|
|
79
138
|
setCodexStatus(null);
|
|
80
139
|
setKeyError('');
|
|
81
140
|
setKey('');
|
|
82
141
|
setKeyDraft('');
|
|
142
|
+
setCodexDeviceCode('');
|
|
143
|
+
setCodexDeviceStatus('idle');
|
|
144
|
+
setCodexDeviceError('');
|
|
83
145
|
setStep('provider');
|
|
84
146
|
};
|
|
85
147
|
// Esc บนทุก step (ยกเว้น provider) = ย้อนกลับไปเลือก provider — กัน dead-end ตอนเลือกผิด
|
|
86
|
-
// หรือ codex detect ค้าง (step codex-auth ตอน pending ไม่มีปุ่มอื่น แต่ Esc ออกได้เสมอ)
|
|
87
148
|
useInput((_input, key) => {
|
|
88
149
|
if (key.return && step === 'key' && !keyDraft.trim()) {
|
|
89
|
-
setKeyError(
|
|
150
|
+
setKeyError(m.keyEmptyError);
|
|
90
151
|
return;
|
|
91
152
|
}
|
|
92
|
-
if (key.escape && step !== 'provider')
|
|
153
|
+
if (key.escape && step !== 'provider' && step !== 'language' && step !== 'codex-device-code')
|
|
93
154
|
backToProvider();
|
|
94
155
|
});
|
|
95
|
-
// ตรวจ API key ในขั้นใส่ key — ว่าง = ไม่ผ่าน, OAuth/format ผิด = บอก error (กัน setup จบทั้งที่ key ใช้ไม่ได้)
|
|
96
156
|
const submitKey = (raw) => {
|
|
97
157
|
const k = raw.trim();
|
|
98
158
|
if (!k) {
|
|
99
|
-
setKeyError(
|
|
159
|
+
setKeyError(m.keyEmptyError);
|
|
100
160
|
return;
|
|
101
161
|
}
|
|
102
162
|
if (cfg) {
|
|
103
163
|
try {
|
|
104
|
-
assertDirectApiKey(cfg, k);
|
|
164
|
+
assertDirectApiKey(cfg, k);
|
|
105
165
|
}
|
|
106
166
|
catch (e) {
|
|
107
167
|
setKeyError(e.message.split('\n')[0]);
|
|
@@ -113,7 +173,13 @@ export function SetupWizard({ onComplete }) {
|
|
|
113
173
|
setKeyDraft(k);
|
|
114
174
|
setStep('model');
|
|
115
175
|
};
|
|
116
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["\u2699
|
|
176
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["\u2699 ", m.title] }), step === 'language' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepLanguage, " (\u2191\u2193 \u00B7 Enter):"] }), _jsxs(Text, { color: "gray", children: [" ", m.languageHint] }), _jsx(Select, { options: [
|
|
177
|
+
{ label: m.languageTh, value: 'th' },
|
|
178
|
+
{ label: m.languageEn, value: 'en' },
|
|
179
|
+
], onChange: (v) => {
|
|
180
|
+
setLocale(normalizeLocale(v));
|
|
181
|
+
setStep('welcome');
|
|
182
|
+
} })] })), step === 'welcome' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepWelcome }), _jsx(Text, { color: "gray", children: m.welcomeBody }), _jsx(Select, { options: [{ label: m.welcomeContinue, value: 'continue' }], onChange: () => setStep('provider') })] })), step === 'provider' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepProvider, " (\u2191\u2193 \u00B7 Enter):"] }), _jsxs(Text, { color: "gray", children: [" ", m.providerHint] }), _jsxs(Text, { color: "gray", children: [" ", m.providerMenuHint] }), providerMenuLines.map((line) => (_jsx(Text, { color: "gray", children: line }, line))), _jsx(Select, { options: providerOptions, onChange: (v) => {
|
|
117
183
|
setProvider(v);
|
|
118
184
|
const p = PROVIDERS[v];
|
|
119
185
|
if (p.kind === 'delegate')
|
|
@@ -122,23 +188,70 @@ export function SetupWizard({ onComplete }) {
|
|
|
122
188
|
setStep('key');
|
|
123
189
|
else
|
|
124
190
|
setStep('model');
|
|
125
|
-
} })] })), step === 'codex-auth' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children:
|
|
126
|
-
{ label:
|
|
127
|
-
{ label:
|
|
128
|
-
], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : !codexStatus.
|
|
129
|
-
{ label:
|
|
130
|
-
{ label:
|
|
131
|
-
], onChange: (v) =>
|
|
191
|
+
} })] })), step === 'codex-auth' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepCodex }), codexStatus === null ? (_jsxs(Text, { color: "gray", children: [" ", m.codexChecking] })) : codexStatus.loggedIn && !codexStatus.installed ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", children: [" \u2705 ", m.codexReady] }), _jsxs(Text, { color: "yellow", children: [" \u26A0 ", m.codexLoggedInNeedCli] }), _jsxs(Text, { children: [' ', _jsx(Text, { color: "cyan", children: m.codexInstallCmd })] }), _jsx(Select, { options: [
|
|
192
|
+
{ label: `${m.recheckLabel}`, value: 'recheck' },
|
|
193
|
+
{ label: m.codexOptionBack, value: 'back' },
|
|
194
|
+
], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : !codexStatus.installed ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [" \u274C ", m.codexNeedInstall] }), _jsx(Select, { options: [
|
|
195
|
+
{ label: m.codexOptionDevice, value: 'device-code' },
|
|
196
|
+
{ label: m.codexOptionBack, value: 'back' },
|
|
197
|
+
], onChange: (v) => {
|
|
198
|
+
if (v === 'device-code')
|
|
199
|
+
setStep('codex-device-code');
|
|
200
|
+
else if (v === 'back')
|
|
201
|
+
backToProvider();
|
|
202
|
+
} })] })) : !codexStatus.loggedIn ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [" \u26A0 ", m.codexNeedLogin] }), _jsx(Select, { options: [
|
|
203
|
+
{ label: m.codexOptionDevice, value: 'device-code' },
|
|
204
|
+
{ label: m.codexOptionCliLogin, value: 'cli-login' },
|
|
205
|
+
{ label: m.recheckLabel, value: 'recheck' },
|
|
206
|
+
{ label: m.codexOptionBack, value: 'back' },
|
|
207
|
+
], onChange: (v) => {
|
|
208
|
+
if (v === 'device-code')
|
|
209
|
+
setStep('codex-device-code');
|
|
210
|
+
else if (v === 'recheck')
|
|
211
|
+
setRecheck((n) => n + 1);
|
|
212
|
+
else if (v === 'back')
|
|
213
|
+
backToProvider();
|
|
214
|
+
} }), _jsx(Text, { color: "gray", children: " codex login" })] })) : (_jsxs(Text, { color: "green", children: [" \u2705 ", m.codexReady] }))] })), step === 'codex-device-code' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.codexDeviceTitle }), codexDeviceStatus === 'running' ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: [" ", m.codexDeviceOpen] }), _jsxs(Text, { color: "cyan", children: [" ", CODEX_DEVICE_VERIFY_URL] }), codexDeviceCode ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "gray", children: [" ", m.codexDeviceEnter] }), _jsx(Text, { color: "cyan", bold: true, children: ` ${codexDeviceCode}` })] })) : (_jsx(Text, { color: "gray", children: " \u2026" })), _jsxs(Text, { color: "gray", children: [" ", m.codexDeviceWaiting] })] })) : codexDeviceStatus === 'error' ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "red", children: [" \u2717 ", codexDeviceError] }), _jsx(Select, { options: [
|
|
215
|
+
{ label: m.codexDeviceRetry, value: 'retry' },
|
|
216
|
+
{ label: m.codexDeviceBack, value: 'back' },
|
|
217
|
+
], onChange: (v) => v === 'retry' ? setCodexDeviceAttempt((n) => n + 1) : setStep('codex-auth') })] })) : (_jsx(Text, { color: "green", children: " \u2705 ~/.codex/auth.json" }))] })), step === 'key' && cfg && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepKey, " \u2014 ", cfg.label, ": ", _jsx(Text, { color: "gray", children: m.keyEscHint })] }), provider === 'openai' ? _jsxs(Text, { color: "yellow", children: [" ", m.keyOpenAiCodexHint] }) : null, consoleUrl(provider) ? _jsxs(Text, { color: "cyan", children: [" \u2192 ", consoleUrl(provider)] }) : null, cfg.keyExample ? (_jsxs(Text, { color: "gray", children: [' ', m.keyFormatHint, ": ", cfg.keyExample] })) : null, _jsxs(Text, { color: "gray", children: [" ", m.keyStorageHint] }), _jsx(PasswordInput, { placeholder: cfg.envVar, onChange: (v) => {
|
|
132
218
|
setKeyDraft(v);
|
|
133
219
|
if (keyError)
|
|
134
220
|
setKeyError('');
|
|
135
221
|
}, onSubmit: submitKey }), keyError ? _jsxs(Text, { color: "red", children: [" \u2717 ", keyError] }) : null] })), step === 'model' &&
|
|
136
222
|
cfg &&
|
|
137
|
-
(loadingModels ? (_jsxs(Text, { color: "gray", children: [
|
|
223
|
+
(loadingModels ? (_jsxs(Text, { color: "gray", children: [' ', m.modelLoading, " ", cfg.label, "\u2026"] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [m.stepModel, " \u2014 ", m.modelPick, remote.length ? _jsxs(Text, { color: "gray", children: [" (", modelOptions.length, ")"] }) : null, ":"] }), _jsx(Select, { options: modelOptions, onChange: (v) => {
|
|
138
224
|
setModel(`${provider}:${v}`);
|
|
139
|
-
setStep('
|
|
140
|
-
} })] }))), step === '
|
|
141
|
-
{ label:
|
|
142
|
-
{ label:
|
|
143
|
-
], onChange: (v) =>
|
|
225
|
+
setStep('agent');
|
|
226
|
+
} })] }))), step === 'agent' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepAgent }), _jsx(Text, { color: "gray", children: m.agentTitle }), _jsx(Select, { options: [
|
|
227
|
+
{ label: m.agentAsk, value: 'ask' },
|
|
228
|
+
{ label: m.agentAuto, value: 'auto' },
|
|
229
|
+
], onChange: (v) => {
|
|
230
|
+
setPermissionMode(v);
|
|
231
|
+
setStep('tools');
|
|
232
|
+
} }), _jsxs(Text, { color: "gray", children: [" ", m.agentHint] })] })), step === 'tools' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepTools }), _jsx(Text, { color: "gray", children: m.toolsBody }), _jsxs(Text, { color: "gray", children: [" ", m.toolsMcpHint] }), _jsx(Select, { options: [
|
|
233
|
+
{ label: m.toolsWebSkip, value: 'skip' },
|
|
234
|
+
{ label: m.toolsWebLater, value: 'later' },
|
|
235
|
+
], onChange: () => setStep('gateway') })] })), step === 'gateway' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepGateway }), _jsx(Text, { color: "gray", children: m.gatewayBody }), _jsx(Select, { options: [
|
|
236
|
+
{ label: m.gatewaySkip, value: 'skip' },
|
|
237
|
+
{ label: m.gatewayTelegram, value: 'telegram' },
|
|
238
|
+
{ label: m.gatewayDiscord, value: 'discord' },
|
|
239
|
+
{ label: m.gatewaySlack, value: 'slack' },
|
|
240
|
+
{ label: m.gatewayDashboard, value: 'dashboard' },
|
|
241
|
+
], onChange: (v) => {
|
|
242
|
+
if (v === 'telegram')
|
|
243
|
+
setGatewayHint('sanook gateway setup telegram');
|
|
244
|
+
else if (v === 'discord')
|
|
245
|
+
setGatewayHint('sanook gateway setup discord');
|
|
246
|
+
else if (v === 'slack')
|
|
247
|
+
setGatewayHint('sanook gateway setup slack');
|
|
248
|
+
else if (v === 'dashboard')
|
|
249
|
+
setGatewayHint('sanook dashboard → Channels');
|
|
250
|
+
else
|
|
251
|
+
setGatewayHint(undefined);
|
|
252
|
+
setStep('brain-offer');
|
|
253
|
+
} })] })), step === 'brain-offer' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepBrain }), _jsx(Text, { color: "gray", children: m.brainQuestion }), _jsx(Select, { options: [
|
|
254
|
+
{ label: m.brainYes, value: 'yes' },
|
|
255
|
+
{ label: m.brainNo, value: 'no' },
|
|
256
|
+
], onChange: (v) => finish(v === 'yes') })] })), step === 'complete' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: m.stepComplete }), _jsx(Text, { bold: true, children: m.completeTitle }), _jsx(Text, { color: "gray", children: m.completeBody }), _jsxs(Text, { color: "cyan", children: [" ", m.completeDashboard, ": ", BRAND.cliName, " dashboard"] }), gatewayHint ? _jsxs(Text, { color: "yellow", children: [" Gateway: ", gatewayHint] }) : null, _jsxs(Text, { color: "gray", children: [" permissionMode: ", permissionMode] }), _jsx(Select, { options: [{ label: m.completeRepl, value: 'repl' }], onChange: () => finishRepl() })] }))] }));
|
|
144
257
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const clip = (text, width) => {
|
|
2
|
+
if (width <= 0)
|
|
3
|
+
return '';
|
|
4
|
+
return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text;
|
|
5
|
+
};
|
|
6
|
+
export function statusSegments(columns) {
|
|
7
|
+
const width = Math.max(20, Math.floor(columns || 80));
|
|
8
|
+
return {
|
|
9
|
+
backgroundTasks: width >= 42,
|
|
10
|
+
compression: width >= 88,
|
|
11
|
+
contextBar: width >= 96,
|
|
12
|
+
cost: width >= 78,
|
|
13
|
+
cwd: width >= 64,
|
|
14
|
+
elapsed: width >= 58,
|
|
15
|
+
hints: width >= 46,
|
|
16
|
+
hotkeys: width >= 72,
|
|
17
|
+
queue: width >= 42,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function statusRuleWidths(columns, rightLabel, minLeftContent = 0) {
|
|
21
|
+
const width = Math.max(1, Math.floor(columns || 1));
|
|
22
|
+
const separatorWidth = width >= 48 ? 3 : 1;
|
|
23
|
+
const baseLeft = width >= 48 ? 20 : 8;
|
|
24
|
+
const leftFloor = Math.min(width, Math.max(baseLeft, Math.floor(minLeftContent)));
|
|
25
|
+
const maxRight = Math.max(0, width - separatorWidth - leftFloor);
|
|
26
|
+
if (!rightLabel || maxRight <= 0)
|
|
27
|
+
return { leftWidth: width, rightWidth: 0, separatorWidth: 0 };
|
|
28
|
+
const rightWidth = Math.min(rightLabel.length, maxRight);
|
|
29
|
+
return {
|
|
30
|
+
leftWidth: Math.max(1, width - separatorWidth - rightWidth),
|
|
31
|
+
rightWidth,
|
|
32
|
+
separatorWidth,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function footerStatus({ branch, backgroundTaskCount = 0, busy = false, columns, contextCompression, contextLimit = 100_000, contextTokens, costHint = '', cwd = '', elapsedSeconds, model, mode, queuedCount = 0, }) {
|
|
36
|
+
const width = Math.max(20, Math.floor(columns || 80));
|
|
37
|
+
const segments = statusSegments(width);
|
|
38
|
+
const state = busy ? 'working' : 'ready';
|
|
39
|
+
const parts = width < 40 ? [shortModel(model), mode] : ['SANOOK', state, shortModel(model), `${mode}-mode`];
|
|
40
|
+
if (contextTokens != null && width >= 52) {
|
|
41
|
+
parts.push(contextSegment(contextTokens, contextLimit, segments.contextBar));
|
|
42
|
+
}
|
|
43
|
+
if (contextCompression && segments.compression)
|
|
44
|
+
parts.push(compressionSegment(contextCompression));
|
|
45
|
+
if (busy && elapsedSeconds != null && segments.elapsed)
|
|
46
|
+
parts.push(`time ${formatElapsed(elapsedSeconds)}`);
|
|
47
|
+
if (queuedCount > 0 && segments.queue)
|
|
48
|
+
parts.push(`q ${queuedCount}`);
|
|
49
|
+
if (backgroundTaskCount > 0 && segments.backgroundTasks)
|
|
50
|
+
parts.push(`bg ${backgroundTaskCount}`);
|
|
51
|
+
if (costHint && segments.cost)
|
|
52
|
+
parts.push(`cost ${costHint}`);
|
|
53
|
+
if (segments.hints)
|
|
54
|
+
parts.push('/help', '@file');
|
|
55
|
+
if (segments.hotkeys)
|
|
56
|
+
parts.push('/hotkeys');
|
|
57
|
+
const left = ` ${parts.join(' · ')}`;
|
|
58
|
+
if (!segments.cwd || !cwd)
|
|
59
|
+
return clip(left, width);
|
|
60
|
+
const right = formatCwd(cwd, branch);
|
|
61
|
+
const minRight = width >= 96 ? Math.min(right.length, 22) : Math.min(right.length, 12);
|
|
62
|
+
const minLeft = Math.min(width, Math.max(20, Math.min(left.length, width - 3 - minRight)));
|
|
63
|
+
const rule = statusRuleWidths(width, right, minLeft);
|
|
64
|
+
if (!rule.rightWidth)
|
|
65
|
+
return clip(left, width);
|
|
66
|
+
const leftPart = clip(left, rule.leftWidth).padEnd(rule.leftWidth);
|
|
67
|
+
const rightPart = clip(right, rule.rightWidth);
|
|
68
|
+
return `${leftPart}${' '.repeat(rule.separatorWidth)}${rightPart}`;
|
|
69
|
+
}
|
|
70
|
+
function shortModel(model) {
|
|
71
|
+
if (model.includes(':')) {
|
|
72
|
+
const [provider, name] = model.split(':', 2);
|
|
73
|
+
return `${provider}:${clip(name ?? '', 18)}`;
|
|
74
|
+
}
|
|
75
|
+
return clip(model, 24);
|
|
76
|
+
}
|
|
77
|
+
function contextSegment(tokens, limit, showBar) {
|
|
78
|
+
const safeTokens = Math.max(0, Math.floor(tokens));
|
|
79
|
+
const safeLimit = Math.max(1, Math.floor(limit));
|
|
80
|
+
const pct = Math.max(0, Math.min(100, Math.round((safeTokens / safeLimit) * 100)));
|
|
81
|
+
if (!showBar)
|
|
82
|
+
return `ctx ${formatTokens(safeTokens)}`;
|
|
83
|
+
return `ctx ${ctxBar(pct)} ${pct}%`;
|
|
84
|
+
}
|
|
85
|
+
function ctxBar(percent, width = 6) {
|
|
86
|
+
const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)));
|
|
87
|
+
return `${'#'.repeat(filled)}${'-'.repeat(width - filled)}`;
|
|
88
|
+
}
|
|
89
|
+
function compressionSegment(mode) {
|
|
90
|
+
if (mode === 'headroom')
|
|
91
|
+
return 'cmp hdr';
|
|
92
|
+
if (mode === 'off')
|
|
93
|
+
return 'cmp off';
|
|
94
|
+
return 'cmp sel';
|
|
95
|
+
}
|
|
96
|
+
function formatTokens(tokens) {
|
|
97
|
+
if (tokens >= 1_000_000)
|
|
98
|
+
return `${trimNumber(tokens / 1_000_000)}m`;
|
|
99
|
+
if (tokens >= 1_000)
|
|
100
|
+
return `${trimNumber(tokens / 1_000)}k`;
|
|
101
|
+
return `${tokens}`;
|
|
102
|
+
}
|
|
103
|
+
export function formatElapsed(seconds) {
|
|
104
|
+
const safe = Math.max(0, Math.floor(seconds));
|
|
105
|
+
if (safe < 60)
|
|
106
|
+
return `${safe}s`;
|
|
107
|
+
const minutes = Math.floor(safe / 60);
|
|
108
|
+
const secs = safe % 60;
|
|
109
|
+
if (minutes < 60)
|
|
110
|
+
return `${minutes}m ${secs.toString().padStart(2, '0')}s`;
|
|
111
|
+
const hours = Math.floor(minutes / 60);
|
|
112
|
+
const mins = minutes % 60;
|
|
113
|
+
return `${hours}h ${mins.toString().padStart(2, '0')}m`;
|
|
114
|
+
}
|
|
115
|
+
function trimNumber(value) {
|
|
116
|
+
return value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, '');
|
|
117
|
+
}
|
|
118
|
+
export function formatCwd(cwd, branch) {
|
|
119
|
+
const home = process.env.HOME;
|
|
120
|
+
const homeBase = home && home !== '/' ? home.replace(/\/+$/, '') : undefined;
|
|
121
|
+
const inHome = Boolean(homeBase && (cwd === homeBase || cwd.startsWith(`${homeBase}/`)));
|
|
122
|
+
const label = inHome && homeBase
|
|
123
|
+
? cwd === homeBase
|
|
124
|
+
? '~'
|
|
125
|
+
: `~/${cwd.slice(homeBase.length + 1)}`
|
|
126
|
+
: cwd;
|
|
127
|
+
const parts = label.split('/').filter(Boolean);
|
|
128
|
+
const shortPath = label.startsWith('~/') && parts.length > 2
|
|
129
|
+
? `~/${parts.slice(-2).join('/')}`
|
|
130
|
+
: label.startsWith('/') && parts.length > 2
|
|
131
|
+
? `/${parts.slice(-2).join('/')}`
|
|
132
|
+
: label || cwd;
|
|
133
|
+
if (!branch)
|
|
134
|
+
return shortPath;
|
|
135
|
+
return `${shortPath} (${shortBranch(branch)})`;
|
|
136
|
+
}
|
|
137
|
+
function shortBranch(branch) {
|
|
138
|
+
const clean = branch.trim();
|
|
139
|
+
if (clean.length <= 19)
|
|
140
|
+
return clean;
|
|
141
|
+
return `…${clean.slice(-18)}`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const THINKING_CHAR_LIMIT = 2_000;
|
|
2
|
+
const THINKING_LINE_LIMIT = 6;
|
|
3
|
+
function clip(text, width) {
|
|
4
|
+
if (width <= 0)
|
|
5
|
+
return '';
|
|
6
|
+
return text.length > width ? `${text.slice(0, Math.max(0, width - 3))}...` : text;
|
|
7
|
+
}
|
|
8
|
+
function normalize(text) {
|
|
9
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
10
|
+
}
|
|
11
|
+
export function snapshotThinking(text) {
|
|
12
|
+
const clean = text.trim();
|
|
13
|
+
if (!clean)
|
|
14
|
+
return undefined;
|
|
15
|
+
const chars = Array.from(clean);
|
|
16
|
+
return chars.length > THINKING_CHAR_LIMIT ? `${chars.slice(0, THINKING_CHAR_LIMIT).join('')}\n[thinking truncated]` : clean;
|
|
17
|
+
}
|
|
18
|
+
export function thinkingPanelLines(text, columns, mode = 'collapsed') {
|
|
19
|
+
const clean = (text ?? '').trim();
|
|
20
|
+
if (!clean || mode === 'hidden')
|
|
21
|
+
return [];
|
|
22
|
+
const width = Math.max(24, Math.min(Math.max(30, columns - 4), 96));
|
|
23
|
+
const header = `Sanook thinking (${clean.length} chars)`;
|
|
24
|
+
const hint = `view: ${mode} | /details thinking hidden|collapsed|expanded`;
|
|
25
|
+
if (mode === 'collapsed') {
|
|
26
|
+
return [header, hint, clip(normalize(clean), width)].map((line) => clip(line, width));
|
|
27
|
+
}
|
|
28
|
+
const lines = clean
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.slice(0, THINKING_LINE_LIMIT)
|
|
33
|
+
.map((line) => clip(line, width));
|
|
34
|
+
const omitted = clean.split('\n').filter((line) => line.trim()).length - lines.length;
|
|
35
|
+
return [header, hint, ...lines, omitted > 0 ? `... ${omitted} more thinking lines` : ''].filter(Boolean).map((line) => clip(line, width));
|
|
36
|
+
}
|