@bolloon/bolloon-agent 0.3.22 → 0.3.24

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 (37) hide show
  1. package/dist/agents/deny-pipeline.js +116 -0
  2. package/dist/agents/parse-tool-call.js +26 -4
  3. package/dist/agents/pi-sdk.js +247 -50
  4. package/dist/agents/session-store.js +87 -2
  5. package/dist/bootstrap/snip-collapse.js +135 -0
  6. package/dist/cli/ink-app.js +116 -0
  7. package/dist/cli/loading-tui.js +64 -10
  8. package/dist/electron/config.js +14 -9
  9. package/dist/electron/dialogs.js +53 -16
  10. package/dist/electron/first-run.js +65 -24
  11. package/dist/electron/ipc.js +14 -10
  12. package/dist/electron/logger.js +44 -7
  13. package/dist/electron/main.js +45 -42
  14. package/dist/electron/menu.js +18 -13
  15. package/dist/electron/paths.js +54 -12
  16. package/dist/electron/server.js +57 -18
  17. package/dist/electron/tray.js +53 -15
  18. package/dist/electron/window.js +61 -22
  19. package/dist/electron-preload.js +19 -16
  20. package/dist/electron.js +4 -1
  21. package/dist/external-engines/delegate.js +19 -0
  22. package/dist/hooks/hooks-engine.js +329 -0
  23. package/dist/index.js +130 -133
  24. package/dist/llm/pi-ai.js +5 -17
  25. package/dist/security/tool-gate.js +8 -1
  26. package/dist/social/dunbar-tier.js +409 -0
  27. package/dist/utils/auto-update.js +51 -12
  28. package/dist/web/client.js +4833 -4328
  29. package/dist/web/components/p2p/index.js +234 -276
  30. package/dist/web/server.js +17 -3
  31. package/dist/web/style.css +2 -2
  32. package/dist/web/ui/message-renderer.js +396 -535
  33. package/dist/web/ui/step-timeline.js +273 -372
  34. package/package.json +27 -25
  35. package/dist/web/components/p2p/P2PModal.js +0 -188
  36. package/dist/web/components/p2p/p2p-modal.js +0 -664
  37. package/dist/web/components/p2p/p2p-tools.js +0 -248
@@ -21,13 +21,80 @@ import * as path from 'path';
21
21
  import * as os from 'os';
22
22
  export class SessionStore {
23
23
  cacheDir;
24
+ /** 2026-07-29: JSONL 目录 (~/.bolloon/sessions/jsonl/) */
25
+ jsonlDir;
24
26
  constructor(config = {}) {
25
27
  this.cacheDir = config.cacheDir ?? path.join(os.homedir(), '.bolloon', 'sessions', 'cache');
28
+ this.jsonlDir = path.join(os.homedir(), '.bolloon', 'sessions', 'jsonl');
26
29
  }
27
- /** 当前缓存目录 (只读). */
28
30
  get dir() {
29
31
  return this.cacheDir;
30
32
  }
33
+ /** JSONL 文件路径 */
34
+ jsonlPathFor(key) {
35
+ if (!key || key.includes('/') || key.includes('..')) {
36
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
37
+ }
38
+ return path.join(this.jsonlDir, `${SessionStore.filenameEscape(key)}.jsonl`);
39
+ }
40
+ /**
41
+ * 2026-07-29: Append-only JSONL — 追加一条消息.
42
+ * 每条消息独立一行 JSON, 不破坏历史数据.
43
+ * 每行格式: {ts, role, content, toolCall?, toolCallId?, toolResult?}
44
+ */
45
+ async appendMessageJsonl(key, msg) {
46
+ if (!key || key.includes('/') || key.includes('..')) {
47
+ throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
48
+ }
49
+ await fs.mkdir(this.jsonlDir, { recursive: true });
50
+ const filePath = this.jsonlPathFor(key);
51
+ const line = JSON.stringify({
52
+ ts: Date.now(),
53
+ role: msg.role,
54
+ content: msg.content,
55
+ toolCall: msg.toolCall || undefined,
56
+ toolCallId: msg.toolCallId || undefined,
57
+ toolResult: msg.toolResult || undefined,
58
+ }) + '\n';
59
+ await fs.appendFile(filePath, line, 'utf-8');
60
+ }
61
+ /**
62
+ * 2026-07-29: 从 JSONL 完整重建消息历史.
63
+ * 文件不存在 → 返回 []. 损坏行 → 跳过不抛错.
64
+ */
65
+ async loadFromJsonl(key) {
66
+ const filePath = this.jsonlPathFor(key);
67
+ let raw;
68
+ try {
69
+ raw = await fs.readFile(filePath, 'utf-8');
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ const messages = [];
75
+ for (const rawLine of raw.split('\n')) {
76
+ const trimmed = rawLine.trim();
77
+ if (!trimmed)
78
+ continue;
79
+ try {
80
+ const entry = JSON.parse(trimmed);
81
+ if (!entry.role)
82
+ continue;
83
+ messages.push({
84
+ role: entry.role,
85
+ content: entry.content || '',
86
+ toolCall: entry.toolCall,
87
+ toolCallId: entry.toolCallId,
88
+ toolResult: entry.toolResult,
89
+ timestamp: entry.ts,
90
+ });
91
+ }
92
+ catch {
93
+ // 跳过损坏行
94
+ }
95
+ }
96
+ return messages;
97
+ }
31
98
  /** 单文件路径 — 暴露出来方便测试和外部读取.
32
99
  *
33
100
  * 2026-07-04 fix: Windows 文件名禁止 `:` (NTFS). web server 用 `channelId:currentSessionId`
@@ -69,10 +136,28 @@ export class SessionStore {
69
136
  },
70
137
  };
71
138
  const filePath = this.pathFor(key);
72
- // 写临时文件 + rename — 防止半写损坏
73
139
  const tmpPath = `${filePath}.tmp`;
74
140
  await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
75
141
  await fs.rename(tmpPath, filePath);
142
+ // 2026-07-29: 同时写 JSONL (增量追加, 不覆盖)
143
+ // 每个 message 一个 append, 保证可审计 + 可重建
144
+ try {
145
+ await fs.mkdir(this.jsonlDir, { recursive: true });
146
+ const jsonlPath = this.jsonlPathFor(key);
147
+ const lines = [];
148
+ for (const msg of messages) {
149
+ lines.push(JSON.stringify({
150
+ ts: msg.timestamp || Date.now(),
151
+ role: msg.role,
152
+ content: msg.content,
153
+ toolCall: msg.toolCall || undefined,
154
+ toolCallId: msg.toolCallId || undefined,
155
+ toolResult: msg.toolResult || undefined,
156
+ }));
157
+ }
158
+ await fs.appendFile(jsonlPath, lines.join('\n') + '\n', 'utf-8');
159
+ }
160
+ catch { /* JSONL 写入失败不阻塞主存储 */ }
76
161
  }
77
162
  /**
78
163
  * 同步版 (PiAgentSession 里多条路径用同步 — 避免 async 链)
@@ -0,0 +1,135 @@
1
+ /**
2
+ * snip-collapse.ts — Phase 2 & 3 (2026-07-29)
3
+ *
4
+ * Claude Code 式: Snip (预算感知裁历史) + Context Collapse (读时虚拟投影)
5
+ *
6
+ * Phase 2 — Snip: 在每次模型调用前, 根据 token 预算裁掉最老的历史.
7
+ * 比 session-window LRU 更强: 保留工具调用链完整性, 不中断 invoke→result 对.
8
+ *
9
+ * Phase 3 — Context Collapse: 读时虚拟投影, 原始 messageHistory 永不破坏.
10
+ * 构建一个"投影"版本: 长工具结果被摘要代替, 模型看到的是压缩版.
11
+ *
12
+ * 两者都不修改 this.messageHistory — 只返回一个新的投影数组.
13
+ */
14
+ /** 错误工具结果 / 空内容 最大保留长度 */
15
+ const MAX_TOOL_RESULT_SNIP_CHARS = 300;
16
+ /** 单条消息最大保留长度 (Budger Reduction) */
17
+ const MAX_MESSAGE_SNIP_CHARS = 2000;
18
+ /** 工具调用结果投影摘要长度 */
19
+ const MAX_COLLAPSED_TOOL_CHARS = 150;
20
+ /**
21
+ * Phase 2: Snip — 预算感知裁历史.
22
+ * 在系统提示装配前调用, 返回裁剪后的消息数组.
23
+ *
24
+ * 规则:
25
+ * 1. Budget Reduction: 每条消息 content 不超过 maxMessageChars
26
+ * 2. Snip: 如果消息数超过 maxMessages, 从最老的开始裁,
27
+ * 但保留最近的工具调用链 (assistant+tool 对不能拆)
28
+ * 3. 如果即使裁到 maxMessages 条还是太贵, 裁剪工具结果大小
29
+ */
30
+ export function snipHistory(messages, opts = {}) {
31
+ const maxMessages = opts.maxMessages ?? 60;
32
+ const maxMessageChars = opts.maxMessageChars ?? MAX_MESSAGE_SNIP_CHARS;
33
+ const maxToolResultChars = opts.maxToolResultChars ?? MAX_TOOL_RESULT_SNIP_CHARS;
34
+ if (messages.length === 0)
35
+ return [];
36
+ // Step 1: Budget Reduction — 每条消息截断到上限
37
+ let budgeted = messages.map(m => {
38
+ if (m.content.length > maxMessageChars) {
39
+ return {
40
+ ...m,
41
+ content: m.content.slice(0, maxMessageChars) + `\n[...截断, 原长 ${m.content.length} 字符]`,
42
+ originalLength: m.content.length,
43
+ transform: 'budget-reduce',
44
+ };
45
+ }
46
+ return m;
47
+ });
48
+ // Step 2: Snip — 超过 maxMessages 时裁最老的
49
+ if (budgeted.length <= maxMessages)
50
+ return budgeted;
51
+ // 工具调用链保护: 从尾部往前数, 保留最近的 assistant+tool 对
52
+ const keepCount = maxMessages;
53
+ const result = [];
54
+ const toRemove = budgeted.length - keepCount;
55
+ // 策略: 从最老的开始裁, 但要保证不会裁掉未配对的 assistant (tool_calls)
56
+ // 遍历时追踪 "悬空的 tool 消息" 保护
57
+ let removed = 0;
58
+ let protectedToolChain = 0; // 从尾部连续 tool 消息不裁
59
+ for (let i = budgeted.length - 1; i >= 0; i--) {
60
+ const m = budgeted[i];
61
+ if (m.role === 'tool' && protectedToolChain < 5) {
62
+ protectedToolChain++;
63
+ }
64
+ else if (m.role === 'assistant' && protectedToolChain > 0) {
65
+ // 遇到 assistant 代表这个工具链结束了
66
+ }
67
+ else {
68
+ protectedToolChain = 0;
69
+ }
70
+ // 如果在保护区内, 不裁
71
+ if (i < budgeted.length - keepCount && budgeted.length - i > protectedToolChain) {
72
+ removed++;
73
+ if (removed <= toRemove) {
74
+ result.unshift({ ...m, content: '[已裁减, Snip]', transform: 'snip' });
75
+ continue;
76
+ }
77
+ }
78
+ result.unshift(m);
79
+ }
80
+ // Step 3: 如果 tool result 仍然太长, 进一步截断
81
+ return result.map(m => {
82
+ if (m.role === 'tool' && m.content.length > maxToolResultChars) {
83
+ return {
84
+ ...m,
85
+ content: m.content.slice(0, maxToolResultChars) + `\n[...工具结果截断]`,
86
+ originalLength: m.content.length,
87
+ transform: m.transform || 'snip-tool',
88
+ };
89
+ }
90
+ return m;
91
+ });
92
+ }
93
+ /**
94
+ * Phase 3: Context Collapse — 读时虚拟投影.
95
+ * 将 verbose 的工具结果替换为摘要, 但不修改原始数据.
96
+ *
97
+ * 适用场景:
98
+ * - 工具返回了很大的 JSON/日志 (> 500 字符)
99
+ * - 模型需要看到"结果", 但不需要完整内容
100
+ * - 原始 messageHistory 依然完整保存在 session store 里
101
+ */
102
+ export function collapseContext(messages, opts = {}) {
103
+ const maxChars = opts.maxCollapsedToolChars ?? MAX_COLLAPSED_TOOL_CHARS;
104
+ return messages.map(m => {
105
+ // 只投影 tool role 和特别长的 assistant 消息
106
+ if (m.role === 'tool' && m.content.length > maxChars) {
107
+ const preview = m.content.slice(0, maxChars);
108
+ return {
109
+ ...m,
110
+ content: `${preview}\n[...Context Collapse 投影: 原始 ${m.content.length} 字符, 已压缩为摘要]`,
111
+ originalLength: m.content.length,
112
+ transform: 'collapse',
113
+ };
114
+ }
115
+ // 也投影超长的 assistant 回复 (只保留开头)
116
+ if (m.role === 'assistant' && m.content.length > maxChars * 3) {
117
+ return {
118
+ ...m,
119
+ content: m.content.slice(0, maxChars * 2) + `\n[...回复过长, 已投影, 原始 ${m.content.length} 字符]`,
120
+ originalLength: m.content.length,
121
+ transform: 'collapse',
122
+ };
123
+ }
124
+ return m;
125
+ });
126
+ }
127
+ /**
128
+ * 组合应用 Snip + Collapse (Claude Code 的 pre-model 管道).
129
+ * 顺序: Budget Reduction → Snip → Context Collapse
130
+ */
131
+ export function applyPreModelPipeline(messages, snipOpts, collapseOpts) {
132
+ let result = snipHistory(messages, snipOpts);
133
+ result = collapseContext(result, collapseOpts);
134
+ return result;
135
+ }
@@ -0,0 +1,116 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ink-app.tsx — Ink (React for CLI) 渲染入口
4
+ *
5
+ * 用 Yoga flexbox 布局实现: 内容置顶, 输入栏固定底部, 状态栏固定
6
+ */
7
+ import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import { render, Box, Text, useInput, useApp } from 'ink';
9
+ import TextInput from 'ink-text-input';
10
+ import { brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './loading-tui.js';
11
+ // ─── 组件: Logo Box ──────────────────────────────────────────────────────────
12
+ const LogoBox = ({ width }) => {
13
+ const art = brandArtLines();
14
+ const mw = Math.max(40, ...art.map(l => dispWidth(l))) + 4;
15
+ const bw = Math.min(width - 2, mw);
16
+ const rows = [boxTop('Bolloon Agent', bw)];
17
+ for (const l of art)
18
+ rows.push(boxRow(l, bw, 'center'));
19
+ rows.push(boxBottom(bw));
20
+ return (_jsx(Box, { flexDirection: "column", children: rows.map((r, i) => _jsx(Text, { children: r }, i)) }));
21
+ };
22
+ // ─── 组件: 消息列表 ──────────────────────────────────────────────────────────
23
+ const Messages = ({ msgs }) => (_jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-start", children: msgs.map((m, i) => {
24
+ const clean = m.replace(/\x1b\[[0-9;]*m/g, '');
25
+ return clean.trim() ? _jsx(Text, { children: clean ? m : '' }, i) : null;
26
+ }) }));
27
+ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
28
+ const [input, setInput] = useState('');
29
+ const [msgs, setMsgs] = useState([]);
30
+ const [status, setStatus] = useState(initialStatus);
31
+ const { exit } = useApp();
32
+ const [thinking, setThinking] = useState(false);
33
+ const thinkingIdx = useRef(0);
34
+ // 全局: 思考动画控制
35
+ useEffect(() => {
36
+ globalThis.__inkSetThinking = (v) => setThinking(v);
37
+ globalThis.__inkAppend = (line) => {
38
+ setMsgs(prev => [...prev, line]);
39
+ };
40
+ globalThis.__inkSetStatus = (s) => {
41
+ setStatus(s);
42
+ };
43
+ return () => {
44
+ delete globalThis.__inkAppend;
45
+ delete globalThis.__inkSetStatus;
46
+ delete globalThis.__inkSetThinking;
47
+ };
48
+ }, []);
49
+ const onSubmit = useCallback((value) => {
50
+ const trimmed = value.trim();
51
+ if (!trimmed)
52
+ return;
53
+ setInput('');
54
+ // 用户消息由 processInput 统一通过 appendLine(renderUserMessage) 显示
55
+ onPrompt(trimmed);
56
+ }, [onPrompt]);
57
+ useInput((_input, key) => {
58
+ if (key.ctrl && _input === 'c')
59
+ exit();
60
+ // TextInput handles actual input; useInput only for Ctrl+C
61
+ });
62
+ // 自动更新状态栏 (每秒)
63
+ useEffect(() => {
64
+ const timer = setInterval(() => {
65
+ const s = getStatusUpdate();
66
+ if (s)
67
+ setStatus(s);
68
+ }, 1000);
69
+ return () => clearInterval(timer);
70
+ }, [getStatusUpdate]);
71
+ // 思考动画 — kaomoji 旋转
72
+ const KAOMOJI = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
73
+ useEffect(() => {
74
+ if (!thinking)
75
+ return;
76
+ const timer = setInterval(() => {
77
+ thinkingIdx.current = (thinkingIdx.current + 1) % KAOMOJI.length;
78
+ }, 600);
79
+ return () => clearInterval(timer);
80
+ }, [thinking]);
81
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", children: [_jsx(LogoBox, { width: terminalW }), _jsx(Messages, { msgs: msgs }), thinking && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] }) }))] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsx(Box, { children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: "\u8F93\u5165\u6D88\u606F..." })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) })] }));
82
+ };
83
+ // ─── 启动 ────────────────────────────────────────────────────────────────────
84
+ let _inkInstance = null;
85
+ export function startInk(onPrompt, initialStatus, getStatusUpdate) {
86
+ const tw = process.stdout.columns || 80;
87
+ const th = process.stdout.rows || 24;
88
+ _inkInstance = render(_jsx(InkApp, { onPrompt: onPrompt, initialStatus: initialStatus, getStatusUpdate: getStatusUpdate, terminalW: tw, terminalH: th }), {
89
+ stdout: process.stdout,
90
+ stdin: process.stdin,
91
+ exitOnCtrlC: false,
92
+ patchConsole: false, // 关键: 阻止 Ink 劫持 console.log
93
+ });
94
+ }
95
+ export function stopInk() {
96
+ if (_inkInstance) {
97
+ _inkInstance.unmount();
98
+ _inkInstance.clear();
99
+ _inkInstance = null;
100
+ }
101
+ }
102
+ export function inkAppendLine(line) {
103
+ const fn = globalThis.__inkAppend;
104
+ if (fn)
105
+ fn(line);
106
+ }
107
+ export function inkSetStatus(s) {
108
+ const fn = globalThis.__inkSetStatus;
109
+ if (fn)
110
+ fn(s);
111
+ }
112
+ export function inkSetThinking(v) {
113
+ const fn = globalThis.__inkSetThinking;
114
+ if (fn)
115
+ fn(v);
116
+ }
@@ -319,9 +319,9 @@ function renderReference(opts) {
319
319
  export function renderUserMessage(body) {
320
320
  return renderMessageBox({ title: '✓ 已发送', body, color: C_OK, maxLines: DEFAULT_MAX_LINES });
321
321
  }
322
- /** 智能体回复框 */
322
+ /** 智能体回复框 (不压缩, 用户需要看到完整回复) */
323
323
  export function renderAgentMessage(body) {
324
- return renderMessageBox({ title: '◉ Bolloon Agent', body, color: C_ACCENT, maxLines: DEFAULT_MAX_LINES });
324
+ return renderMessageBox({ title: '◉ Bolloon Agent', body, color: C_ACCENT, maxLines: 0 });
325
325
  }
326
326
  /** 循环工作流连接线: 用 ╼ ╾ 串联相邻工具框 */
327
327
  export function flowConnector(width) {
@@ -331,7 +331,56 @@ export function flowConnector(width) {
331
331
  s += unit;
332
332
  return s.slice(0, width);
333
333
  }
334
- /** 渲染单个工具调用为圆角框 (参数 / 状态 / 输出预览) */
334
+ /** 判断输出是否包含 git diff / 补丁内容 */
335
+ function isDiffOutput(text) {
336
+ return /^diff --git|^--- |^\+\+\+ |^@@ /m.test(text) ||
337
+ /^[\+\-]\s+\S/m.test(text); // + 开头的新增行 / - 开头的删除行
338
+ }
339
+ /** 把 diff 内容着色: + 行绿色, - 行红色, @@ 行蓝色, 其余不变 */
340
+ function colorizeDiff(text) {
341
+ const lines = text.split('\n');
342
+ const out = lines.map(line => {
343
+ if (line.startsWith('diff --git') || line.startsWith('---') || line.startsWith('+++')) {
344
+ return C_DIM + line + RESET;
345
+ }
346
+ if (line.startsWith('@@')) {
347
+ return fg(0x21, 0x96, 0xf3) + line + RESET; // blue
348
+ }
349
+ if (line.startsWith('+')) {
350
+ return C_OK + line + RESET; // green
351
+ }
352
+ if (line.startsWith('-')) {
353
+ return C_ERROR + line + RESET; // red
354
+ }
355
+ // 上下文行用灰色缩进
356
+ if (/^\s/.test(line) || /^[ \t]/.test(line)) {
357
+ return C_DIM + line + RESET;
358
+ }
359
+ return line;
360
+ });
361
+ return out.join('\n');
362
+ }
363
+ /** 渲染单条工具调用为紧凑列表项 (增量显示, 一行, 无 args) */
364
+ export function renderToolCallListItem(item, index, total) {
365
+ const dur = item.durationMs != null ? ` ${C_DIM}${item.durationMs}ms${RESET}` : '';
366
+ const label = item.status === 'error' ? `${C_ERROR}${item.tool}${RESET}` : `${C_ACCENT}${item.tool}${RESET}`;
367
+ return ` 🔧 ${label}${dur}`;
368
+ }
369
+ /** 紧凑渲染工具输出 (检测到 diff 则着色, 否则普通截断) — 当前不显示 body */
370
+ export function renderToolCallBody(item, width = 72) {
371
+ return '';
372
+ }
373
+ /** 增量列表标题行 (含步骤计数) */
374
+ export function renderToolCallsHeader(count) {
375
+ if (count === 0)
376
+ return '';
377
+ return ` ${C_DIM}╭─ 工具调用 (${count} 步)${RESET}`;
378
+ }
379
+ /** 增量列表结尾行 — 当前不显示 */
380
+ export function renderToolCallsFooter(count) {
381
+ return '';
382
+ }
383
+ /** 渲染单个工具调用为圆角框 (参数 / 状态 / 输出预览) — 旧版保留兼容 */
335
384
  export function renderToolCall(v) {
336
385
  const color = v.status === 'ok' ? C_OK : C_ERROR;
337
386
  const sym = v.status === 'ok' ? '✅' : '❌';
@@ -344,12 +393,17 @@ export function renderToolCall(v) {
344
393
  rows.push(`状态: ${sym} ${v.status === 'ok' ? '成功' : '失败'}${dur}`);
345
394
  const body = v.status === 'ok' ? v.output : v.error;
346
395
  if (body) {
347
- const wrapped = wrapText(body, w - 6);
348
- const shown = wrapped.slice(0, 3);
349
- for (const l of shown)
350
- rows.push(`▏ ${l}`);
351
- if (wrapped.length > 3)
352
- rows.push(`▏ 已压缩 ${wrapped.length - 3} 行`);
396
+ if (isDiffOutput(body)) {
397
+ rows.push(colorizeDiff(body));
398
+ }
399
+ else {
400
+ const wrapped = wrapText(body, w - 6);
401
+ const shown = wrapped.slice(0, 3);
402
+ for (const l of shown)
403
+ rows.push(`▏ ${l}`);
404
+ if (wrapped.length > 3)
405
+ rows.push(`▏ … 已压缩 ${wrapped.length - 3} 行`);
406
+ }
353
407
  }
354
408
  const lines = [];
355
409
  lines.push(boxTop(`${color}◉ ${v.tool}${RESET}`, w, RD));
@@ -358,7 +412,7 @@ export function renderToolCall(v) {
358
412
  lines.push(boxBottom(w, RD));
359
413
  return lines.join('\n');
360
414
  }
361
- const FRAMES = ['', '', '', '⠸', '⠼', '', '', '⠧', '⠇', '⠏'];
415
+ const FRAMES = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)', 'ヽ(´▽`)/'];
362
416
  export class LoadingTUI {
363
417
  write;
364
418
  timer = null;
@@ -1,16 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isDev = exports.MAIN_WINDOW_MIN = exports.MAIN_WINDOW_DEFAULT = exports.WEB_SERVER_STARTUP_TIMEOUT_MS = exports.DEFAULT_HOST = exports.DEFAULT_PORT = void 0;
4
+ exports.preferredPort = preferredPort;
1
5
  /**
2
6
  * 常量配置 (env 解析在这里集中, 不散在 main 流程)
3
7
  */
4
- import { app } from 'electron';
5
- export const DEFAULT_PORT = 54188;
8
+ const electron_1 = require("electron");
9
+ exports.DEFAULT_PORT = 54188;
6
10
  /** Hard-pin to loopback; LAN exposure must be explicit. */
7
- export const DEFAULT_HOST = '127.0.0.1';
8
- export const WEB_SERVER_STARTUP_TIMEOUT_MS = 15_000;
9
- export const MAIN_WINDOW_DEFAULT = { width: 1200, height: 800 };
10
- export const MAIN_WINDOW_MIN = { width: 800, height: 600 };
11
- export function preferredPort() {
11
+ exports.DEFAULT_HOST = '127.0.0.1';
12
+ exports.WEB_SERVER_STARTUP_TIMEOUT_MS = 15_000;
13
+ exports.MAIN_WINDOW_DEFAULT = { width: 1200, height: 800 };
14
+ exports.MAIN_WINDOW_MIN = { width: 800, height: 600 };
15
+ function preferredPort() {
12
16
  const raw = process.env.ELECTRON_PORT || process.env.PORT;
13
17
  const n = parseInt(raw || '', 10);
14
- return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT;
18
+ return Number.isFinite(n) && n > 0 && n < 65536 ? n : exports.DEFAULT_PORT;
15
19
  }
16
- export const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
20
+ exports.isDev = process.env.NODE_ENV === 'development' || !electron_1.app.isPackaged;
21
+ //# sourceMappingURL=config.js.map
@@ -1,25 +1,61 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerDialogIpc = registerDialogIpc;
1
37
  /**
2
38
  * 文件 dialog 桥 (open / save / dir) + 安全的 fs 桥 (read / write / exists)
3
39
  *
4
40
  * 5MB read 上限保护 — 渲染进程直接 fs.readFile 没法做限制, 走主进程就有界
5
41
  * 所有 handler 解析 event.sender 拿到 window, 让 dialog 模态在该窗口上
6
42
  */
7
- import { BrowserWindow, dialog, ipcMain } from 'electron';
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { log } from './logger';
43
+ const electron_1 = require("electron");
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const logger_1 = require("./logger");
11
47
  const MAX_READ_BYTES = 5 * 1024 * 1024; // 5MB
12
48
  function windowFor(event) {
13
- return BrowserWindow.fromWebContents(event.sender);
49
+ return electron_1.BrowserWindow.fromWebContents(event.sender);
14
50
  }
15
51
  function resolveSafe(target) {
16
52
  // 不去硬限制路径 — user 给 renderer 暴露 fs 已经信任了, 这里只 normalize
17
53
  return path.resolve(target);
18
54
  }
19
- export function registerDialogIpc() {
20
- ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
55
+ function registerDialogIpc() {
56
+ electron_1.ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
21
57
  const win = windowFor(event);
22
- const result = await dialog.showOpenDialog(win, {
58
+ const result = await electron_1.dialog.showOpenDialog(win, {
23
59
  title: opts.title,
24
60
  defaultPath: opts.defaultPath,
25
61
  filters: opts.filters,
@@ -27,25 +63,25 @@ export function registerDialogIpc() {
27
63
  });
28
64
  return { canceled: result.canceled, filePaths: result.filePaths };
29
65
  });
30
- ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
66
+ electron_1.ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
31
67
  const win = windowFor(event);
32
- const result = await dialog.showSaveDialog(win, {
68
+ const result = await electron_1.dialog.showSaveDialog(win, {
33
69
  title: opts.title,
34
70
  defaultPath: opts.defaultPath,
35
71
  filters: opts.filters,
36
72
  });
37
73
  return { canceled: result.canceled, filePath: result.filePath };
38
74
  });
39
- ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
75
+ electron_1.ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
40
76
  const win = windowFor(event);
41
- const result = await dialog.showOpenDialog(win, {
77
+ const result = await electron_1.dialog.showOpenDialog(win, {
42
78
  title: opts.title,
43
79
  defaultPath: opts.defaultPath,
44
80
  properties: ['openDirectory', 'createDirectory'],
45
81
  });
46
82
  return { canceled: result.canceled, filePaths: result.filePaths };
47
83
  });
48
- ipcMain.handle('fs:read-text-file', async (_event, opts) => {
84
+ electron_1.ipcMain.handle('fs:read-text-file', async (_event, opts) => {
49
85
  const target = resolveSafe(opts.path);
50
86
  const stat = fs.statSync(target);
51
87
  if (stat.size > MAX_READ_BYTES) {
@@ -53,12 +89,12 @@ export function registerDialogIpc() {
53
89
  }
54
90
  return fs.readFileSync(target, { encoding: opts.encoding ?? 'utf8' });
55
91
  });
56
- ipcMain.handle('fs:write-text-file', async (_event, opts) => {
92
+ electron_1.ipcMain.handle('fs:write-text-file', async (_event, opts) => {
57
93
  const target = resolveSafe(opts.path);
58
94
  fs.mkdirSync(path.dirname(target), { recursive: true });
59
95
  fs.writeFileSync(target, opts.content, { encoding: opts.encoding ?? 'utf8' });
60
96
  });
61
- ipcMain.handle('fs:path-exists', async (_event, opts) => {
97
+ electron_1.ipcMain.handle('fs:path-exists', async (_event, opts) => {
62
98
  try {
63
99
  fs.accessSync(resolveSafe(opts.path));
64
100
  return true;
@@ -67,5 +103,6 @@ export function registerDialogIpc() {
67
103
  return false;
68
104
  }
69
105
  });
70
- log('dialog + fs IPC handlers registered');
106
+ (0, logger_1.log)('dialog + fs IPC handlers registered');
71
107
  }
108
+ //# sourceMappingURL=dialogs.js.map