sanook-cli 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +161 -3
- package/CHANGELOG.md +57 -8
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3026 -196
- package/dist/brain-context.js +223 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +371 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +12 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +152 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/commands.js +172 -13
- package/dist/compaction.js +96 -11
- package/dist/config.js +118 -28
- package/dist/context-compression.js +191 -0
- package/dist/cost.js +49 -15
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +37 -8
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +357 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/email.js +472 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +18 -0
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +343 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/insights-args.js +35 -0
- package/dist/insights.js +86 -0
- package/dist/loop.js +123 -24
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-registry.js +350 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +44 -6
- package/dist/memory.js +100 -33
- package/dist/orchestrate.js +49 -19
- package/dist/personality.js +58 -0
- package/dist/providers/codex.js +70 -36
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +75 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session.js +93 -7
- package/dist/skill-install.js +29 -12
- package/dist/support-dump.js +175 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +923 -9
- package/dist/tools/read.js +16 -4
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +217 -13
- package/dist/tools/task.js +18 -7
- package/dist/tools/timeout.js +21 -3
- package/dist/trust.js +11 -1
- package/dist/ui/app.js +48 -8
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/setup.js +17 -4
- package/dist/update.js +24 -11
- package/dist/worktree.js +175 -4
- package/package.json +4 -4
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +3 -1
- package/second-brain/Projects/sanook-cli/_Index.md +26 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/_Index.md +6 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +22 -3
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/tools/read.js
CHANGED
|
@@ -3,6 +3,17 @@ import { z } from 'zod';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { clamp, resolveAgentPath } from './util.js';
|
|
5
5
|
import { checkReadPath } from './permission.js';
|
|
6
|
+
function splitReadableLines(content) {
|
|
7
|
+
if (!content)
|
|
8
|
+
return [];
|
|
9
|
+
const lines = content.split(/\r\n|\n|\r/);
|
|
10
|
+
if (/(\r\n|\n|\r)$/.test(content))
|
|
11
|
+
lines.pop();
|
|
12
|
+
return lines;
|
|
13
|
+
}
|
|
14
|
+
function normalizeReadableLineEndings(content) {
|
|
15
|
+
return content.replace(/\r\n|\r/g, '\n');
|
|
16
|
+
}
|
|
6
17
|
export const readFileTool = tool({
|
|
7
18
|
description: 'อ่านไฟล์ใน workspace (UTF-8). อ่านก่อนแก้ไฟล์เสมอ. ' +
|
|
8
19
|
'ไฟล์ใหญ่หรือต้องการแค่บางส่วน → ใส่ offset/limit อ่านเฉพาะช่วงบรรทัด (ประหยัด token มาก — คู่กับ grep ที่ให้เลขบรรทัด)',
|
|
@@ -20,12 +31,13 @@ export const readFileTool = tool({
|
|
|
20
31
|
const content = await readFile(full, 'utf8');
|
|
21
32
|
// ไม่ระบุช่วง → คืนทั้งไฟล์ (clamp) เหมือนเดิม
|
|
22
33
|
if (offset == null && limit == null)
|
|
23
|
-
return clamp(content);
|
|
34
|
+
return clamp(normalizeReadableLineEndings(content));
|
|
24
35
|
// ระบุช่วง → อ่านเฉพาะบรรทัด start..end (ส่งเฉพาะที่ต้องการเข้า context, ประหยัด token)
|
|
25
|
-
const lines = content
|
|
26
|
-
const
|
|
36
|
+
const lines = splitReadableLines(content);
|
|
37
|
+
const requestedOffset = offset ?? 1;
|
|
38
|
+
const start = Math.max(0, requestedOffset - 1);
|
|
27
39
|
if (start >= lines.length)
|
|
28
|
-
return `(ไฟล์มี ${lines.length} บรรทัด — offset ${
|
|
40
|
+
return `(ไฟล์มี ${lines.length} บรรทัด — offset ${requestedOffset} เกินช่วง)`;
|
|
29
41
|
const end = limit == null ? lines.length : Math.min(lines.length, start + limit);
|
|
30
42
|
const slice = lines.slice(start, end).join('\n');
|
|
31
43
|
return clamp(`[บรรทัด ${start + 1}-${end} จาก ${lines.length}]\n${slice}`);
|
package/dist/tools/schedule.js
CHANGED
|
@@ -2,6 +2,7 @@ import { tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { parseSchedule } from '../gateway/schedule.js';
|
|
4
4
|
import { enqueueTask, listTasks, removeTask } from '../gateway/ledger.js';
|
|
5
|
+
import { formatTarget, parseSendTarget } from '../gateway/targets.js';
|
|
5
6
|
/** ตั้งงานตามเวลา — agent เรียกเองเมื่อ user พูดเรื่องเวลา/รอบ ("ทุกๆ X โมง/นาที") */
|
|
6
7
|
export const scheduleTaskTool = tool({
|
|
7
8
|
description: 'ตั้งงานให้ทำตามเวลา/เป็นรอบ — เรียกเมื่อ user ขอให้ทำอะไร "ทุกๆ X" หรือ "ตอน X โมง" หรือเวลาในอนาคต. ' +
|
|
@@ -13,21 +14,36 @@ export const scheduleTaskTool = tool({
|
|
|
13
14
|
when: z.string().describe('เวลา: every 30m / 09:00 / ISO / "ทุก 2 ชั่วโมง"'),
|
|
14
15
|
task: z.string().describe('สิ่งที่จะให้ทำตอนถึงเวลา — เขียนเป็น prompt เต็มในตัวเอง (รันเป็น fresh agent ไม่มี context นี้)'),
|
|
15
16
|
model: z.string().optional().describe('model spec (ไม่ใส่ = default ของ gateway)'),
|
|
17
|
+
deliver: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('ปลายทางส่งผลลัพธ์ เช่น telegram, telegram:123, discord:channel, slack:C01, mattermost:channel, homeassistant:notification_id, email:owner@example.com, line:U123, sms:+15551234567, ntfy:topic, signal:+15551234567, whatsapp:15551234567, matrix:!room:server, googlechat:spaces/AAA, bluebubbles:user@example.com, teams'),
|
|
16
21
|
}),
|
|
17
|
-
execute: async ({ when, task, model }) => {
|
|
22
|
+
execute: async ({ when, task, model, deliver }) => {
|
|
18
23
|
const sched = parseSchedule(when, Date.now());
|
|
19
24
|
if (!sched) {
|
|
20
25
|
return `ตั้งเวลาไม่ได้: "${when}" ไม่ใช่รูปแบบที่รองรับ — ลอง "every 30m", "09:00", ISO, หรือ "ทุก 2 ชั่วโมง"`;
|
|
21
26
|
}
|
|
27
|
+
let normalizedDeliver;
|
|
28
|
+
if (deliver?.trim()) {
|
|
29
|
+
try {
|
|
30
|
+
normalizedDeliver = formatTarget(parseSendTarget(deliver));
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return `ตั้งปลายทางส่งผลลัพธ์ไม่ได้: ${e.message}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
const t = await enqueueTask({
|
|
23
37
|
kind: sched.recurring ? 'cron' : 'once',
|
|
24
38
|
spec: task,
|
|
25
39
|
schedule: sched.recurring ? sched.normalized : undefined,
|
|
26
40
|
model,
|
|
41
|
+
deliver: normalizedDeliver,
|
|
27
42
|
runAt: sched.runAt,
|
|
28
43
|
});
|
|
29
44
|
const at = new Date(t.runAt).toLocaleString();
|
|
30
|
-
return (`ตั้งงาน ${t.id} แล้ว — รัน ${at}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ' (ครั้งเดียว)'}
|
|
45
|
+
return (`ตั้งงาน ${t.id} แล้ว — รัน ${at}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ' (ครั้งเดียว)'}` +
|
|
46
|
+
`${normalizedDeliver ? ` และส่งผลลัพธ์ไป ${normalizedDeliver}` : ''}. ` +
|
|
31
47
|
`งานจะทำงานเมื่อ gateway เปิดอยู่ (sanook serve)`);
|
|
32
48
|
},
|
|
33
49
|
});
|
|
@@ -44,7 +60,7 @@ export const listScheduledTool = tool({
|
|
|
44
60
|
if (!tasks.length)
|
|
45
61
|
return filter ? `ไม่มีงานสถานะ ${filter}` : 'ยังไม่มีงานที่ตั้งเวลาไว้';
|
|
46
62
|
return tasks
|
|
47
|
-
.map((t) => `${t.id} [${t.status}] ${t.schedule ?? 'once'} → ${t.spec.slice(0, 60)} (next ${new Date(t.runAt).toLocaleString()})`)
|
|
63
|
+
.map((t) => `${t.id} [${t.status}] ${t.schedule ?? 'once'}${t.deliver ? ` to:${t.deliver}` : ''} → ${t.spec.slice(0, 60)} (next ${new Date(t.runAt).toLocaleString()})`)
|
|
48
64
|
.join('\n');
|
|
49
65
|
},
|
|
50
66
|
});
|
package/dist/tools/search.js
CHANGED
|
@@ -11,17 +11,194 @@ import { agentCwd } from '../agentContext.js';
|
|
|
11
11
|
const FALLBACK_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.cache', '.turbo', '.vercel', 'vendor']);
|
|
12
12
|
const FALLBACK_MAX_FILE = 2 * 1024 * 1024; // ข้ามไฟล์ใหญ่ (กันช้า/binary)
|
|
13
13
|
const PER_FILE_CAP = 50; // เหมือน rg --max-count 50
|
|
14
|
+
function otherAsciiCase(ch) {
|
|
15
|
+
const code = ch.charCodeAt(0);
|
|
16
|
+
if (code >= 65 && code <= 90)
|
|
17
|
+
return ch.toLowerCase();
|
|
18
|
+
if (code >= 97 && code <= 122)
|
|
19
|
+
return ch.toUpperCase();
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function isAsciiLower(ch) {
|
|
23
|
+
const code = ch.charCodeAt(0);
|
|
24
|
+
return code >= 97 && code <= 122;
|
|
25
|
+
}
|
|
26
|
+
function isAsciiUpper(ch) {
|
|
27
|
+
const code = ch.charCodeAt(0);
|
|
28
|
+
return code >= 65 && code <= 90;
|
|
29
|
+
}
|
|
30
|
+
function findCharClassEnd(source, start) {
|
|
31
|
+
let escaping = false;
|
|
32
|
+
const literalRightBracket = source[start] === '^' ? start + 1 : start;
|
|
33
|
+
for (let i = start; i < source.length; i += 1) {
|
|
34
|
+
const ch = source[i];
|
|
35
|
+
if (escaping) {
|
|
36
|
+
escaping = false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === '\\') {
|
|
40
|
+
escaping = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === ']' && i !== literalRightBracket)
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
function findScopedGroupEnd(source, start) {
|
|
49
|
+
let depth = 1;
|
|
50
|
+
let escaping = false;
|
|
51
|
+
for (let i = start; i < source.length; i += 1) {
|
|
52
|
+
const ch = source[i];
|
|
53
|
+
if (escaping) {
|
|
54
|
+
escaping = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === '\\') {
|
|
58
|
+
escaping = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '[') {
|
|
62
|
+
const end = findCharClassEnd(source, i + 1);
|
|
63
|
+
if (end < 0)
|
|
64
|
+
return -1;
|
|
65
|
+
i = end;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === '(')
|
|
69
|
+
depth += 1;
|
|
70
|
+
if (ch === ')') {
|
|
71
|
+
depth -= 1;
|
|
72
|
+
if (depth === 0)
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
function foldAsciiRegexCharClass(source) {
|
|
79
|
+
let out = '';
|
|
80
|
+
const literalRightBracket = source[0] === '^' ? 1 : 0;
|
|
81
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
82
|
+
const ch = source[i];
|
|
83
|
+
if (ch === ']' && i === literalRightBracket) {
|
|
84
|
+
out += '\\]';
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === '\\' && i + 1 < source.length) {
|
|
88
|
+
out += `${ch}${source[i + 1]}`;
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (i + 2 < source.length &&
|
|
93
|
+
source[i + 1] === '-' &&
|
|
94
|
+
source[i + 2] !== ']' &&
|
|
95
|
+
((isAsciiLower(ch) && isAsciiLower(source[i + 2])) || (isAsciiUpper(ch) && isAsciiUpper(source[i + 2]))) &&
|
|
96
|
+
ch.charCodeAt(0) <= source[i + 2].charCodeAt(0)) {
|
|
97
|
+
out += `${ch}-${source[i + 2]}${otherAsciiCase(ch)}-${otherAsciiCase(source[i + 2])}`;
|
|
98
|
+
i += 2;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const other = i === 0 && ch === '^' ? undefined : otherAsciiCase(ch);
|
|
102
|
+
out += other ? `${ch}${other}` : ch;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function foldAsciiRegexLetters(source) {
|
|
107
|
+
let out = '';
|
|
108
|
+
let escaping = false;
|
|
109
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
110
|
+
const ch = source[i];
|
|
111
|
+
if (escaping) {
|
|
112
|
+
out += ch;
|
|
113
|
+
escaping = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ch === '\\') {
|
|
117
|
+
out += ch;
|
|
118
|
+
escaping = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch === '[') {
|
|
122
|
+
const end = findCharClassEnd(source, i + 1);
|
|
123
|
+
if (end < 0) {
|
|
124
|
+
out += ch;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out += `[${foldAsciiRegexCharClass(source.slice(i + 1, end))}]`;
|
|
128
|
+
i = end;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const other = otherAsciiCase(ch);
|
|
132
|
+
out += other ? `[${ch}${other}]` : ch;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function expandScopedCaseInsensitiveGroups(pattern) {
|
|
137
|
+
let out = '';
|
|
138
|
+
let changed = false;
|
|
139
|
+
let escaping = false;
|
|
140
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
141
|
+
const ch = pattern[i];
|
|
142
|
+
if (escaping) {
|
|
143
|
+
out += ch;
|
|
144
|
+
escaping = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (ch === '\\') {
|
|
148
|
+
out += ch;
|
|
149
|
+
escaping = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (ch === '[') {
|
|
153
|
+
const end = findCharClassEnd(pattern, i + 1);
|
|
154
|
+
if (end < 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
out += pattern.slice(i, end + 1);
|
|
157
|
+
i = end;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!pattern.startsWith('(?i:', i)) {
|
|
161
|
+
out += ch;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const end = findScopedGroupEnd(pattern, i + 4);
|
|
165
|
+
if (end < 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
out += `(?:${foldAsciiRegexLetters(pattern.slice(i + 4, end))})`;
|
|
168
|
+
i = end;
|
|
169
|
+
changed = true;
|
|
170
|
+
}
|
|
171
|
+
return changed ? out : undefined;
|
|
172
|
+
}
|
|
173
|
+
function compileFallbackRegex(pattern) {
|
|
174
|
+
const caseInsensitive = pattern.match(/^\(\?i\)([\s\S]*)$/);
|
|
175
|
+
if (caseInsensitive) {
|
|
176
|
+
const source = expandScopedCaseInsensitiveGroups(caseInsensitive[1]) ?? caseInsensitive[1];
|
|
177
|
+
return new RegExp(source, 'i');
|
|
178
|
+
}
|
|
179
|
+
const scopedCaseInsensitive = expandScopedCaseInsensitiveGroups(pattern);
|
|
180
|
+
if (scopedCaseInsensitive)
|
|
181
|
+
return new RegExp(scopedCaseInsensitive);
|
|
182
|
+
return new RegExp(pattern); // rg ใช้ Rust regex; JS regex ใกล้เคียงพอสำหรับ pattern ทั่วไป
|
|
183
|
+
}
|
|
14
184
|
export async function jsGrep(pattern, base, target) {
|
|
15
185
|
let re;
|
|
16
186
|
try {
|
|
17
|
-
re =
|
|
187
|
+
re = compileFallbackRegex(pattern);
|
|
18
188
|
}
|
|
19
189
|
catch {
|
|
20
190
|
return `ERROR: grep regex ไม่ถูกต้อง: "${pattern}"`;
|
|
21
191
|
}
|
|
22
192
|
const root = isAbsolute(target) ? target : join(base, target);
|
|
193
|
+
const rootGuard = await checkReadPath(root);
|
|
194
|
+
if (!rootGuard.ok)
|
|
195
|
+
return `BLOCKED: ${rootGuard.reason}`;
|
|
23
196
|
const out = [];
|
|
197
|
+
let truncated = false;
|
|
24
198
|
const scanFile = async (full) => {
|
|
199
|
+
const guard = await checkReadPath(full);
|
|
200
|
+
if (!guard.ok)
|
|
201
|
+
return;
|
|
25
202
|
let s;
|
|
26
203
|
try {
|
|
27
204
|
s = await stat(full);
|
|
@@ -41,10 +218,14 @@ export async function jsGrep(pattern, base, target) {
|
|
|
41
218
|
if (content.includes('\u0000'))
|
|
42
219
|
return; // binary
|
|
43
220
|
const rel = relative(base, full) || full;
|
|
44
|
-
const lines = content.split(/\r
|
|
221
|
+
const lines = content.split(/\r\n|\n|\r/);
|
|
45
222
|
let perFile = 0;
|
|
46
|
-
for (let i = 0; i < lines.length &&
|
|
223
|
+
for (let i = 0; i < lines.length && !truncated; i++) {
|
|
47
224
|
if (re.test(lines[i])) {
|
|
225
|
+
if (out.length >= MAX_RESULTS) {
|
|
226
|
+
truncated = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
48
229
|
out.push(`${rel}:${i + 1}:${lines[i].slice(0, 300)}`);
|
|
49
230
|
if (++perFile >= PER_FILE_CAP)
|
|
50
231
|
break;
|
|
@@ -52,7 +233,7 @@ export async function jsGrep(pattern, base, target) {
|
|
|
52
233
|
}
|
|
53
234
|
};
|
|
54
235
|
const walk = async (dir) => {
|
|
55
|
-
if (
|
|
236
|
+
if (truncated)
|
|
56
237
|
return;
|
|
57
238
|
let entries;
|
|
58
239
|
try {
|
|
@@ -61,15 +242,19 @@ export async function jsGrep(pattern, base, target) {
|
|
|
61
242
|
catch {
|
|
62
243
|
return;
|
|
63
244
|
}
|
|
64
|
-
for (const e of entries) {
|
|
65
|
-
if (
|
|
245
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
246
|
+
if (truncated)
|
|
66
247
|
return;
|
|
248
|
+
const full = join(dir, e.name);
|
|
249
|
+
const guard = await checkReadPath(full);
|
|
250
|
+
if (!guard.ok)
|
|
251
|
+
continue;
|
|
67
252
|
if (e.isDirectory()) {
|
|
68
253
|
if (!FALLBACK_IGNORE.has(e.name) && !e.name.startsWith('.'))
|
|
69
|
-
await walk(
|
|
254
|
+
await walk(full);
|
|
70
255
|
}
|
|
71
256
|
else if (e.isFile()) {
|
|
72
|
-
await scanFile(
|
|
257
|
+
await scanFile(full);
|
|
73
258
|
}
|
|
74
259
|
}
|
|
75
260
|
};
|
|
@@ -86,10 +271,22 @@ export async function jsGrep(pattern, base, target) {
|
|
|
86
271
|
await walk(root);
|
|
87
272
|
if (!out.length)
|
|
88
273
|
return '(no matches)';
|
|
274
|
+
if (truncated)
|
|
275
|
+
out.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
89
276
|
return `${clamp(out.join('\n'))}\n[JS fallback — ติดตั้ง ripgrep (rg) เพื่อความเร็ว + เคารพ .gitignore: brew/apt/choco/scoop install ripgrep]`;
|
|
90
277
|
}
|
|
91
278
|
const execFileAsync = promisify(execFile);
|
|
92
279
|
const MAX_RESULTS = 200;
|
|
280
|
+
export function formatRipgrepOutput(stdout) {
|
|
281
|
+
const text = stdout.replace(/(?:\r\n|\n|\r)$/, '');
|
|
282
|
+
if (!text)
|
|
283
|
+
return '(no matches)';
|
|
284
|
+
const allLines = text.split(/\r\n|\n|\r/);
|
|
285
|
+
const lines = allLines.slice(0, MAX_RESULTS);
|
|
286
|
+
if (allLines.length > MAX_RESULTS)
|
|
287
|
+
lines.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
288
|
+
return clamp(lines.join('\n')) || '(no matches)';
|
|
289
|
+
}
|
|
93
290
|
function unsafeGlobPattern(pattern) {
|
|
94
291
|
return isAbsolute(pattern) || pattern.split(/[\\/]+/).includes('..');
|
|
95
292
|
}
|
|
@@ -109,14 +306,22 @@ export const globTool = tool({
|
|
|
109
306
|
return `BLOCKED: ${guard.reason}`;
|
|
110
307
|
try {
|
|
111
308
|
const out = [];
|
|
309
|
+
let truncated = false;
|
|
112
310
|
for await (const f of glob(pattern, { cwd: base })) {
|
|
113
|
-
|
|
311
|
+
const match = String(f);
|
|
312
|
+
const itemGuard = await checkReadPath(join(base, match));
|
|
313
|
+
if (!itemGuard.ok)
|
|
314
|
+
continue;
|
|
114
315
|
if (out.length >= MAX_RESULTS) {
|
|
115
|
-
|
|
316
|
+
truncated = true;
|
|
116
317
|
break;
|
|
117
318
|
}
|
|
319
|
+
out.push(match);
|
|
118
320
|
}
|
|
119
|
-
|
|
321
|
+
out.sort();
|
|
322
|
+
if (truncated)
|
|
323
|
+
out.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
324
|
+
return out.length ? out.join('\n') : '(no matches)';
|
|
120
325
|
}
|
|
121
326
|
catch (err) {
|
|
122
327
|
return `ERROR: glob "${pattern}" ล้มเหลว — ${err.message}`;
|
|
@@ -138,8 +343,7 @@ export const grepTool = tool({
|
|
|
138
343
|
// execFile (args array, ไม่ผ่าน shell) → $(...)/backtick/$VAR ใน pattern/path เป็น inert
|
|
139
344
|
// กัน command injection (JSON.stringify ไม่ใช่ shell quoting — เคยรั่ว); -e กัน pattern ขึ้นต้นด้วย -
|
|
140
345
|
const { stdout } = await execFileAsync('rg', ['--line-number', '--no-heading', '--max-count', '50', '-e', pattern, '--', path], { cwd: base, maxBuffer: 10 * 1024 * 1024 });
|
|
141
|
-
|
|
142
|
-
return clamp(lines.join('\n')) || '(no matches)';
|
|
346
|
+
return formatRipgrepOutput(stdout);
|
|
143
347
|
}
|
|
144
348
|
catch (err) {
|
|
145
349
|
// ripgrep exit code 1 = ไม่เจอ match (ไม่ใช่ error จริง)
|
package/dist/tools/task.js
CHANGED
|
@@ -2,7 +2,7 @@ import { tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { agentContext, agentCwd } from '../agentContext.js';
|
|
4
4
|
import { approvalContext } from '../approval.js';
|
|
5
|
-
import { runParallel, runThunks, TaskRegistry } from '../orchestrate.js';
|
|
5
|
+
import { runParallel, runThunks, TaskRegistry, formatSubagentError, withGlobalSubagentSlot, } from '../orchestrate.js';
|
|
6
6
|
import { runInWorktrees, getRepoRoot } from '../worktree.js';
|
|
7
7
|
// task = มอบงานย่อยให้ sub-agent ทำใน context แยก (เลียน Claude Code Task tool)
|
|
8
8
|
// depth/model/budget thread ผ่าน AsyncLocalStorage (parallel-safe, ไม่ใช่ process.env)
|
|
@@ -22,7 +22,15 @@ const registry = new TaskRegistry();
|
|
|
22
22
|
function parentCtx() {
|
|
23
23
|
const ctx = agentContext.getStore();
|
|
24
24
|
const appr = approvalContext.getStore();
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
model: ctx?.model,
|
|
27
|
+
budgetUsd: ctx?.budgetUsd,
|
|
28
|
+
sharedBudget: ctx?.sharedBudget,
|
|
29
|
+
depth: ctx?.depth ?? 0,
|
|
30
|
+
cwd: ctx?.cwd,
|
|
31
|
+
mode: appr?.mode ?? 'ask',
|
|
32
|
+
approve: appr?.approve,
|
|
33
|
+
};
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* real subagent runner — รัน runAgent ใน context แยก. ครอบด้วย agentContext.run() ให้
|
|
@@ -43,8 +51,8 @@ function makeRunner(parent) {
|
|
|
43
51
|
const model = spec.model ?? process.env.SANOOK_SUBAGENT_MODEL ?? parent.model ?? 'sonnet';
|
|
44
52
|
const depth = parent.depth + 1;
|
|
45
53
|
const cwd = spec.cwd ?? parent.cwd; // worktree ของ subagent นี้ (ถ้า isolate) ไม่งั้น inherit
|
|
46
|
-
const childStore = { model, budgetUsd: parent.budgetUsd, depth, cwd };
|
|
47
|
-
const { text } = await agentContext.run(childStore, () => runAgent({
|
|
54
|
+
const childStore = { model, budgetUsd: parent.budgetUsd, sharedBudget: parent.sharedBudget, depth, cwd };
|
|
55
|
+
const { text } = await withGlobalSubagentSlot(() => agentContext.run(childStore, () => runAgent({
|
|
48
56
|
model,
|
|
49
57
|
budgetUsd: parent.budgetUsd, // cap เดียวกับ main (กัน subagent วิ่ง uncapped)
|
|
50
58
|
subagentDepth: depth,
|
|
@@ -55,7 +63,7 @@ function makeRunner(parent) {
|
|
|
55
63
|
maxSteps: SUB_MAX_STEPS,
|
|
56
64
|
signal,
|
|
57
65
|
tools: Object.fromEntries(picked),
|
|
58
|
-
}));
|
|
66
|
+
})));
|
|
59
67
|
return text || '(sub-agent ไม่มีผลลัพธ์)';
|
|
60
68
|
};
|
|
61
69
|
}
|
|
@@ -98,12 +106,15 @@ async function runIsolated(specs, parent, concurrency) {
|
|
|
98
106
|
const root = await getRepoRoot(parent.cwd ?? agentCwd());
|
|
99
107
|
if (!root)
|
|
100
108
|
return 'isolate=worktree ต้องอยู่ใน git repo — ใช้ task_parallel แบบปกติแทน (ไม่มี worktree)';
|
|
101
|
-
|
|
109
|
+
// isolate=true has one coarse approval at the task_parallel tool boundary. Inside
|
|
110
|
+
// the temporary worktree, subagents run auto so the REPL is not spammed with
|
|
111
|
+
// per-file approvals from hidden child turns; only the captured diff is merged.
|
|
112
|
+
const runner = makeRunner({ ...parent, mode: 'auto', approve: undefined });
|
|
102
113
|
const runs = await runInWorktrees(specs, root,
|
|
103
114
|
// งานต่อ subagent: รันใน worktree (cwd) ของมัน, readonly=false (isolate มีไว้ให้แก้ไฟล์)
|
|
104
115
|
(spec, cwd) => runner({ ...spec, cwd, readonly: spec.readonly ?? false }, undefined)
|
|
105
116
|
.then((text) => ({ ok: true, description: spec.description, text }))
|
|
106
|
-
.catch((e) => ({ ok: false, description: spec.description, text: '', error: e
|
|
117
|
+
.catch((e) => ({ ok: false, description: spec.description, text: '', error: formatSubagentError(e) })), (thunks) => runThunks(thunks, concurrency));
|
|
107
118
|
if (!runs)
|
|
108
119
|
return 'สร้าง git worktree ไม่สำเร็จ (หรือไม่ใช่ git repo) — ยกเลิก isolate';
|
|
109
120
|
const outcomes = runs.map((r) => r.result);
|
package/dist/tools/timeout.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
1
2
|
// ครอบ tool ด้วย timeout — กัน read/grep/glob/edit บนไฟล์ใหญ่ค้าง แล้วแขวน loop ทั้ง session ไม่จบ
|
|
2
|
-
// tool ที่จัดการ timeout เองอยู่แล้ว → ไม่ครอบ: run_bash (120s ในตัว),
|
|
3
|
-
const SELF_TIMED = new Set(['run_bash', 'task']);
|
|
3
|
+
// tool ที่จัดการ timeout เองอยู่แล้ว → ไม่ครอบ: run_bash (120s ในตัว), sub-agent orchestration (อาจรัน/รอนานโดยตั้งใจ)
|
|
4
|
+
const SELF_TIMED = new Set(['run_bash', 'task', 'task_parallel', 'task_collect']);
|
|
4
5
|
export const DEFAULT_TOOL_TIMEOUT = 120_000;
|
|
6
|
+
function formatToolError(e) {
|
|
7
|
+
if (e instanceof Error)
|
|
8
|
+
return e.message || e.name;
|
|
9
|
+
if (typeof e === 'string')
|
|
10
|
+
return e;
|
|
11
|
+
if (e == null)
|
|
12
|
+
return String(e);
|
|
13
|
+
try {
|
|
14
|
+
const json = JSON.stringify(e);
|
|
15
|
+
if (json)
|
|
16
|
+
return json;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return inspect(e, { breakLength: Infinity, depth: 2 });
|
|
20
|
+
}
|
|
21
|
+
return String(e);
|
|
22
|
+
}
|
|
5
23
|
/** Promise.race tool execute กับ timer — timeout คืนเป็น ERROR string (tool ไม่ throw เข้า loop) */
|
|
6
24
|
export function wrapToolsWithTimeout(tools, ms = DEFAULT_TOOL_TIMEOUT) {
|
|
7
25
|
const out = {};
|
|
@@ -22,7 +40,7 @@ export function wrapToolsWithTimeout(tools, ms = DEFAULT_TOOL_TIMEOUT) {
|
|
|
22
40
|
return await Promise.race([Promise.resolve(orig(input, opts)), timeout]);
|
|
23
41
|
}
|
|
24
42
|
catch (e) {
|
|
25
|
-
return `ERROR: ${e
|
|
43
|
+
return `ERROR: ${formatToolError(e)}`;
|
|
26
44
|
}
|
|
27
45
|
finally {
|
|
28
46
|
if (timer)
|
package/dist/trust.js
CHANGED
|
@@ -20,6 +20,9 @@ async function canonical(p) {
|
|
|
20
20
|
return resolve(p);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
function isUsableStoredRoot(root) {
|
|
24
|
+
return typeof root === 'string' && root.trim().length > 0 && !root.includes('\0');
|
|
25
|
+
}
|
|
23
26
|
export async function projectRoot(cwd = process.cwd()) {
|
|
24
27
|
let dir = resolve(cwd);
|
|
25
28
|
for (;;) {
|
|
@@ -35,7 +38,14 @@ export async function projectRoot(cwd = process.cwd()) {
|
|
|
35
38
|
async function readStore() {
|
|
36
39
|
try {
|
|
37
40
|
const parsed = JSON.parse(await readFile(TRUST_FILE, 'utf8'));
|
|
38
|
-
|
|
41
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
42
|
+
return {};
|
|
43
|
+
const roots = parsed.trustedProjectRoots;
|
|
44
|
+
if (roots === undefined)
|
|
45
|
+
return {};
|
|
46
|
+
return {
|
|
47
|
+
trustedProjectRoots: Array.isArray(roots) ? roots.filter(isUsableStoredRoot) : [],
|
|
48
|
+
};
|
|
39
49
|
}
|
|
40
50
|
catch {
|
|
41
51
|
return {};
|
package/dist/ui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useRef
|
|
2
|
+
import { useState, useRef } from 'react';
|
|
3
3
|
import { execFile } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { Box, Text, Static, useApp, useInput } from 'ink';
|
|
@@ -9,8 +9,9 @@ import { saveSession, newSessionId } from '../session.js';
|
|
|
9
9
|
import { getBrainPath, appendBrainWorklog } from '../memory.js';
|
|
10
10
|
import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
|
|
11
11
|
import { makeSummarizer } from '../summarize.js';
|
|
12
|
-
import { agentTuning } from '../config.js';
|
|
12
|
+
import { agentTuning, patchGlobalConfig } from '../config.js';
|
|
13
13
|
import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
|
|
14
|
+
import { renderInsights } from '../insights.js';
|
|
14
15
|
import { useEditor } from './useEditor.js';
|
|
15
16
|
import { loadHistory, appendHistory } from './history.js';
|
|
16
17
|
import { expandMentions } from './mentions.js';
|
|
@@ -40,6 +41,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
40
41
|
const approvalResolve = useRef(null);
|
|
41
42
|
const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
|
|
42
43
|
const checkpoints = useRef([]);
|
|
44
|
+
const lastRun = useRef(null);
|
|
43
45
|
const editor = useEditor(replHistory.current);
|
|
44
46
|
// real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
|
|
45
47
|
const abortRef = useRef(null);
|
|
@@ -99,6 +101,13 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
99
101
|
if (a === 'submit') {
|
|
100
102
|
const v = editor.value.trim();
|
|
101
103
|
editor.reset();
|
|
104
|
+
const slash = parseSlashInvocation(v);
|
|
105
|
+
if (slash?.name === 'stop') {
|
|
106
|
+
addTurn('user', v);
|
|
107
|
+
abortRef.current?.abort();
|
|
108
|
+
clearQueue();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
102
111
|
if (v)
|
|
103
112
|
enqueue(v);
|
|
104
113
|
}
|
|
@@ -131,9 +140,26 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
131
140
|
: ` · ไฟล์: ${r.reason}`;
|
|
132
141
|
}
|
|
133
142
|
msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
|
|
143
|
+
lastRun.current = null;
|
|
134
144
|
setHistory((h) => h.filter((t) => t.id < cp.turnId));
|
|
135
145
|
addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
|
|
136
146
|
}
|
|
147
|
+
async function retryLastTurn() {
|
|
148
|
+
const previous = lastRun.current;
|
|
149
|
+
if (!previous) {
|
|
150
|
+
addTurn('user', '/retry');
|
|
151
|
+
addTurn('system', 'ยังไม่มี turn ให้ retry');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
msgsRef.current = msgsRef.current.slice(0, previous.msgLen);
|
|
155
|
+
checkpoints.current = checkpoints.current.filter((cp) => cp.turnId < previous.turnId);
|
|
156
|
+
setHistory((h) => h.filter((t) => t.id < previous.turnId));
|
|
157
|
+
const mark = { turnId: idRef.current, msgLen: previous.msgLen };
|
|
158
|
+
const preview = previous.userText.length > 120 ? `${previous.userText.slice(0, 117)}...` : previous.userText;
|
|
159
|
+
addTurn('user', '/retry');
|
|
160
|
+
addTurn('system', `retry: ${preview}`);
|
|
161
|
+
await runAssistantTurn(previous.promptText, previous.images, mark, previous.userText);
|
|
162
|
+
}
|
|
137
163
|
/** บีบ context: 'summarize' (ใช้ model ถูกย่อ) ถ้าตั้งไว้ ไม่งั้น 'truncate' (zero-LLM) */
|
|
138
164
|
async function compactHistory(targetTokens, label) {
|
|
139
165
|
const before = estimateTokens(msgsRef.current);
|
|
@@ -175,7 +201,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
175
201
|
addTurn('system', `custom command /${slash.name} ว่าง`);
|
|
176
202
|
return;
|
|
177
203
|
}
|
|
178
|
-
await runAssistantTurn(expanded, [], mark);
|
|
204
|
+
await runAssistantTurn(expanded, [], mark, text);
|
|
179
205
|
return;
|
|
180
206
|
}
|
|
181
207
|
}
|
|
@@ -188,6 +214,7 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
188
214
|
if (cmd.action === 'clear') {
|
|
189
215
|
msgsRef.current = [];
|
|
190
216
|
checkpoints.current = [];
|
|
217
|
+
lastRun.current = null;
|
|
191
218
|
return setHistory([]);
|
|
192
219
|
}
|
|
193
220
|
if (cmd.action === 'compact') {
|
|
@@ -196,6 +223,20 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
196
223
|
}
|
|
197
224
|
if (cmd.action === 'diff')
|
|
198
225
|
return void runGit(['diff', '--stat'], 'diff');
|
|
226
|
+
if (cmd.action === 'retry')
|
|
227
|
+
return void retryLastTurn();
|
|
228
|
+
if (cmd.action === 'personality') {
|
|
229
|
+
void patchGlobalConfig({ personality: cmd.personalityChange || undefined })
|
|
230
|
+
.then(() => addTurn('system', cmd.message ?? 'ตั้ง personality แล้ว'))
|
|
231
|
+
.catch((e) => addTurn('system', `personality: ${e.message}`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (cmd.action === 'insights') {
|
|
235
|
+
void renderInsights({ days: cmd.insightsDays, cwd: cmd.insightsAll ? null : undefined })
|
|
236
|
+
.then((msg) => addTurn('system', msg))
|
|
237
|
+
.catch((e) => addTurn('system', `insights: ${e.message}`));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
199
240
|
if (cmd.action === 'undo') {
|
|
200
241
|
void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
|
|
201
242
|
return;
|
|
@@ -212,9 +253,10 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
212
253
|
const { text: expanded, images, errors } = await expandMentions(text);
|
|
213
254
|
if (errors.length)
|
|
214
255
|
addTurn('system', `@mention: ${errors.join(' · ')}`);
|
|
215
|
-
await runAssistantTurn(expanded, images, mark);
|
|
256
|
+
await runAssistantTurn(expanded, images, mark, text);
|
|
216
257
|
}
|
|
217
|
-
async function runAssistantTurn(promptText, images, mark) {
|
|
258
|
+
async function runAssistantTurn(promptText, images, mark, userText = promptText) {
|
|
259
|
+
lastRun.current = { ...mark, userText, promptText, images };
|
|
218
260
|
// proactive summarize-compaction สำหรับ session ยาวมาก (เฉพาะ mode summarize) — เริ่ม turn ให้ context lean
|
|
219
261
|
// (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
|
|
220
262
|
if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
|
|
@@ -304,10 +346,8 @@ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = '
|
|
|
304
346
|
if (next)
|
|
305
347
|
void submit(next);
|
|
306
348
|
}
|
|
307
|
-
// banner ผูกกับ live `model` (ไม่ใช่ initialModel) → /model เปลี่ยนแล้ว banner อัปเดตตาม ไม่ค้าง model เก่า
|
|
308
|
-
const banner = useMemo(() => _jsx(Banner, { model: model }), [model]);
|
|
309
349
|
const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
|
|
310
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
350
|
+
return (_jsxs(Box, { flexDirection: "column", children: [history.length === 0 ? _jsx(Banner, { model: model }) : null, _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}` : ''] })] }));
|
|
311
351
|
}
|
|
312
352
|
/** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
|
|
313
353
|
function InputView({ value, cursor, busy }) {
|