sanook-cli 0.4.0 → 0.5.0
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 +19 -0
- package/CHANGELOG.md +144 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +394 -51
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +2 -2
- package/dist/providers/keys.js +3 -2
- package/dist/providers/registry.js +133 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +218 -27
- package/dist/ui/banner.js +4 -9
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/setup.js +6 -5
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/ui/app.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useMemo } from 'react';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
3
5
|
import { Box, Text, Static, useApp, useInput } from 'ink';
|
|
4
|
-
import { parseCommand } from '../commands.js';
|
|
6
|
+
import { BUILTIN_COMMANDS, expandCustomCommand, loadCustomCommands, parseCommand, parseSlashInvocation, } from '../commands.js';
|
|
5
7
|
import { runAgent } from '../loop.js';
|
|
6
8
|
import { saveSession, newSessionId } from '../session.js';
|
|
9
|
+
import { getBrainPath, appendBrainWorklog } from '../memory.js';
|
|
10
|
+
import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
|
|
11
|
+
import { makeSummarizer } from '../summarize.js';
|
|
12
|
+
import { agentTuning } from '../config.js';
|
|
13
|
+
import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
|
|
14
|
+
import { useEditor } from './useEditor.js';
|
|
15
|
+
import { loadHistory, appendHistory } from './history.js';
|
|
16
|
+
import { expandMentions } from './mentions.js';
|
|
17
|
+
import { BRAND } from '../brand.js';
|
|
7
18
|
import { Banner } from './banner.js';
|
|
8
|
-
|
|
19
|
+
const execFileP = promisify(execFile);
|
|
20
|
+
const PRE_TURN_COMPACT_TOKENS = 100_000; // session ยาวมากเท่านั้นถึง summarize ก่อน turn (mode summarize)
|
|
21
|
+
export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory }) {
|
|
9
22
|
const { exit } = useApp();
|
|
10
23
|
const [history, setHistory] = useState(initialHistory?.length
|
|
11
24
|
? [{ id: -1, role: 'system', text: `↻ ต่อจาก session ก่อน (${initialHistory.length} ข้อความ)` }]
|
|
12
25
|
: []);
|
|
13
|
-
const [input, setInput] = useState('');
|
|
14
26
|
const [streaming, setStreaming] = useState('');
|
|
15
27
|
const [busy, setBusy] = useState(false);
|
|
16
28
|
const [model, setModel] = useState(initialModel);
|
|
@@ -21,45 +33,148 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
|
|
|
21
33
|
const sessionId = useRef(newSessionId());
|
|
22
34
|
const sessionCreated = useRef(new Date().toISOString());
|
|
23
35
|
const approvalResolve = useRef(null);
|
|
36
|
+
const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
|
|
37
|
+
const checkpoints = useRef([]);
|
|
38
|
+
const editor = useEditor(replHistory.current);
|
|
39
|
+
// real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
|
|
40
|
+
const abortRef = useRef(null);
|
|
41
|
+
const queueRef = useRef([]);
|
|
42
|
+
const [queued, setQueued] = useState([]);
|
|
43
|
+
const enqueue = (msg) => {
|
|
44
|
+
queueRef.current.push(msg);
|
|
45
|
+
setQueued([...queueRef.current]);
|
|
46
|
+
};
|
|
47
|
+
const dequeue = () => {
|
|
48
|
+
const m = queueRef.current.shift();
|
|
49
|
+
setQueued([...queueRef.current]);
|
|
50
|
+
return m;
|
|
51
|
+
};
|
|
52
|
+
const clearQueue = () => {
|
|
53
|
+
queueRef.current = [];
|
|
54
|
+
setQueued([]);
|
|
55
|
+
};
|
|
24
56
|
const addTurn = (role, text) => setHistory((h) => [...h, { id: idRef.current++, role, text }]);
|
|
57
|
+
// /diff /undo — git-backed (execFile ไม่ผ่าน shell)
|
|
58
|
+
async function runGit(args, label) {
|
|
59
|
+
try {
|
|
60
|
+
const { stdout, stderr } = await execFileP('git', args, { cwd: process.cwd(), maxBuffer: 1_000_000 });
|
|
61
|
+
addTurn('system', (stdout || stderr).trim() || `(${label}: ไม่มีการเปลี่ยนแปลง)`);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
addTurn('system', `git ${label}: ${e.message.split('\n')[0]}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
25
67
|
// ask-mode: tool ขออนุมัติ → คืน Promise ที่ resolve เมื่อ user กด y/n
|
|
26
68
|
const requestApproval = (tool, summary) => new Promise((resolve) => {
|
|
27
69
|
approvalResolve.current = resolve;
|
|
28
70
|
setApprovalReq({ tool, summary });
|
|
29
71
|
});
|
|
30
|
-
useInput((
|
|
72
|
+
useInput((input, key) => {
|
|
31
73
|
// มี approval ค้าง → จับ y/n ก่อน (แม้ agent กำลังรัน/busy)
|
|
32
74
|
if (approvalReq) {
|
|
33
|
-
if (
|
|
75
|
+
if (input === 'y' || input === 'Y' || key.return) {
|
|
34
76
|
approvalResolve.current?.(true);
|
|
35
77
|
setApprovalReq(null);
|
|
36
78
|
}
|
|
37
|
-
else if (
|
|
79
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
38
80
|
approvalResolve.current?.(false);
|
|
39
81
|
setApprovalReq(null);
|
|
40
82
|
}
|
|
41
83
|
return;
|
|
42
84
|
}
|
|
43
|
-
if (busy)
|
|
85
|
+
if (busy) {
|
|
86
|
+
// steering ระหว่าง turn: Esc / Ctrl+C = หยุด turn นี้ (ไม่ออกจากแอป) + ล้างคิว
|
|
87
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
88
|
+
abortRef.current?.abort();
|
|
89
|
+
clearQueue();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// พิมพ์ระหว่าง busy ได้ — Enter = ต่อคิว (รันอัตโนมัติหลัง turn นี้จบ)
|
|
93
|
+
const a = editor.handleKey(input, key);
|
|
94
|
+
if (a === 'submit') {
|
|
95
|
+
const v = editor.value.trim();
|
|
96
|
+
editor.reset();
|
|
97
|
+
if (v)
|
|
98
|
+
enqueue(v);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const action = editor.handleKey(input, key);
|
|
103
|
+
if (action === 'submit')
|
|
104
|
+
void submit(editor.value);
|
|
105
|
+
else if (action === 'interrupt') {
|
|
106
|
+
if (editor.value)
|
|
107
|
+
editor.reset(); // Ctrl+C ครั้งแรก = ล้างบรรทัด, ว่างแล้ว = ออก
|
|
108
|
+
else
|
|
109
|
+
exit();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
/** ย้อน 1 turn — คืนไฟล์ (git, recoverable) + ตัดบทสนทนากลับ */
|
|
113
|
+
async function rewind() {
|
|
114
|
+
const cp = checkpoints.current.pop();
|
|
115
|
+
if (!cp) {
|
|
116
|
+
addTurn('system', 'ไม่มี checkpoint ให้ย้อน');
|
|
44
117
|
return;
|
|
45
|
-
if (key.return) {
|
|
46
|
-
void submit();
|
|
47
118
|
}
|
|
48
|
-
|
|
49
|
-
|
|
119
|
+
let note = '';
|
|
120
|
+
if (cp.ref) {
|
|
121
|
+
const r = await restoreWorkTree(cp.ref);
|
|
122
|
+
note = r.ok
|
|
123
|
+
? r.recovery
|
|
124
|
+
? ` · ไฟล์คืนแล้ว (กู้สถานะก่อนหน้า: ${r.recovery})`
|
|
125
|
+
: ' · ไฟล์คืนแล้ว'
|
|
126
|
+
: ` · ไฟล์: ${r.reason}`;
|
|
50
127
|
}
|
|
51
|
-
|
|
52
|
-
|
|
128
|
+
msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
|
|
129
|
+
setHistory((h) => h.filter((t) => t.id < cp.turnId));
|
|
130
|
+
addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
|
|
131
|
+
}
|
|
132
|
+
/** บีบ context: 'summarize' (ใช้ model ถูกย่อ) ถ้าตั้งไว้ ไม่งั้น 'truncate' (zero-LLM) */
|
|
133
|
+
async function compactHistory(targetTokens, label) {
|
|
134
|
+
const before = estimateTokens(msgsRef.current);
|
|
135
|
+
if (before <= targetTokens) {
|
|
136
|
+
addTurn('system', `context ~${before} tokens — ยังไม่ต้องบีบ`);
|
|
137
|
+
return;
|
|
53
138
|
}
|
|
54
|
-
|
|
55
|
-
|
|
139
|
+
const tuning = await agentTuning().catch(() => null);
|
|
140
|
+
if (tuning?.compaction === 'summarize') {
|
|
141
|
+
addTurn('system', '⏳ กำลังย่อ context ด้วย model ถูก…');
|
|
142
|
+
msgsRef.current = await summarizeCompact(msgsRef.current, targetTokens, makeSummarizer(model, tuning.summaryModel), 20).catch(() => autoCompact(msgsRef.current, targetTokens, 20));
|
|
143
|
+
addTurn('system', `ย่อ context แล้ว (summarize): ~${before} → ~${estimateTokens(msgsRef.current)} tokens`);
|
|
56
144
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
145
|
+
else {
|
|
146
|
+
msgsRef.current = autoCompact(msgsRef.current, targetTokens, 20);
|
|
147
|
+
addTurn('system', `บีบ context แล้ว: ~${before} → ~${estimateTokens(msgsRef.current)} tokens`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function submit(raw) {
|
|
151
|
+
const text = raw.trim();
|
|
152
|
+
editor.reset();
|
|
60
153
|
if (!text)
|
|
61
154
|
return;
|
|
62
|
-
|
|
155
|
+
appendHistory(text, replHistory.current[replHistory.current.length - 1]);
|
|
156
|
+
replHistory.current.push(text);
|
|
157
|
+
const slash = parseSlashInvocation(text);
|
|
158
|
+
if (slash) {
|
|
159
|
+
if (slash.name === 'rewind') {
|
|
160
|
+
await rewind();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!BUILTIN_COMMANDS.has(slash.name)) {
|
|
164
|
+
const custom = (await loadCustomCommands()).get(slash.name);
|
|
165
|
+
if (custom) {
|
|
166
|
+
const expanded = expandCustomCommand(custom, slash.args);
|
|
167
|
+
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
168
|
+
addTurn('user', text);
|
|
169
|
+
if (!expanded.trim()) {
|
|
170
|
+
addTurn('system', `custom command /${slash.name} ว่าง`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await runAssistantTurn(expanded, [], mark);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
63
178
|
const cmd = parseCommand(text, { model, costSummary: lastCost.current });
|
|
64
179
|
if (cmd.handled) {
|
|
65
180
|
addTurn('user', text);
|
|
@@ -67,26 +182,62 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
|
|
|
67
182
|
return exit();
|
|
68
183
|
if (cmd.action === 'clear') {
|
|
69
184
|
msgsRef.current = [];
|
|
185
|
+
checkpoints.current = [];
|
|
70
186
|
return setHistory([]);
|
|
71
187
|
}
|
|
188
|
+
if (cmd.action === 'compact') {
|
|
189
|
+
void compactHistory(40_000, 'บีบ context');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (cmd.action === 'diff')
|
|
193
|
+
return void runGit(['diff', '--stat'], 'diff');
|
|
194
|
+
if (cmd.action === 'undo') {
|
|
195
|
+
void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
72
198
|
if (cmd.modelChange)
|
|
73
199
|
setModel(cmd.modelChange);
|
|
74
200
|
if (cmd.message)
|
|
75
201
|
addTurn('system', cmd.message);
|
|
76
202
|
return;
|
|
77
203
|
}
|
|
204
|
+
// prompt ปกติ → expand @mentions (inline ไฟล์ text + เก็บ path รูป)
|
|
205
|
+
const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
|
|
78
206
|
addTurn('user', text);
|
|
207
|
+
const { text: expanded, images, errors } = await expandMentions(text);
|
|
208
|
+
if (errors.length)
|
|
209
|
+
addTurn('system', `@mention: ${errors.join(' · ')}`);
|
|
210
|
+
await runAssistantTurn(expanded, images, mark);
|
|
211
|
+
}
|
|
212
|
+
async function runAssistantTurn(promptText, images, mark) {
|
|
213
|
+
// proactive summarize-compaction สำหรับ session ยาวมาก (เฉพาะ mode summarize) — เริ่ม turn ให้ context lean
|
|
214
|
+
// (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
|
|
215
|
+
if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
|
|
216
|
+
const t = await agentTuning().catch(() => null);
|
|
217
|
+
if (t?.compaction === 'summarize') {
|
|
218
|
+
addTurn('system', '⏳ context ยาว — ย่ออัตโนมัติก่อนรอบนี้…');
|
|
219
|
+
msgsRef.current = await summarizeCompact(msgsRef.current, PRE_TURN_COMPACT_TOKENS, makeSummarizer(model, t.summaryModel), 20).catch(() => msgsRef.current);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// checkpoint สถานะก่อนรัน (ไฟล์ git + ขอบเขตบทสนทนา) → /rewind ย้อนได้
|
|
223
|
+
const ref = await snapshotWorkTree();
|
|
224
|
+
checkpoints.current.push({ ref, turnId: mark.turnId, msgLen: mark.msgLen });
|
|
225
|
+
const ac = new AbortController(); // steering: ให้ Esc/Ctrl+C หยุด stream กลางทางได้
|
|
226
|
+
abortRef.current = ac;
|
|
79
227
|
setBusy(true);
|
|
80
228
|
let buf = '';
|
|
81
229
|
let lastFlush = 0;
|
|
82
230
|
try {
|
|
83
|
-
const { cost, messages } = await runAgent({
|
|
231
|
+
const { cost, messages, text } = await runAgent({
|
|
84
232
|
model,
|
|
85
|
-
|
|
233
|
+
fallbackModel,
|
|
234
|
+
prompt: promptText,
|
|
235
|
+
images: images.length ? images : undefined,
|
|
86
236
|
history: msgsRef.current,
|
|
87
237
|
budgetUsd,
|
|
88
238
|
permissionMode,
|
|
89
239
|
approve: requestApproval,
|
|
240
|
+
signal: ac.signal,
|
|
90
241
|
onEvent: (e) => {
|
|
91
242
|
if (e.type === 'text') {
|
|
92
243
|
buf += e.text ?? '';
|
|
@@ -104,8 +255,8 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
|
|
|
104
255
|
});
|
|
105
256
|
msgsRef.current = messages;
|
|
106
257
|
lastCost.current = cost.summary();
|
|
107
|
-
addTurn('assistant', buf.trim());
|
|
108
|
-
// เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
|
|
258
|
+
addTurn('assistant', buf.trim() || text.trim());
|
|
259
|
+
// เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
|
|
109
260
|
void saveSession({
|
|
110
261
|
id: sessionId.current,
|
|
111
262
|
created: sessionCreated.current,
|
|
@@ -114,19 +265,59 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
|
|
|
114
265
|
cwd: process.cwd(),
|
|
115
266
|
messages,
|
|
116
267
|
});
|
|
268
|
+
// worklog เข้า second-brain — vault จำว่าทำอะไรใน session นี้
|
|
269
|
+
void (async () => {
|
|
270
|
+
const brain = await getBrainPath();
|
|
271
|
+
if (brain) {
|
|
272
|
+
await appendBrainWorklog(brain, {
|
|
273
|
+
prompt: promptText,
|
|
274
|
+
summary: cost.summary(),
|
|
275
|
+
model,
|
|
276
|
+
today: new Date().toISOString().slice(0, 10),
|
|
277
|
+
}).catch(() => { });
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
117
280
|
}
|
|
118
281
|
catch (err) {
|
|
119
|
-
|
|
282
|
+
if (ac.signal.aborted) {
|
|
283
|
+
// หยุดเอง — เก็บ partial output ไว้ดู, ทิ้ง turn นี้ออกจาก LLM history (msgsRef ไม่อัปเดต)
|
|
284
|
+
if (buf.trim())
|
|
285
|
+
addTurn('assistant', buf.trim());
|
|
286
|
+
addTurn('system', '⊘ หยุด turn แล้ว (ไฟล์ที่ tool แก้ไปแล้วคืนด้วย /rewind ได้)');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
addTurn('system', `ERROR: ${err.message}`);
|
|
290
|
+
}
|
|
120
291
|
}
|
|
121
292
|
finally {
|
|
122
293
|
setStreaming('');
|
|
123
294
|
setBusy(false);
|
|
295
|
+
abortRef.current = null;
|
|
124
296
|
}
|
|
297
|
+
// steering: ข้อความที่พิมพ์ค้างคิวระหว่าง turn → รันต่อทันที (ถ้าไม่ได้ถูกหยุด)
|
|
298
|
+
const next = ac.signal.aborted ? undefined : dequeue();
|
|
299
|
+
if (next)
|
|
300
|
+
void submit(next);
|
|
125
301
|
}
|
|
126
302
|
const banner = useMemo(() => _jsx(Banner, { model: initialModel }), [initialModel]);
|
|
127
|
-
|
|
303
|
+
const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
|
|
304
|
+
return (_jsxs(Box, { flexDirection: "column", children: [banner, _jsx(Static, { items: history, children: (turn) => _jsx(TurnView, { turn: turn }, turn.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: streaming }) })) : null, queued.length ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: queued.map((q, i) => (_jsxs(Text, { dimColor: true, children: ["\u23F3 \u0E04\u0E34\u0E27 ", i + 1, ": ", q.length > 64 ? `${q.slice(0, 64)}…` : q] }, i))) })) : 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 })] })), _jsxs(Text, { dimColor: true, children: [' ', model, " \u00B7 ", permissionMode === 'ask' ? 'ask-mode' : 'auto', " \u00B7 /help \u00B7 @file \u00B7 \u2191 history", costHint ? ` · ${costHint}` : ''] })] }));
|
|
305
|
+
}
|
|
306
|
+
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
307
|
+
function InputView({ value, cursor, busy }) {
|
|
308
|
+
if (busy && !value)
|
|
309
|
+
return _jsx(Text, { dimColor: true, children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E17\u0E33\u0E07\u0E32\u0E19\u2026 Esc/Ctrl+C \u0E2B\u0E22\u0E38\u0E14 \u00B7 \u0E1E\u0E34\u0E21\u0E1E\u0E4C\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E15\u0E48\u0E2D\u0E04\u0E34\u0E27 (\u23CE)" });
|
|
310
|
+
if (!busy && !value)
|
|
311
|
+
return _jsx(Text, { dimColor: true, children: "\u0E16\u0E32\u0E21\u0E2D\u0E30\u0E44\u0E23\u0E01\u0E47\u0E44\u0E14\u0E49 \u2014 /help \u0E14\u0E39\u0E04\u0E33\u0E2A\u0E31\u0E48\u0E07 \u00B7 /tools \u0E14\u0E39 tools \u00B7 @\u0E44\u0E1F\u0E25\u0E4C \u0E41\u0E19\u0E1A context/\u0E23\u0E39\u0E1B" });
|
|
312
|
+
const before = value.slice(0, cursor);
|
|
313
|
+
const at = value.slice(cursor, cursor + 1) || ' ';
|
|
314
|
+
const after = value.slice(cursor + 1);
|
|
315
|
+
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] }));
|
|
128
316
|
}
|
|
129
317
|
function TurnView({ turn }) {
|
|
130
|
-
|
|
131
|
-
|
|
318
|
+
if (turn.role === 'system')
|
|
319
|
+
return _jsx(Text, { dimColor: true, children: turn.text });
|
|
320
|
+
if (turn.role === 'user')
|
|
321
|
+
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { color: "cyan", children: turn.text })] }));
|
|
322
|
+
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: turn.text }) }));
|
|
132
323
|
}
|
package/dist/ui/banner.js
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
3
|
import Gradient from 'ink-gradient';
|
|
4
|
-
import BigText from 'ink-big-text';
|
|
5
4
|
import { homedir } from 'node:os';
|
|
6
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { BRAND } from '../brand.js';
|
|
7
7
|
// gradient ของ Sanook: เขียว → ส้ม → ฟ้า (สนุก = สดใส)
|
|
8
8
|
const SANOOK_GRADIENT = ['#22C55E', '#F97316', '#38BDF8'];
|
|
9
9
|
// version จาก package.json (single source of truth) — กัน default drift เหมือน bin.ts
|
|
10
10
|
const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
11
|
-
/** welcome banner —
|
|
11
|
+
/** welcome banner — minimal: gradient wordmark + meta บรรทัดเดียว (terminal-first, ไม่รก) */
|
|
12
12
|
export function Banner({ model, version = VERSION, account = 'BYOK', cwd }) {
|
|
13
|
-
const { stdout } = useStdout();
|
|
14
|
-
const columns = stdout?.columns ?? 80;
|
|
15
13
|
const dir = (cwd ?? process.cwd()).replace(homedir(), '~');
|
|
16
|
-
|
|
17
|
-
const bigText = columns >= 92 ? 'Sanook AI' : 'Sanook';
|
|
18
|
-
const font = columns >= 48 ? 'block' : 'tiny';
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Gradient, { colors: SANOOK_GRADIENT, children: _jsx(BigText, { text: bigText, font: font }) }), _jsxs(Box, { marginTop: -1, marginLeft: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Sanook AI CLI" }), _jsxs(Text, { color: "gray", children: [" v", version, " \u00B7 terminal coding agent \u00B7 BYOK"] })] }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "green", children: "\u25CF" }), " ", model, ' ', "account: ", account, ' ', "cwd: ", dir] })] })] }));
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Gradient, { colors: SANOOK_GRADIENT, children: _jsx(Text, { bold: true, children: BRAND.cliName }) }), _jsxs(Text, { dimColor: true, children: [" v", version, " \u00B7 terminal coding agent \u00B7 ", account] })] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "green", children: "\u25CF" }), " ", model, " \u00B7 ", dir] })] }));
|
|
20
15
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { appHomePath, persistenceEnabled } from '../brand.js';
|
|
3
|
+
// prompt history แบบ persist ข้าม session (เลียน shell history) — เก็บที่ ~/.sanook/history
|
|
4
|
+
const HISTORY_PATH = appHomePath('history');
|
|
5
|
+
const MAX_ENTRIES = 500;
|
|
6
|
+
/** โหลด prompt เก่า (เก่า→ใหม่) สำหรับ Up/Down navigation ใน REPL */
|
|
7
|
+
export function loadHistory() {
|
|
8
|
+
try {
|
|
9
|
+
const lines = readFileSync(HISTORY_PATH, 'utf8').split('\n').filter(Boolean);
|
|
10
|
+
return lines.slice(-MAX_ENTRIES);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** append 1 prompt (ข้ามถ้าซ้ำกับอันล่าสุด / เป็น slash command / ว่าง) */
|
|
17
|
+
export function appendHistory(prompt, last) {
|
|
18
|
+
if (!persistenceEnabled())
|
|
19
|
+
return;
|
|
20
|
+
const p = prompt.trim();
|
|
21
|
+
if (!p || p === last)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
mkdirSync(appHomePath(), { recursive: true });
|
|
25
|
+
appendFileSync(HISTORY_PATH, `${p.replace(/\n/g, ' ')}\n`, { mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* เขียนไม่ได้ = ไม่เป็นไร (history เป็น nice-to-have) */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { resolve, extname } from 'node:path';
|
|
3
|
+
import { checkReadPath } from '../tools/permission.js';
|
|
4
|
+
// @-file mentions: "@path" ใน prompt → inline เนื้อหาไฟล์ (text) หรือแนบเป็น image (รูป)
|
|
5
|
+
// ลด tool round-trip (agent ไม่ต้อง read_file เอง) + เปิดทาง vision input
|
|
6
|
+
const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
|
|
7
|
+
const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
|
|
8
|
+
const MAX_INLINE = 60_000;
|
|
9
|
+
/** แตก @mention ใน prompt: text file → inline, image → คืน path ไปแนบ, ที่เหลือคงไว้ */
|
|
10
|
+
export async function expandMentions(input) {
|
|
11
|
+
const mentions = [...input.matchAll(MENTION_RE)].map((m) => m[1]);
|
|
12
|
+
if (!mentions.length)
|
|
13
|
+
return { text: input, images: [], errors: [] };
|
|
14
|
+
const images = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const inlined = [];
|
|
17
|
+
for (const rel of [...new Set(mentions)]) {
|
|
18
|
+
const abs = resolve(rel);
|
|
19
|
+
// canonicalize ก่อนเช็ก extension → symlink ที่ชื่อไม่มีนามสกุลแต่ชี้ไปรูป ก็จับเป็น image ถูก
|
|
20
|
+
const real = await realpath(abs).catch(() => abs);
|
|
21
|
+
if (IMAGE_EXT.has(extname(real).toLowerCase())) {
|
|
22
|
+
const guard = await checkReadPath(real);
|
|
23
|
+
if (guard.ok)
|
|
24
|
+
images.push(real);
|
|
25
|
+
else
|
|
26
|
+
errors.push(`@${rel} (${guard.reason})`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const guard = await checkReadPath(real);
|
|
30
|
+
if (!guard.ok) {
|
|
31
|
+
errors.push(`@${rel} (${guard.reason})`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const content = (await readFile(real, 'utf8')).slice(0, MAX_INLINE);
|
|
36
|
+
inlined.push(`<file path="${rel}">\n${content}\n</file>`);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
errors.push(`@${rel} (${e.message})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const text = inlined.length ? `${input}\n\n${inlined.join('\n\n')}` : input;
|
|
43
|
+
return { text, images, errors };
|
|
44
|
+
}
|
package/dist/ui/setup.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { Select, PasswordInput } from '@inkjs/ui';
|
|
5
|
-
import { PROVIDERS } from '../providers/registry.js';
|
|
5
|
+
import { PROVIDERS, consoleUrl } from '../providers/registry.js';
|
|
6
6
|
import { listRemoteModels, mergeModelOptions } from '../providers/models.js';
|
|
7
|
+
import { BRAND } from '../brand.js';
|
|
7
8
|
/** first-run setup wizard: เลือก provider → ใส่ API key → เลือก model → เสนอสร้าง second-brain */
|
|
8
9
|
export function SetupWizard({ onComplete }) {
|
|
9
10
|
const [step, setStep] = useState('provider');
|
|
@@ -28,10 +29,10 @@ export function SetupWizard({ onComplete }) {
|
|
|
28
29
|
}, [step, cfg, key]);
|
|
29
30
|
const modelOptions = cfg ? mergeModelOptions(cfg, remote) : [];
|
|
30
31
|
const finish = (createBrain) => onComplete({ provider, model, envVar: cfg?.envVar ?? '', key, createBrain });
|
|
31
|
-
return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [
|
|
32
|
+
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:" }), _jsx(Select, { options: providerOptions, onChange: (v) => {
|
|
32
33
|
setProvider(v);
|
|
33
34
|
setStep(PROVIDERS[v].requiresKey ? 'key' : 'model');
|
|
34
|
-
} })] })), 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: " (key \u0E15\u0E23\u0E07\u0E08\u0E32\u0E01 console \
|
|
35
|
+
} })] })), step === 'key' && cfg && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E27\u0E32\u0E07 API key \u0E02\u0E2D\u0E07 ", cfg.label, ":"] }), consoleUrl(provider) ? (_jsxs(Text, { color: "cyan", children: [" \u2192 \u0E40\u0E2D\u0E32 key \u0E17\u0E35\u0E48: ", consoleUrl(provider)] })) : null, _jsx(Text, { color: "gray", children: " (API key \u0E15\u0E23\u0E07\u0E08\u0E32\u0E01 console \u2014 \u0E2B\u0E49\u0E32\u0E21 OAuth/subscription token)" }), _jsx(PasswordInput, { placeholder: cfg.envVar, onSubmit: (v) => {
|
|
35
36
|
setKey(v.trim());
|
|
36
37
|
setStep('model');
|
|
37
38
|
} })] })), step === 'model' &&
|
|
@@ -41,6 +42,6 @@ export function SetupWizard({ onComplete }) {
|
|
|
41
42
|
setStep('brain-offer');
|
|
42
43
|
} })] }))), 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: [
|
|
43
44
|
{ label: 'สร้างเลย — ตอบไม่กี่ข้อ (ชื่อ + ที่เก็บ)', value: 'yes' },
|
|
44
|
-
{ label:
|
|
45
|
+
{ label: `ข้ามไปก่อน (สั่ง ${BRAND.cliName} brain init ทีหลังได้)`, value: 'no' },
|
|
45
46
|
], onChange: (v) => finish(v === 'yes') })] }))] }));
|
|
46
47
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
export function useEditor(history) {
|
|
3
|
+
const [value, setValue] = useState('');
|
|
4
|
+
const [cursor, setCursor] = useState(0);
|
|
5
|
+
const histIndex = useRef(null); // null = กำลังแก้ draft (ไม่ได้อยู่ในประวัติ)
|
|
6
|
+
const draft = useRef('');
|
|
7
|
+
const set = (v, c = v.length) => {
|
|
8
|
+
setValue(v);
|
|
9
|
+
setCursor(Math.max(0, Math.min(c, v.length)));
|
|
10
|
+
};
|
|
11
|
+
const reset = () => {
|
|
12
|
+
histIndex.current = null;
|
|
13
|
+
set('');
|
|
14
|
+
};
|
|
15
|
+
const insert = (s) => set(value.slice(0, cursor) + s + value.slice(cursor), cursor + s.length);
|
|
16
|
+
const historyPrev = () => {
|
|
17
|
+
if (!history.length)
|
|
18
|
+
return;
|
|
19
|
+
if (histIndex.current === null) {
|
|
20
|
+
draft.current = value;
|
|
21
|
+
histIndex.current = history.length - 1;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
histIndex.current = Math.max(0, histIndex.current - 1);
|
|
25
|
+
}
|
|
26
|
+
set(history[histIndex.current]);
|
|
27
|
+
};
|
|
28
|
+
const historyNext = () => {
|
|
29
|
+
if (histIndex.current === null)
|
|
30
|
+
return;
|
|
31
|
+
if (histIndex.current >= history.length - 1) {
|
|
32
|
+
histIndex.current = null;
|
|
33
|
+
set(draft.current);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
histIndex.current += 1;
|
|
37
|
+
set(history[histIndex.current]);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const handleKey = (input, key) => {
|
|
41
|
+
if (key.return) {
|
|
42
|
+
// Alt/Option+Enter หรือบรรทัดลงท้าย "\" → ขึ้นบรรทัดใหม่ (multiline) ไม่ submit
|
|
43
|
+
if (key.meta)
|
|
44
|
+
return insert('\n'), 'handled';
|
|
45
|
+
if (value.slice(0, cursor).endsWith('\\'))
|
|
46
|
+
return set(value.slice(0, cursor - 1) + '\n' + value.slice(cursor), cursor), 'handled';
|
|
47
|
+
return 'submit';
|
|
48
|
+
}
|
|
49
|
+
if (key.upArrow)
|
|
50
|
+
return historyPrev(), 'handled';
|
|
51
|
+
if (key.downArrow)
|
|
52
|
+
return historyNext(), 'handled';
|
|
53
|
+
if (key.leftArrow)
|
|
54
|
+
return setCursor(Math.max(0, cursor - 1)), 'handled';
|
|
55
|
+
if (key.rightArrow)
|
|
56
|
+
return setCursor(Math.min(value.length, cursor + 1)), 'handled';
|
|
57
|
+
if (key.ctrl) {
|
|
58
|
+
switch (input) {
|
|
59
|
+
case 'a': return setCursor(0), 'handled';
|
|
60
|
+
case 'e': return setCursor(value.length), 'handled';
|
|
61
|
+
case 'u': return set(value.slice(cursor), 0), 'handled'; // ลบจากต้นบรรทัดถึง cursor
|
|
62
|
+
case 'k': return set(value.slice(0, cursor), cursor), 'handled'; // ลบจาก cursor ถึงท้าย
|
|
63
|
+
case 'w': { // ลบ word ก่อน cursor (รวมกรณีเหลือแต่ whitespace)
|
|
64
|
+
const left = value.slice(0, cursor).replace(/\s+$|\s*\S+\s*$/, '');
|
|
65
|
+
return set(left + value.slice(cursor), left.length), 'handled';
|
|
66
|
+
}
|
|
67
|
+
case 'c': return 'interrupt';
|
|
68
|
+
default: return 'handled';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (key.backspace || key.delete) {
|
|
72
|
+
if (cursor === 0)
|
|
73
|
+
return 'handled';
|
|
74
|
+
return set(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1), 'handled';
|
|
75
|
+
}
|
|
76
|
+
if (input && !key.meta) {
|
|
77
|
+
histIndex.current = null; // เริ่มพิมพ์ = ออกจากโหมดดูประวัติ
|
|
78
|
+
return insert(input), 'handled';
|
|
79
|
+
}
|
|
80
|
+
return 'none';
|
|
81
|
+
};
|
|
82
|
+
return { value, cursor, setValue: (v) => set(v), reset, handleKey };
|
|
83
|
+
}
|