@foxden-app/foxclaw 0.12.0 → 0.13.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/antigravity/account_ui.d.ts +34 -0
  3. package/dist/antigravity/account_ui.js +354 -0
  4. package/dist/antigravity/adapter.js +1 -1
  5. package/dist/antigravity/auth.d.ts +4 -1
  6. package/dist/antigravity/auth.js +17 -5
  7. package/dist/antigravity/controller.d.ts +3 -108
  8. package/dist/antigravity/controller.js +2 -2129
  9. package/dist/antigravity/conversation_ui.d.ts +56 -0
  10. package/dist/antigravity/conversation_ui.js +509 -0
  11. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  12. package/dist/antigravity/ui_callbacks.js +6 -0
  13. package/dist/bridge/unified_bridge.d.ts +114 -0
  14. package/dist/bridge/unified_bridge.js +1343 -0
  15. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  16. package/dist/channels/bridge_messaging_router.js +15 -4
  17. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  18. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  19. package/dist/channels/text_actions.d.ts +14 -0
  20. package/dist/channels/text_actions.js +48 -0
  21. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  22. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  23. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  24. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  25. package/dist/codex_app/activity.d.ts +62 -0
  26. package/dist/codex_app/activity.js +418 -0
  27. package/dist/codex_app/adapter.d.ts +2 -2
  28. package/dist/codex_app/adapter.js +55 -25
  29. package/dist/codex_app/backend_ui.d.ts +4 -0
  30. package/dist/codex_app/backend_ui.js +48 -0
  31. package/dist/codex_app/client.js +0 -12
  32. package/dist/controller/access.d.ts +1 -11
  33. package/dist/controller/access.js +1 -33
  34. package/dist/controller/activity.d.ts +1 -62
  35. package/dist/controller/activity.js +2 -418
  36. package/dist/controller/auth_errors.d.ts +11 -0
  37. package/dist/controller/auth_errors.js +124 -0
  38. package/dist/controller/auth_files.d.ts +13 -0
  39. package/dist/controller/auth_files.js +156 -0
  40. package/dist/controller/auth_presentation.d.ts +77 -0
  41. package/dist/controller/auth_presentation.js +697 -0
  42. package/dist/controller/auth_quota_service.d.ts +27 -0
  43. package/dist/controller/auth_quota_service.js +151 -0
  44. package/dist/controller/controller.d.ts +30 -56
  45. package/dist/controller/controller.js +181 -3497
  46. package/dist/controller/interactions.d.ts +74 -0
  47. package/dist/controller/interactions.js +560 -0
  48. package/dist/controller/local_usage_service.d.ts +19 -0
  49. package/dist/controller/local_usage_service.js +62 -0
  50. package/dist/controller/native_panel_service.d.ts +55 -0
  51. package/dist/controller/native_panel_service.js +214 -0
  52. package/dist/controller/native_panels.d.ts +61 -0
  53. package/dist/controller/native_panels.js +784 -0
  54. package/dist/controller/shared_helpers.d.ts +53 -0
  55. package/dist/controller/shared_helpers.js +399 -0
  56. package/dist/controller/state_types.d.ts +390 -0
  57. package/dist/controller/state_types.js +82 -0
  58. package/dist/controller/turn_rendering.d.ts +22 -0
  59. package/dist/controller/turn_rendering.js +288 -0
  60. package/dist/core/access.d.ts +11 -0
  61. package/dist/core/access.js +33 -0
  62. package/dist/core/attachment_files.d.ts +13 -0
  63. package/dist/core/attachment_files.js +180 -0
  64. package/dist/core/attachment_types.d.ts +22 -0
  65. package/dist/core/attachment_types.js +1 -0
  66. package/dist/core/attachments.d.ts +5 -3
  67. package/dist/core/attachments.js +2 -2
  68. package/dist/core/backend_registry.d.ts +22 -0
  69. package/dist/core/backend_registry.js +50 -0
  70. package/dist/core/backend_ui.d.ts +28 -0
  71. package/dist/core/backend_ui.js +1 -0
  72. package/dist/core/channel_events.d.ts +48 -0
  73. package/dist/core/channel_events.js +1 -0
  74. package/dist/core/channel_port.d.ts +52 -8
  75. package/dist/core/engine_spi.d.ts +19 -4
  76. package/dist/core/listener_scope.d.ts +11 -0
  77. package/dist/core/listener_scope.js +10 -0
  78. package/dist/core/message_text.d.ts +2 -0
  79. package/dist/core/message_text.js +28 -0
  80. package/dist/core/network_errors.d.ts +1 -0
  81. package/dist/core/network_errors.js +23 -0
  82. package/dist/core/orchestrator.d.ts +53 -24
  83. package/dist/core/orchestrator.js +855 -507
  84. package/dist/core/queued_engine_input.d.ts +12 -0
  85. package/dist/core/queued_engine_input.js +16 -0
  86. package/dist/core/runtime_supervisor.d.ts +19 -0
  87. package/dist/core/runtime_supervisor.js +45 -0
  88. package/dist/core/scope_operations.d.ts +7 -0
  89. package/dist/core/scope_operations.js +19 -0
  90. package/dist/core/stream_preview.js +21 -9
  91. package/dist/dsh/adapter.js +18 -5
  92. package/dist/dsh/backend.d.ts +7 -0
  93. package/dist/dsh/backend.js +21 -0
  94. package/dist/dsh/runtime.d.ts +0 -1
  95. package/dist/dsh/runtime.js +4 -13
  96. package/dist/dsh/ui.d.ts +8 -8
  97. package/dist/dsh/ui.js +1 -1
  98. package/dist/main.js +55 -43
  99. package/dist/opencode/adapter.js +28 -9
  100. package/dist/opencode/controller.js +1 -1
  101. package/dist/store/channel_inbox.d.ts +14 -0
  102. package/dist/store/channel_inbox.js +21 -0
  103. package/dist/store/database.d.ts +7 -3
  104. package/dist/store/database.js +7 -2
  105. package/dist/store/task_journal.d.ts +32 -0
  106. package/dist/store/task_journal.js +70 -0
  107. package/dist/telegram/api.d.ts +2 -2
  108. package/dist/telegram/api.js +10 -28
  109. package/dist/telegram/gateway.d.ts +13 -27
  110. package/dist/telegram/gateway.js +86 -43
  111. package/dist/telegram/media.d.ts +2 -34
  112. package/dist/telegram/media.js +1 -180
  113. package/dist/telegram/task_preview.d.ts +13 -0
  114. package/dist/telegram/task_preview.js +92 -0
  115. package/dist/telegram/task_result.d.ts +2 -0
  116. package/dist/telegram/task_result.js +12 -0
  117. package/dist/telegram/text.d.ts +1 -1
  118. package/dist/telegram/text.js +2 -27
  119. package/dist/types.d.ts +1 -1
  120. package/docs/release.md +13 -11
  121. package/docs/zh/architecture-review.md +106 -0
  122. package/docs/zh/release.md +13 -11
  123. package/docs/zh/telegram-experience.md +40 -0
  124. package/package.json +5 -4
@@ -0,0 +1,56 @@
1
+ import type { AppLocale } from '../types.js';
2
+ import { AntigravitySubagentTracker } from './subagents.js';
3
+ import type { AppConfig } from '../config.js';
4
+ import type { BridgeStore } from '../store/database.js';
5
+ import type { Logger } from '../logger.js';
6
+ import type { BackendUiHost } from '../core/backend_ui.js';
7
+ import type { IEngineAdapter } from '../core/engine_spi.js';
8
+ import type { AntigravityConversationManager } from './conversations.js';
9
+ interface PanelUi {
10
+ sendMessage: BackendUiHost['sendMessage'];
11
+ editMessage: BackendUiHost['editMessage'];
12
+ scheduleStalePanelDeletion: BackendUiHost['scheduleStalePanelDeletion'];
13
+ }
14
+ interface ExecutionView {
15
+ syncCurrentBackendSettings(scopeId: string): void;
16
+ hasActiveTurn(scopeId: string): boolean;
17
+ getAdapterForScope(scopeId: string): IEngineAdapter;
18
+ }
19
+ export interface AntigravityWatcher {
20
+ scopeId: string;
21
+ conversationId: string;
22
+ transcriptPath: string;
23
+ fileOffset: number;
24
+ remainder: string;
25
+ timer: NodeJS.Timeout | null;
26
+ stopped: boolean;
27
+ messageId: number | null;
28
+ currentToolLines: string[];
29
+ lastContentPreview: string;
30
+ subagentTracker: AntigravitySubagentTracker;
31
+ }
32
+ export declare class AntigravityConversationUi {
33
+ private readonly config;
34
+ private readonly store;
35
+ private readonly logger;
36
+ private readonly conversations;
37
+ private readonly ui;
38
+ private readonly orchestrator;
39
+ private readonly watchers;
40
+ private readonly polls;
41
+ private closed;
42
+ constructor(config: AppConfig, store: BridgeStore, logger: Logger, conversations: AntigravityConversationManager, ui: PanelUi, orchestrator: ExecutionView);
43
+ sendThreadsPanel(scopeId: string, search: string, locale: AppLocale, editMessageId?: number): Promise<void>;
44
+ openThread(scopeId: string, rawTarget: string, locale: AppLocale, editMessageId?: number): Promise<void>;
45
+ watchConversation(scopeId: string, rawTarget: string, locale: AppLocale, editMessageId?: number, notify?: boolean): Promise<void>;
46
+ unwatchConversation(scopeId: string, locale: AppLocale, notify?: boolean): Promise<void>;
47
+ scheduleWatcherPoll(watcher: AntigravityWatcher): void;
48
+ getScopeLocale(scopeId: string): AppLocale;
49
+ renderWatcherProgressUpdate(watcher: AntigravityWatcher): Promise<void>;
50
+ pollWatcher(watcher: AntigravityWatcher): Promise<void>;
51
+ handleWatcherTranscriptEntry(watcher: AntigravityWatcher, entry: any): Promise<void>;
52
+ watcherFor(scopeId: string): AntigravityWatcher | undefined;
53
+ stop(): Promise<void>;
54
+ private trackPoll;
55
+ }
56
+ export {};
@@ -0,0 +1,509 @@
1
+ import { AGY_OPEN_CALLBACK_PREFIX, AGY_SETUP_CALLBACK_PREFIX, AGY_WATCH_CALLBACK_PREFIX } from './ui_callbacks.js';
2
+ import { formatAge } from './conversations.js';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { AntigravitySubagentTracker } from './subagents.js';
6
+ import { escapeTelegramHtml } from '../telegram/html.js';
7
+ import { chunkTelegramMessage } from '../telegram/text.js';
8
+ export class AntigravityConversationUi {
9
+ config;
10
+ store;
11
+ logger;
12
+ conversations;
13
+ ui;
14
+ orchestrator;
15
+ watchers = new Map();
16
+ polls = new Set();
17
+ closed = false;
18
+ constructor(config, store, logger, conversations, ui, orchestrator) {
19
+ this.config = config;
20
+ this.store = store;
21
+ this.logger = logger;
22
+ this.conversations = conversations;
23
+ this.ui = ui;
24
+ this.orchestrator = orchestrator;
25
+ }
26
+ async sendThreadsPanel(scopeId, search, locale, editMessageId) {
27
+ const boundThreadId = this.store.getBinding(scopeId)?.threadId;
28
+ const list = this.conversations.listConversations(10, search);
29
+ if (list.length === 0) {
30
+ const emptyText = locale === 'zh'
31
+ ? `📁 **Antigravity 会话列表**\n\n暂无历史会话。发送任意消息或执行 \`/new\` 即可创建。`
32
+ : `📁 **Antigravity Conversations**\n\nNo history found. Send a message or run \`/new\` to create one.`;
33
+ const emptyKb = [
34
+ [{ text: '✨ 新建会话', callback_data: 'engine:setup:new' }],
35
+ [{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }],
36
+ ];
37
+ if (editMessageId) {
38
+ await this.ui.editMessage(scopeId, editMessageId, emptyText, emptyKb);
39
+ }
40
+ else {
41
+ await this.ui.sendMessage(scopeId, emptyText, emptyKb);
42
+ }
43
+ return;
44
+ }
45
+ // Cache threads in database so `/open <idx>` works
46
+ this.store.cacheThreadList(scopeId, list.map((c, i) => ({
47
+ listIndex: i + 1,
48
+ threadId: c.conversationId,
49
+ name: c.title,
50
+ preview: c.preview,
51
+ cwd: c.workspaceDir ?? this.config.defaultCwd,
52
+ modelProvider: 'antigravity',
53
+ status: 'idle',
54
+ updatedAt: c.updatedAt,
55
+ })));
56
+ const currentWatcher = this.watchers.get(scopeId);
57
+ const watchingThreadId = currentWatcher && !currentWatcher.stopped ? currentWatcher.conversationId : null;
58
+ const lines = [
59
+ locale === 'zh' ? `📁 **Antigravity 会话列表**:` : `📁 **Antigravity Conversations**:`,
60
+ '',
61
+ ];
62
+ const keyboard = [];
63
+ list.forEach((session, idx) => {
64
+ const isCurrent = session.conversationId === boundThreadId;
65
+ const isWatching = session.conversationId === watchingThreadId;
66
+ const marker = isCurrent ? '●' : '○';
67
+ const short = session.conversationId.slice(0, 8);
68
+ const dir = session.workspaceDir ?? this.config.defaultCwd;
69
+ const age = formatAge(session.updatedAt, locale);
70
+ const watchBadge = isWatching ? ' [👁 观察中]' : '';
71
+ lines.push(`${marker} **${idx + 1}.** ${session.title}${watchBadge}`);
72
+ lines.push(` \`${short}\` · \`${dir}\` · ${age}`);
73
+ if (idx < 8) {
74
+ const dirName = path.basename(dir || this.config.defaultCwd);
75
+ const titleSnippet = session.title ? session.title.replace(/\s+/g, ' ') : 'Untitled';
76
+ const openText = `${isCurrent ? '✅ ' : ''}${idx + 1}. ${dirName}|${titleSnippet.length > 32 ? titleSnippet.slice(0, 31) + '…' : titleSnippet}`;
77
+ keyboard.push([
78
+ {
79
+ text: openText,
80
+ callback_data: `${AGY_OPEN_CALLBACK_PREFIX}${session.conversationId}`,
81
+ },
82
+ ]);
83
+ keyboard.push([
84
+ { text: '✏️', callback_data: `agy:rename:${session.conversationId}` },
85
+ { text: isWatching ? '👁 监视' : '👀', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}${session.conversationId}` },
86
+ { text: '🗑️', callback_data: `agy:archive:${session.conversationId}` },
87
+ { text: '➕', callback_data: `agy:new:${session.conversationId}` },
88
+ ]);
89
+ }
90
+ });
91
+ if (watchingThreadId) {
92
+ keyboard.push([
93
+ {
94
+ text: locale === 'zh' ? '🛑 停止观察当前会话 (/unwatch)' : '🛑 Stop watching (/unwatch)',
95
+ callback_data: `${AGY_WATCH_CALLBACK_PREFIX}stop`,
96
+ },
97
+ ]);
98
+ }
99
+ lines.push('', locale === 'zh'
100
+ ? `• 点击名称切换会话;\n• 点击快捷按钮:\`[✏️ 重命名]\` \`[👀 观察]\` \`[🗑️ 归档]\` \`[➕ 新建分支]\`;\n• 命令行:\`/open <编号>\`,\`/watch <编号>\`,\`/unwatch\`,\`/new\`。`
101
+ : `• Tap name to bind conversation;\n• Tap actions: \`[✏️ Rename]\` \`[👀 Watch]\` \`[🗑️ Archive]\` \`[➕ Fork]\`;\n• Commands: \`/open <num>\`, \`/watch <num>\`, \`/unwatch\`, \`/new\`.`);
102
+ keyboard.push([
103
+ { text: '➕ 新建', callback_data: 'engine:setup:new' },
104
+ { text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
105
+ ]);
106
+ const content = lines.join('\n');
107
+ if (editMessageId) {
108
+ await this.ui.editMessage(scopeId, editMessageId, content, keyboard);
109
+ this.ui.scheduleStalePanelDeletion(scopeId, editMessageId);
110
+ }
111
+ else {
112
+ const msgId = await this.ui.sendMessage(scopeId, content, keyboard);
113
+ this.ui.scheduleStalePanelDeletion(scopeId, msgId);
114
+ }
115
+ }
116
+ async openThread(scopeId, rawTarget, locale, editMessageId) {
117
+ if (!rawTarget) {
118
+ await this.ui.sendMessage(scopeId, locale === 'zh'
119
+ ? '用法:`/open <编号|会话ID>`。例如 `/open 1`。可使用 `/threads` 查看有效编号。'
120
+ : 'Usage: `/open <number|conversation-id>`. Use `/threads` to list.');
121
+ return;
122
+ }
123
+ const conv = this.conversations.resolveConversation(rawTarget, scopeId, this.store);
124
+ if (!conv) {
125
+ await this.ui.sendMessage(scopeId, locale === 'zh'
126
+ ? `❌ 未找到会话 \`${rawTarget}\`。请使用 \`/threads\` 查看会话列表。`
127
+ : `❌ Conversation \`${rawTarget}\` not found. Use \`/threads\` to list.`);
128
+ return;
129
+ }
130
+ const targetCwd = conv.workspaceDir || this.config.defaultCwd;
131
+ this.store.setBinding(scopeId, conv.conversationId, targetCwd);
132
+ const currentWatcher = this.watchers.get(scopeId);
133
+ let watchNoteZh = '';
134
+ let watchNoteEn = '';
135
+ if (currentWatcher && !currentWatcher.stopped) {
136
+ watchNoteZh = `\n• **当前观察**: \`${currentWatcher.conversationId.slice(0, 8)}\``;
137
+ watchNoteEn = `\n• **Watching**: \`${currentWatcher.conversationId.slice(0, 8)}\``;
138
+ }
139
+ const text = locale === 'zh'
140
+ ? `✅ **已切换绑定到既有 Antigravity 会话**\n\n` +
141
+ `• **标题**: ${conv.title}\n` +
142
+ `• **会话 ID**: \`${conv.conversationId}\`\n` +
143
+ `• **工作目录**: \`${targetCwd}\`\n` +
144
+ `• **更新时间**: ${formatAge(conv.updatedAt, 'zh')}` +
145
+ watchNoteZh +
146
+ `\n\n后续消息将在此会话继续执行。`
147
+ : `✅ **Switched to Antigravity Conversation**\n\n` +
148
+ `• **Title**: ${conv.title}\n` +
149
+ `• **ID**: \`${conv.conversationId}\`\n` +
150
+ `• **Directory**: \`${targetCwd}\`\n` +
151
+ `• **Updated**: ${formatAge(conv.updatedAt, 'en')}` +
152
+ watchNoteEn +
153
+ `\n\nNext messages will continue in this conversation.`;
154
+ const keyboard = [
155
+ [{ text: '👁 开启实时观察 (/watch)', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}${conv.conversationId}` }],
156
+ [
157
+ { text: '📁 查看其他会话', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
158
+ { text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
159
+ ],
160
+ ];
161
+ if (editMessageId) {
162
+ await this.ui.editMessage(scopeId, editMessageId, text, keyboard);
163
+ }
164
+ else {
165
+ await this.ui.sendMessage(scopeId, text, keyboard);
166
+ }
167
+ }
168
+ async watchConversation(scopeId, rawTarget, locale, editMessageId, notify = true) {
169
+ let conv = null;
170
+ if (rawTarget) {
171
+ conv = this.conversations.resolveConversation(rawTarget, scopeId, this.store);
172
+ }
173
+ else {
174
+ const boundThreadId = this.store.getBinding(scopeId)?.threadId;
175
+ if (boundThreadId) {
176
+ conv = this.conversations.getConversation(boundThreadId);
177
+ }
178
+ }
179
+ if (!conv) {
180
+ if (notify) {
181
+ await this.ui.sendMessage(scopeId, locale === 'zh'
182
+ ? `❌ 未找到指定会话进行观察。用法:\`/watch <编号|会话ID>\`,或使用 \`/threads\` 选择。`
183
+ : `❌ Conversation not found. Usage: \`/watch <num|id>\` or check \`/threads\`.`);
184
+ }
185
+ return;
186
+ }
187
+ const transcriptPath = this.conversations.getTranscriptPath(conv.conversationId);
188
+ if (!transcriptPath) {
189
+ if (notify) {
190
+ await this.ui.sendMessage(scopeId, locale === 'zh'
191
+ ? `⚠️ 会话 \`${conv.conversationId.slice(0, 8)}\` 暂无本地日志文件,无法进入实时观察模式。`
192
+ : `⚠️ No transcript log found for \`${conv.conversationId.slice(0, 8)}\`.`);
193
+ }
194
+ return;
195
+ }
196
+ // Stop existing watcher for this scope if any
197
+ const existing = this.watchers.get(scopeId);
198
+ if (existing) {
199
+ existing.stopped = true;
200
+ if (existing.timer)
201
+ clearTimeout(existing.timer);
202
+ }
203
+ // Seek to end of file initially to only tail new entries
204
+ let initialOffset = 0;
205
+ try {
206
+ if (fs.existsSync(transcriptPath)) {
207
+ initialOffset = fs.statSync(transcriptPath).size;
208
+ }
209
+ }
210
+ catch {
211
+ /* ignore */
212
+ }
213
+ const watcher = {
214
+ scopeId,
215
+ conversationId: conv.conversationId,
216
+ transcriptPath,
217
+ fileOffset: initialOffset,
218
+ remainder: '',
219
+ timer: null,
220
+ stopped: false,
221
+ messageId: null,
222
+ currentToolLines: [],
223
+ lastContentPreview: '',
224
+ subagentTracker: new AntigravitySubagentTracker(conv.conversationId, undefined, this.logger),
225
+ };
226
+ if (this.closed)
227
+ return;
228
+ this.watchers.set(scopeId, watcher);
229
+ this.store.setWatchedThread(scopeId, conv.conversationId);
230
+ if (notify) {
231
+ const keyboard = [
232
+ [{ text: '🛑 停止观察 (/unwatch)', callback_data: `${AGY_WATCH_CALLBACK_PREFIX}stop` }],
233
+ [
234
+ { text: '📁 会话列表', callback_data: `${AGY_SETUP_CALLBACK_PREFIX}threads` },
235
+ { text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
236
+ ],
237
+ ];
238
+ const text = locale === 'zh'
239
+ ? `👁 **已开启实时观察会话**\n\n` +
240
+ `• **会话标题**: ${conv.title}\n` +
241
+ `• **会话 ID**: \`${conv.conversationId}\`\n` +
242
+ `• **工作目录**: \`${conv.workspaceDir || this.config.defaultCwd}\`\n` +
243
+ `• **日志路径**: \`${transcriptPath}\`\n\n` +
244
+ `现在开始,外部终端或其他客户端在此会话产生的步骤、工具调用和回复将实时同步至 Telegram。\n\n` +
245
+ `发送 \`/unwatch\` 或点击下方按钮可随时退出观察。`
246
+ : `👁 **Started Watching Conversation**\n\n` +
247
+ `• **Title**: ${conv.title}\n` +
248
+ `• **ID**: \`${conv.conversationId}\`\n` +
249
+ `• **Directory**: \`${conv.workspaceDir || this.config.defaultCwd}\`\n` +
250
+ `• **Log**: \`${transcriptPath}\`\n\n` +
251
+ `Now monitoring new turns, tools, and responses in real-time.\n\n` +
252
+ `Send \`/unwatch\` or tap below to stop.`;
253
+ if (editMessageId) {
254
+ await this.ui.editMessage(scopeId, editMessageId, text, keyboard);
255
+ }
256
+ else {
257
+ await this.ui.sendMessage(scopeId, text, keyboard);
258
+ }
259
+ }
260
+ this.scheduleWatcherPoll(watcher);
261
+ }
262
+ async unwatchConversation(scopeId, locale, notify = true) {
263
+ this.store.setWatchedThread(scopeId, null);
264
+ const watcher = this.watchers.get(scopeId);
265
+ if (!watcher) {
266
+ if (notify) {
267
+ await this.ui.sendMessage(scopeId, locale === 'zh' ? 'ℹ️ 当前没有正在观察的会话。' : 'ℹ️ No conversation is currently being watched.');
268
+ }
269
+ return;
270
+ }
271
+ watcher.stopped = true;
272
+ if (watcher.timer)
273
+ clearTimeout(watcher.timer);
274
+ this.watchers.delete(scopeId);
275
+ if (notify) {
276
+ await this.ui.sendMessage(scopeId, locale === 'zh'
277
+ ? `🛑 **已停止观察会话**\n\n已退出只读监控模式。`
278
+ : `🛑 **Stopped Watching Conversation**\n\nExited read-only watch mode.`);
279
+ }
280
+ }
281
+ scheduleWatcherPoll(watcher) {
282
+ if (watcher.stopped || this.closed)
283
+ return;
284
+ watcher.timer = setTimeout(() => {
285
+ watcher.timer = null;
286
+ void this.trackPoll(watcher)
287
+ .catch((err) => {
288
+ this.logger.debug('antigravity.watch_poll_error', { error: String(err) });
289
+ })
290
+ .finally(() => {
291
+ if (!watcher.stopped && this.watchers.get(watcher.scopeId) === watcher) {
292
+ this.scheduleWatcherPoll(watcher);
293
+ }
294
+ });
295
+ }, 1500);
296
+ watcher.timer?.unref?.();
297
+ }
298
+ getScopeLocale(scopeId) {
299
+ return this.store.getChatSettings(scopeId)?.locale || 'zh';
300
+ }
301
+ async renderWatcherProgressUpdate(watcher) {
302
+ const locale = this.getScopeLocale(watcher.scopeId);
303
+ let messageText = `👁 <b>[Antigravity 观察中]</b>\n\n`;
304
+ if (watcher.currentToolLines.length > 0) {
305
+ const recent = watcher.currentToolLines.slice(-8);
306
+ messageText += `<blockquote expandable>🛠️ <b>主会话工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>\n\n`;
307
+ }
308
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
309
+ if (subBlock) {
310
+ messageText += `${subBlock}\n\n`;
311
+ }
312
+ const activeSubSummary = watcher.subagentTracker.getActiveSummaryLine(locale);
313
+ if (activeSubSummary) {
314
+ messageText += `⏳ <b>${escapeTelegramHtml(activeSubSummary)}</b>`;
315
+ }
316
+ else if (watcher.lastContentPreview) {
317
+ messageText += watcher.lastContentPreview.slice(-3000);
318
+ }
319
+ else {
320
+ messageText += `⏳ 正在执行中…`;
321
+ }
322
+ if (watcher.messageId) {
323
+ await this.ui.editMessage(watcher.scopeId, watcher.messageId, messageText).catch(() => { });
324
+ }
325
+ else {
326
+ watcher.messageId = await this.ui.sendMessage(watcher.scopeId, messageText).catch(() => null);
327
+ }
328
+ }
329
+ async pollWatcher(watcher) {
330
+ if (watcher.stopped)
331
+ return;
332
+ const currentBinding = this.store.getBinding(watcher.scopeId);
333
+ if (currentBinding?.threadId && currentBinding.threadId !== watcher.conversationId) {
334
+ await this.unwatchConversation(watcher.scopeId, this.getScopeLocale(watcher.scopeId), false);
335
+ return;
336
+ }
337
+ if (this.orchestrator.hasActiveTurn(watcher.scopeId)) {
338
+ try {
339
+ if (fs.existsSync(watcher.transcriptPath)) {
340
+ watcher.fileOffset = fs.statSync(watcher.transcriptPath).size;
341
+ }
342
+ }
343
+ catch {
344
+ /* ignore */
345
+ }
346
+ return;
347
+ }
348
+ try {
349
+ let hasParentLines = false;
350
+ const lines = [];
351
+ if (fs.existsSync(watcher.transcriptPath)) {
352
+ const stats = fs.statSync(watcher.transcriptPath);
353
+ if (stats.size < watcher.fileOffset) {
354
+ watcher.fileOffset = 0;
355
+ watcher.remainder = '';
356
+ }
357
+ else if (stats.size > watcher.fileOffset) {
358
+ const bytesToRead = stats.size - watcher.fileOffset;
359
+ const buffer = Buffer.alloc(bytesToRead);
360
+ const fd = fs.openSync(watcher.transcriptPath, 'r');
361
+ try {
362
+ fs.readSync(fd, buffer, 0, bytesToRead, watcher.fileOffset);
363
+ }
364
+ finally {
365
+ fs.closeSync(fd);
366
+ }
367
+ watcher.fileOffset = stats.size;
368
+ const chunk = watcher.remainder + buffer.toString('utf8');
369
+ const splitLines = chunk.split('\n');
370
+ watcher.remainder = splitLines.pop() ?? '';
371
+ lines.push(...splitLines);
372
+ hasParentLines = splitLines.length > 0;
373
+ }
374
+ }
375
+ for (const line of lines) {
376
+ const trimmed = line.trim();
377
+ if (!trimmed)
378
+ continue;
379
+ let entry;
380
+ try {
381
+ entry = JSON.parse(trimmed);
382
+ }
383
+ catch {
384
+ continue;
385
+ }
386
+ await this.handleWatcherTranscriptEntry(watcher, entry);
387
+ }
388
+ // Poll subagents
389
+ const subResult = await watcher.subagentTracker.poll();
390
+ // If subagents had new tool events or status updates while the parent has no new lines
391
+ if (subResult.hasUpdates && !hasParentLines) {
392
+ await this.renderWatcherProgressUpdate(watcher);
393
+ }
394
+ }
395
+ catch (err) {
396
+ this.logger.debug('antigravity.poll_watcher_error', { error: String(err) });
397
+ }
398
+ }
399
+ async handleWatcherTranscriptEntry(watcher, entry) {
400
+ const locale = this.getScopeLocale(watcher.scopeId);
401
+ if (entry.type === 'USER_INPUT') {
402
+ const text = typeof entry.content === 'string' ? entry.content : '';
403
+ const preview = text.length > 300 ? text.slice(0, 300) + '…' : text;
404
+ await this.ui.sendMessage(watcher.scopeId, `👤 <b>外部用户输入</b>:\n${escapeTelegramHtml(preview)}`);
405
+ watcher.messageId = null;
406
+ watcher.currentToolLines = [];
407
+ watcher.lastContentPreview = '';
408
+ return;
409
+ }
410
+ if (entry.type === 'GENERIC') {
411
+ const content = typeof entry.content === 'string' ? entry.content : '';
412
+ if (content.includes('Created the following subagents') || content.includes('conversationId')) {
413
+ const subResult = await watcher.subagentTracker.poll();
414
+ if (subResult.hasUpdates) {
415
+ await this.renderWatcherProgressUpdate(watcher);
416
+ }
417
+ }
418
+ return;
419
+ }
420
+ if (entry.type === 'PLANNER_RESPONSE') {
421
+ let hasUpdate = false;
422
+ if (Array.isArray(entry.tool_calls) && entry.tool_calls.length > 0) {
423
+ for (const tc of entry.tool_calls) {
424
+ const name = tc.name || 'tool';
425
+ let desc = '';
426
+ if (tc.args) {
427
+ const raw = tc.args.toolSummary || tc.args.toolAction || tc.args.CommandLine || tc.args.TargetFile || '';
428
+ desc = typeof raw === 'string' ? raw.replace(/^"|"$/g, '').trim() : '';
429
+ }
430
+ const toolLine = `⚙️ <code>${escapeTelegramHtml(name)}</code>${desc ? ` · <i>${escapeTelegramHtml(desc.slice(0, 60))}</i>` : ''}`;
431
+ if (!watcher.currentToolLines.includes(toolLine)) {
432
+ watcher.currentToolLines.push(toolLine);
433
+ hasUpdate = true;
434
+ }
435
+ }
436
+ }
437
+ const isFinalAnswer = entry.status === 'DONE' &&
438
+ typeof entry.content === 'string' &&
439
+ entry.content.trim().length > 0 &&
440
+ (!entry.tool_calls || entry.tool_calls.length === 0);
441
+ if (isFinalAnswer) {
442
+ if (watcher.messageId) {
443
+ let progressFinal = `✅ <b>[Antigravity 步骤已完成]</b>`;
444
+ if (watcher.currentToolLines.length > 0) {
445
+ const maxPreview = 3;
446
+ const recent = watcher.currentToolLines.slice(-maxPreview);
447
+ const hiddenCount = watcher.currentToolLines.length - recent.length;
448
+ const hiddenNote = hiddenCount > 0
449
+ ? `\n<i>(其余 ${hiddenCount} 项历史工具调用已折叠收起)</i>`
450
+ : '';
451
+ progressFinal += `\n\n<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}${hiddenNote}</blockquote>`;
452
+ }
453
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
454
+ if (subBlock) {
455
+ progressFinal += `\n\n<blockquote expandable>${subBlock}</blockquote>`;
456
+ }
457
+ await this.ui.editMessage(watcher.scopeId, watcher.messageId, progressFinal).catch(() => { });
458
+ }
459
+ const chunks = chunkTelegramMessage(entry.content.trim(), 4000);
460
+ for (const chunk of chunks) {
461
+ await this.ui.sendMessage(watcher.scopeId, chunk);
462
+ }
463
+ watcher.messageId = null;
464
+ watcher.currentToolLines = [];
465
+ watcher.lastContentPreview = '';
466
+ watcher.subagentTracker = new AntigravitySubagentTracker(watcher.conversationId, undefined, this.logger);
467
+ return;
468
+ }
469
+ if (entry.content) {
470
+ watcher.lastContentPreview = entry.content;
471
+ hasUpdate = true;
472
+ }
473
+ if (hasUpdate) {
474
+ let messageText = `👁 <b>[Antigravity 观察中]</b>\n\n`;
475
+ if (watcher.currentToolLines.length > 0) {
476
+ const recent = watcher.currentToolLines.slice(-8);
477
+ messageText += `<blockquote expandable>🛠️ <b>已调用工具 (${watcher.currentToolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>\n\n`;
478
+ }
479
+ const subBlock = watcher.subagentTracker.renderTelegramBlock(locale);
480
+ if (subBlock) {
481
+ messageText += `${subBlock}\n\n`;
482
+ }
483
+ const activeSubSummary = watcher.subagentTracker.getActiveSummaryLine(locale);
484
+ if (activeSubSummary) {
485
+ messageText += `⏳ <b>${escapeTelegramHtml(activeSubSummary)}</b>`;
486
+ }
487
+ else if (watcher.lastContentPreview) {
488
+ messageText += watcher.lastContentPreview.slice(-3000);
489
+ }
490
+ else {
491
+ messageText += `⏳ 正在执行步骤 #${entry.step_index ?? '…'}…`;
492
+ }
493
+ if (watcher.messageId) {
494
+ await this.ui.editMessage(watcher.scopeId, watcher.messageId, messageText).catch(() => { });
495
+ }
496
+ else {
497
+ watcher.messageId = await this.ui.sendMessage(watcher.scopeId, messageText).catch(() => null);
498
+ }
499
+ }
500
+ }
501
+ }
502
+ watcherFor(scopeId) { return this.watchers.get(scopeId); }
503
+ async stop() { this.closed = true; for (const watcher of this.watchers.values()) {
504
+ watcher.stopped = true;
505
+ if (watcher.timer)
506
+ clearTimeout(watcher.timer);
507
+ } this.watchers.clear(); await Promise.allSettled([...this.polls]); }
508
+ trackPoll(watcher) { const pending = this.pollWatcher(watcher); this.polls.add(pending); void pending.finally(() => this.polls.delete(pending)).catch(() => { }); return pending; }
509
+ }
@@ -0,0 +1,6 @@
1
+ export declare const AGY_MODEL_CALLBACK_PREFIX = "agy:m:";
2
+ export declare const AGY_AUTH_CALLBACK_PREFIX = "agy:a:";
3
+ export declare const AGY_EFFORT_CALLBACK_PREFIX = "agy:e:";
4
+ export declare const AGY_OPEN_CALLBACK_PREFIX = "agy:open:";
5
+ export declare const AGY_SETUP_CALLBACK_PREFIX = "agy:setup:";
6
+ export declare const AGY_WATCH_CALLBACK_PREFIX = "agy:w:";
@@ -0,0 +1,6 @@
1
+ export const AGY_MODEL_CALLBACK_PREFIX = 'agy:m:';
2
+ export const AGY_AUTH_CALLBACK_PREFIX = 'agy:a:';
3
+ export const AGY_EFFORT_CALLBACK_PREFIX = 'agy:e:';
4
+ export const AGY_OPEN_CALLBACK_PREFIX = 'agy:open:';
5
+ export const AGY_SETUP_CALLBACK_PREFIX = 'agy:setup:';
6
+ export const AGY_WATCH_CALLBACK_PREFIX = 'agy:w:';