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.
Files changed (217) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +148 -10
  3. package/README.md +255 -26
  4. package/README.th.md +95 -7
  5. package/dist/approval.js +13 -0
  6. package/dist/bin.js +3552 -155
  7. package/dist/brain-consolidate.js +335 -0
  8. package/dist/brain-context.js +262 -0
  9. package/dist/brain-doctor.js +318 -0
  10. package/dist/brain-eval.js +186 -0
  11. package/dist/brain-final.js +377 -0
  12. package/dist/brain-metrics.js +277 -0
  13. package/dist/brain-new.js +402 -0
  14. package/dist/brain-pack.js +210 -0
  15. package/dist/brain-repair.js +280 -0
  16. package/dist/brain-review.js +382 -0
  17. package/dist/brain.js +15 -1
  18. package/dist/brand.js +1 -1
  19. package/dist/cli-args.js +190 -0
  20. package/dist/cli-option-values.js +16 -0
  21. package/dist/clipboard.js +65 -0
  22. package/dist/commands.js +266 -27
  23. package/dist/compaction.js +96 -11
  24. package/dist/config.js +149 -33
  25. package/dist/context-compression.js +191 -0
  26. package/dist/context-pack.js +145 -0
  27. package/dist/cost.js +49 -15
  28. package/dist/dashboard/api-helpers.js +87 -0
  29. package/dist/dashboard/server.js +179 -0
  30. package/dist/dashboard/static/app.js +277 -0
  31. package/dist/dashboard/static/index.html +39 -0
  32. package/dist/dashboard/static/styles.css +85 -0
  33. package/dist/diff.js +10 -2
  34. package/dist/first-run.js +21 -0
  35. package/dist/gateway/auth.js +49 -9
  36. package/dist/gateway/bluebubbles.js +205 -0
  37. package/dist/gateway/config.js +929 -0
  38. package/dist/gateway/deliver.js +399 -0
  39. package/dist/gateway/discord.js +124 -0
  40. package/dist/gateway/doctor.js +456 -0
  41. package/dist/gateway/email.js +501 -0
  42. package/dist/gateway/googlechat.js +207 -0
  43. package/dist/gateway/homeassistant.js +256 -0
  44. package/dist/gateway/ledger.js +38 -1
  45. package/dist/gateway/line.js +171 -0
  46. package/dist/gateway/lock.js +3 -1
  47. package/dist/gateway/matrix.js +366 -0
  48. package/dist/gateway/mattermost.js +322 -0
  49. package/dist/gateway/ntfy.js +218 -0
  50. package/dist/gateway/schedule.js +31 -4
  51. package/dist/gateway/serve.js +267 -7
  52. package/dist/gateway/server.js +253 -19
  53. package/dist/gateway/service.js +224 -0
  54. package/dist/gateway/session.js +362 -0
  55. package/dist/gateway/signal.js +351 -0
  56. package/dist/gateway/slack.js +124 -0
  57. package/dist/gateway/sms.js +169 -0
  58. package/dist/gateway/targets.js +576 -0
  59. package/dist/gateway/teams.js +106 -0
  60. package/dist/gateway/telegram.js +38 -15
  61. package/dist/gateway/webhooks.js +220 -0
  62. package/dist/gateway/whatsapp.js +230 -0
  63. package/dist/hooks.js +13 -2
  64. package/dist/hotkeys.js +21 -0
  65. package/dist/i18n/en.js +98 -0
  66. package/dist/i18n/index.js +19 -0
  67. package/dist/i18n/th.js +98 -0
  68. package/dist/i18n/types.js +1 -0
  69. package/dist/insights-args.js +55 -0
  70. package/dist/insights.js +86 -0
  71. package/dist/knowledge.js +55 -29
  72. package/dist/loop.js +157 -29
  73. package/dist/lsp/index.js +23 -5
  74. package/dist/mcp-hub.js +33 -0
  75. package/dist/mcp-registry.js +494 -0
  76. package/dist/mcp-risk.js +71 -0
  77. package/dist/mcp-server.js +1 -1
  78. package/dist/mcp.js +120 -10
  79. package/dist/memory-log.js +90 -0
  80. package/dist/memory-store.js +37 -1
  81. package/dist/memory.js +148 -37
  82. package/dist/model-picker.js +58 -0
  83. package/dist/orchestrate.js +51 -19
  84. package/dist/personality.js +58 -0
  85. package/dist/plan-handoff.js +17 -0
  86. package/dist/polyglot.js +162 -0
  87. package/dist/process-runner.js +96 -0
  88. package/dist/project-init.js +91 -0
  89. package/dist/project-registry.js +143 -0
  90. package/dist/project-scaffold.js +124 -0
  91. package/dist/prompt-size.js +155 -0
  92. package/dist/providers/codex-login.js +138 -0
  93. package/dist/providers/codex.js +89 -43
  94. package/dist/providers/keys.js +22 -1
  95. package/dist/providers/models.js +2 -2
  96. package/dist/providers/registry.js +14 -47
  97. package/dist/search/chunk.js +7 -8
  98. package/dist/search/cli.js +83 -0
  99. package/dist/search/embed-store.js +3 -0
  100. package/dist/search/embedding-config.js +22 -0
  101. package/dist/search/engine.js +2 -13
  102. package/dist/search/indexer.js +44 -1
  103. package/dist/search/store.js +23 -1
  104. package/dist/session-distill.js +84 -0
  105. package/dist/session.js +92 -16
  106. package/dist/skill-install.js +53 -13
  107. package/dist/skills.js +33 -0
  108. package/dist/slash-completion.js +155 -0
  109. package/dist/support-dump.js +206 -0
  110. package/dist/tool-catalog.js +59 -0
  111. package/dist/tools/edit.js +45 -15
  112. package/dist/tools/git.js +10 -5
  113. package/dist/tools/homeassistant.js +106 -0
  114. package/dist/tools/index.js +10 -0
  115. package/dist/tools/list.js +19 -6
  116. package/dist/tools/permission.js +992 -12
  117. package/dist/tools/polyglot.js +126 -0
  118. package/dist/tools/read.js +16 -4
  119. package/dist/tools/sandbox.js +38 -13
  120. package/dist/tools/schedule.js +19 -3
  121. package/dist/tools/search.js +226 -15
  122. package/dist/tools/task.js +40 -9
  123. package/dist/tools/timeout.js +23 -3
  124. package/dist/tools/web-fetch-tool.js +33 -0
  125. package/dist/trust.js +11 -1
  126. package/dist/turn-retrieval.js +83 -0
  127. package/dist/ui/app.js +878 -32
  128. package/dist/ui/banner.js +78 -4
  129. package/dist/ui/history.js +37 -5
  130. package/dist/ui/markdown.js +122 -0
  131. package/dist/ui/mentions.js +3 -2
  132. package/dist/ui/overlay.js +496 -0
  133. package/dist/ui/queue.js +23 -0
  134. package/dist/ui/render.js +20 -1
  135. package/dist/ui/session-panel.js +115 -0
  136. package/dist/ui/setup-providers.js +40 -0
  137. package/dist/ui/setup.js +172 -46
  138. package/dist/ui/status.js +142 -0
  139. package/dist/ui/thinking-panel.js +36 -0
  140. package/dist/ui/tool-trail.js +97 -0
  141. package/dist/ui/transcript.js +26 -0
  142. package/dist/ui/useBusyElapsed.js +19 -0
  143. package/dist/ui/useEditor.js +144 -5
  144. package/dist/ui/useGitBranch.js +57 -0
  145. package/dist/update.js +56 -17
  146. package/dist/web-fetch.js +637 -0
  147. package/dist/web-surface.js +190 -0
  148. package/dist/worktree.js +175 -4
  149. package/package.json +5 -5
  150. package/second-brain/AGENTS.md +6 -4
  151. package/second-brain/CLAUDE.md +7 -1
  152. package/second-brain/Evals/_Index.md +10 -2
  153. package/second-brain/Evals/quality-ledger.md +9 -1
  154. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  155. package/second-brain/GEMINI.md +5 -4
  156. package/second-brain/Home.md +1 -1
  157. package/second-brain/Projects/_Index.md +19 -4
  158. package/second-brain/Projects/sanook-cli/_Index.md +30 -0
  159. package/second-brain/Projects/sanook-cli/context.md +35 -0
  160. package/second-brain/Projects/sanook-cli/current-state.md +32 -0
  161. package/second-brain/Projects/sanook-cli/overview.md +41 -0
  162. package/second-brain/Projects/sanook-cli/repo.md +34 -0
  163. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
  164. package/second-brain/README.md +1 -1
  165. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  166. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  167. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  168. package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
  169. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  170. package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
  171. package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
  172. package/second-brain/Research/_Index.md +8 -1
  173. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  174. package/second-brain/Reviews/_Index.md +1 -1
  175. package/second-brain/Runbooks/_Index.md +6 -1
  176. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  177. package/second-brain/SANOOK.md +45 -0
  178. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  179. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  180. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  181. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  182. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  183. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  184. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  185. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  186. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  187. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  188. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  189. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  190. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  191. package/second-brain/Sessions/_Index.md +15 -1
  192. package/second-brain/Shared/AI-Context-Index.md +22 -0
  193. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  194. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  195. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  196. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  197. package/second-brain/Shared/Operating-State/current-state.md +14 -4
  198. package/second-brain/Shared/Scripts/_Index.md +3 -1
  199. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  200. package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
  201. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  202. package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
  203. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  204. package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
  205. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  206. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  207. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  208. package/second-brain/Templates/_Index.md +9 -0
  209. package/second-brain/Templates/final-lite.md +111 -0
  210. package/second-brain/Templates/final.md +231 -0
  211. package/second-brain/Templates/project-workspace/_Index.md +31 -0
  212. package/second-brain/Templates/project-workspace/context.md +28 -0
  213. package/second-brain/Templates/project-workspace/current-state.md +29 -0
  214. package/second-brain/Templates/project-workspace/overview.md +39 -0
  215. package/second-brain/Templates/project-workspace/repo.md +33 -0
  216. package/second-brain/Vault Structure Map.md +2 -1
  217. package/skills/structured-output-llm/SKILL.md +1 -1
@@ -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,31 +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
5
  import { PROVIDERS, consoleUrl } from '../providers/registry.js';
6
- import { resolveKeyFromEnv, assertDirectApiKey } from '../providers/keys.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
- // จัดลำดับ provider ในเมนู: cloud ยอดนิยม → cloud อื่น → local → ChatGPT-plan (codex) ท้ายสุด
11
- const PROVIDER_ORDER = ['anthropic', 'openai', 'google', 'deepseek', 'xai', 'mistral', 'groq', 'glm', 'minimax', 'ollama', 'lmstudio', 'codex'];
12
- /** label + hint ต่อ provider: เจอ key ใน env / local / ChatGPT-login / ต้องมี key — ให้เลือกง่ายขึ้น */
13
- export function providerOption(id) {
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 (resolveKeyFromEnv(p.envVar, p.envFallbacks))
21
- hint = '✓ เจอ key ใน env';
22
- else
23
- hint = 'ต้องมี API key';
24
- return { label: `${p.label} — ${hint}`, value: p.id };
25
- }
26
- /** 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 */
27
15
  export function SetupWizard({ onComplete }) {
28
- const [step, setStep] = useState('provider');
16
+ const [step, setStep] = useState('language');
17
+ const [locale, setLocale] = useState(detectDefaultLocale());
18
+ const m = getLocaleCatalog(locale).setup;
29
19
  const [provider, setProvider] = useState('');
30
20
  const [key, setKey] = useState('');
31
21
  const [model, setModel] = useState('');
@@ -33,9 +23,24 @@ export function SetupWizard({ onComplete }) {
33
23
  const [loadingModels, setLoadingModels] = useState(false);
34
24
  const [codexStatus, setCodexStatus] = useState(null);
35
25
  const [recheck, setRecheck] = useState(0);
26
+ const [keyDraft, setKeyDraft] = useState('');
36
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();
37
34
  const cfg = provider ? PROVIDERS[provider] : undefined;
38
- const providerOptions = PROVIDER_ORDER.filter((id) => PROVIDERS[id]).map(providerOption);
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
+ };
39
44
  // codex-auth: เช็ก codex CLI ติดตั้ง + login ChatGPT (re-run เมื่อกด "เช็กใหม่")
40
45
  useEffect(() => {
41
46
  if (step !== 'codex-auth')
@@ -46,16 +51,48 @@ export function SetupWizard({ onComplete }) {
46
51
  if (!alive)
47
52
  return;
48
53
  setCodexStatus(s);
49
- if (s.installed && s.loggedIn) {
50
- // login แล้ว → ใช้ default model ของ codex (ChatGPT-plan เลือก model เอง) ข้ามขั้นเลือก key/model
51
- setModel(`codex:${PROVIDERS.codex.models.default}`);
52
- setStep('brain-offer');
53
- }
54
+ advanceIfCodexReady(s);
54
55
  });
55
56
  return () => {
56
57
  alive = false;
57
58
  };
58
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]);
59
96
  // ดึงรายชื่อ model จริงจาก provider (เฉพาะ provider แบบ SDK ที่ต้อง/ไม่ต้อง key)
60
97
  useEffect(() => {
61
98
  if (step !== 'model' || !cfg)
@@ -70,30 +107,61 @@ export function SetupWizard({ onComplete }) {
70
107
  };
71
108
  }, [step, cfg, key]);
72
109
  const modelOptions = cfg ? mergeModelOptions(cfg, remote) : [];
73
- const finish = (createBrain) => onComplete({ provider, model, envVar: cfg?.envVar ?? '', key, 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
+ });
74
136
  const backToProvider = () => {
75
137
  setProvider('');
76
138
  setCodexStatus(null);
77
139
  setKeyError('');
78
140
  setKey('');
141
+ setKeyDraft('');
142
+ setCodexDeviceCode('');
143
+ setCodexDeviceStatus('idle');
144
+ setCodexDeviceError('');
79
145
  setStep('provider');
80
146
  };
81
147
  // Esc บนทุก step (ยกเว้น provider) = ย้อนกลับไปเลือก provider — กัน dead-end ตอนเลือกผิด
82
- // หรือ codex detect ค้าง (step codex-auth ตอน pending ไม่มีปุ่มอื่น แต่ Esc ออกได้เสมอ)
83
148
  useInput((_input, key) => {
84
- if (key.escape && step !== 'provider')
149
+ if (key.return && step === 'key' && !keyDraft.trim()) {
150
+ setKeyError(m.keyEmptyError);
151
+ return;
152
+ }
153
+ if (key.escape && step !== 'provider' && step !== 'language' && step !== 'codex-device-code')
85
154
  backToProvider();
86
155
  });
87
- // ตรวจ API key ในขั้นใส่ key — ว่าง = ไม่ผ่าน, OAuth/format ผิด = บอก error (กัน setup จบทั้งที่ key ใช้ไม่ได้)
88
156
  const submitKey = (raw) => {
89
157
  const k = raw.trim();
90
158
  if (!k) {
91
- setKeyError('วาง API key ก่อนค่ะ (กด Enter ทั้งที่ว่างไม่ได้) · Esc = กลับไปเลือก provider');
159
+ setKeyError(m.keyEmptyError);
92
160
  return;
93
161
  }
94
162
  if (cfg) {
95
163
  try {
96
- assertDirectApiKey(cfg, k); // reject OAuth/subscription token + format ผิด (เหมือน runtime)
164
+ assertDirectApiKey(cfg, k);
97
165
  }
98
166
  catch (e) {
99
167
  setKeyError(e.message.split('\n')[0]);
@@ -102,9 +170,16 @@ export function SetupWizard({ onComplete }) {
102
170
  }
103
171
  setKeyError('');
104
172
  setKey(k);
173
+ setKeyDraft(k);
105
174
  setStep('model');
106
175
  };
107
- return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["\u2699 \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 ", BRAND.bannerTitle, " (\u0E04\u0E23\u0E31\u0E49\u0E07\u0E41\u0E23\u0E01)"] }), step === 'provider' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "1. \u0E40\u0E25\u0E37\u0E2D\u0E01 AI provider (\u2191\u2193 \u0E40\u0E25\u0E37\u0E2D\u0E01 \u00B7 Enter \u0E22\u0E37\u0E19\u0E22\u0E31\u0E19):" }), _jsx(Text, { color: "gray", children: " cloud = \u0E43\u0E2A\u0E48 API key \u00B7 local = \u0E1F\u0E23\u0E35\u0E1A\u0E19\u0E40\u0E04\u0E23\u0E37\u0E48\u0E2D\u0E07 \u00B7 Codex = login \u0E14\u0E49\u0E27\u0E22 ChatGPT" }), _jsx(Select, { options: providerOptions, onChange: (v) => {
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) => {
108
183
  setProvider(v);
109
184
  const p = PROVIDERS[v];
110
185
  if (p.kind === 'delegate')
@@ -113,19 +188,70 @@ export function SetupWizard({ onComplete }) {
113
188
  setStep('key');
114
189
  else
115
190
  setStep('model');
116
- } })] })), step === 'codex-auth' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "2. \u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21 OpenAI Codex (\u0E43\u0E0A\u0E49\u0E42\u0E04\u0E27\u0E15\u0E49\u0E32 ChatGPT plan \u2014 \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E21\u0E35 API key):" }), codexStatus === null ? (_jsx(Text, { color: "gray", children: " \u0E01\u0E33\u0E25\u0E31\u0E07\u0E40\u0E0A\u0E47\u0E01 codex CLI + \u0E2A\u0E16\u0E32\u0E19\u0E30 login\u2026" })) : !codexStatus.installed ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: " \u274C \u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07 codex CLI" }), _jsxs(Text, { children: [' ', "\u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07\u0E43\u0E19 terminal \u0E2D\u0E35\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E15\u0E48\u0E32\u0E07: ", _jsx(Text, { color: "cyan", children: "npm i -g @openai/codex" })] }), _jsx(Select, { options: [
117
- { label: 'เช็กใหม่ (ติดตั้งเสร็จแล้ว)', value: 'recheck' },
118
- { label: '← กลับไปเลือก provider อื่น', value: 'back' },
119
- ], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : !codexStatus.loggedIn ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: " \u26A0 \u0E15\u0E34\u0E14\u0E15\u0E31\u0E49\u0E07\u0E41\u0E25\u0E49\u0E27 \u0E41\u0E15\u0E48\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49 login ChatGPT" }), _jsxs(Text, { children: [' ', "\u0E23\u0E31\u0E19\u0E43\u0E19 terminal \u0E2D\u0E35\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E15\u0E48\u0E32\u0E07: ", _jsx(Text, { color: "cyan", children: "codex login" }), " ", _jsx(Text, { color: "gray", children: "(\u0E40\u0E1B\u0E34\u0E14 browser \u0E43\u0E2B\u0E49\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E14\u0E49\u0E27\u0E22\u0E1A\u0E31\u0E0D\u0E0A\u0E35 ChatGPT)" })] }), _jsx(Select, { options: [
120
- { label: 'เช็กใหม่ (login เสร็จแล้ว)', value: 'recheck' },
121
- { label: '← กลับไปเลือก provider อื่น', value: 'back' },
122
- ], onChange: (v) => (v === 'recheck' ? setRecheck((n) => n + 1) : backToProvider()) })] })) : (_jsx(Text, { color: "green", children: " \u2705 login ChatGPT \u0E41\u0E25\u0E49\u0E27 \u2014 \u0E01\u0E33\u0E25\u0E31\u0E07\u0E44\u0E1B\u0E15\u0E48\u0E2D\u2026" }))] })), step === 'key' && cfg && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E27\u0E32\u0E07 API key \u0E02\u0E2D\u0E07 ", cfg.label, ": ", _jsx(Text, { color: "gray", children: "(Esc = \u0E01\u0E25\u0E31\u0E1A)" })] }), consoleUrl(provider) ? _jsxs(Text, { color: "cyan", children: [" \u2192 \u0E40\u0E2D\u0E32 key \u0E17\u0E35\u0E48: ", consoleUrl(provider)] }) : null, cfg.keyExample ? _jsxs(Text, { color: "gray", children: [" \u0E23\u0E39\u0E1B\u0E41\u0E1A\u0E1A key: ", cfg.keyExample] }) : null, _jsx(Text, { color: "gray", children: " (API key \u0E15\u0E23\u0E07\u0E08\u0E32\u0E01 console \u2014 \u0E2B\u0E49\u0E32\u0E21 OAuth/subscription token \u00B7 key \u0E08\u0E30\u0E40\u0E01\u0E47\u0E1A\u0E41\u0E1A\u0E1A\u0E40\u0E02\u0E49\u0E32\u0E23\u0E2B\u0E31\u0E2A\u0E43\u0E19\u0E40\u0E04\u0E23\u0E37\u0E48\u0E2D\u0E07)" }), _jsx(PasswordInput, { placeholder: cfg.envVar, onSubmit: submitKey }), keyError ? _jsxs(Text, { color: "red", children: [" \u2717 ", keyError] }) : null] })), step === 'model' &&
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) => {
218
+ setKeyDraft(v);
219
+ if (keyError)
220
+ setKeyError('');
221
+ }, onSubmit: submitKey }), keyError ? _jsxs(Text, { color: "red", children: [" \u2717 ", keyError] }) : null] })), step === 'model' &&
123
222
  cfg &&
124
- (loadingModels ? (_jsxs(Text, { color: "gray", children: [" \u0E01\u0E33\u0E25\u0E31\u0E07\u0E14\u0E36\u0E07\u0E23\u0E32\u0E22\u0E0A\u0E37\u0E48\u0E2D model \u0E08\u0E32\u0E01 ", cfg.label, "\u2026"] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["3. \u0E40\u0E25\u0E37\u0E2D\u0E01 model \u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19", remote.length ? _jsxs(Text, { color: "gray", children: [" (", modelOptions.length, " \u0E15\u0E31\u0E27\u0E08\u0E32\u0E01 provider + alias)"] }) : null, ":"] }), _jsx(Select, { options: modelOptions, onChange: (v) => {
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) => {
125
224
  setModel(`${provider}:${v}`);
126
- setStep('brain-offer');
127
- } })] }))), step === 'brain-offer' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "4. \u0E2A\u0E23\u0E49\u0E32\u0E07 \"second brain\" workspace (Obsidian) \u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A\u0E08\u0E31\u0E14\u0E40\u0E01\u0E47\u0E1A\u0E07\u0E32\u0E19 + \u0E04\u0E27\u0E32\u0E21\u0E08\u0E33 AI?" }), _jsx(Select, { options: [
128
- { label: 'สร้างเลย — ตอบไม่กี่ข้อ (ชื่อ + ที่เก็บ)', value: 'yes' },
129
- { label: `ข้ามไปก่อน (สั่ง ${BRAND.cliName} brain init ทีหลังได้)`, value: 'no' },
130
- ], onChange: (v) => finish(v === 'yes') })] }))] }));
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() })] }))] }));
131
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
+ }