chatccc 0.2.5 → 0.2.6

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/src/index.ts CHANGED
@@ -48,13 +48,17 @@ import {
48
48
  getDefaultCwd,
49
49
  maskAppId,
50
50
  setDefaultCwd,
51
+ getRecentDirs,
52
+ addRecentDir,
53
+ sessionPrefixForTool,
54
+ toolDisplayName,
51
55
  ts,
52
56
  } from "./config.ts";
53
57
  import { printServiceDidNotStart, printServiceRunningHint } from "./exit-banner.ts";
54
58
  import {
55
59
  addReaction,
56
60
  createGroupChat,
57
- extractSessionId,
61
+ extractSessionInfo,
58
62
  getChatInfo,
59
63
  getTenantAccessToken,
60
64
  recallMessage,
@@ -66,7 +70,7 @@ import {
66
70
  verifyAllPermissions,
67
71
  reportPermissionResults,
68
72
  } from "./feishu-api.ts";
69
- import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildSessionsCard } from "./cards.ts";
73
+ import { buildHelpCard, buildStatusCard, buildProgressCard, buildCdContent, buildCdCard, buildSessionsCard } from "./cards.ts";
70
74
  import { updateCardKitCard } from "./cardkit.ts";
71
75
  import {
72
76
  MAX_PROCESSED,
@@ -78,8 +82,7 @@ import {
78
82
  resetState,
79
83
  resumeAndPrompt,
80
84
  sessionInfoMap,
81
- initAdapter,
82
- getAdapter,
85
+ getAdapterForTool,
83
86
  } from "./session.ts";
84
87
 
85
88
  // ---------------------------------------------------------------------------
@@ -97,7 +100,11 @@ function getInnerEvent(data: Evt): InnerEvent {
97
100
  return (data.event ?? data) as InnerEvent;
98
101
  }
99
102
 
100
- function extractText(message: { message_type?: string; content?: string }): string {
103
+ /**
104
+ * 将飞书消息的原始 content JSON 结构转成可读文本,保留代码块等结构信息。
105
+ * 未知类型直接返回 JSON 原文,让 AI 自行理解。
106
+ */
107
+ function formatMessageContent(message: { message_type?: string; content?: string }): string {
101
108
  const contentStr = message.content ?? "{}";
102
109
  let content: Record<string, unknown>;
103
110
  try { content = JSON.parse(contentStr); } catch { return ""; }
@@ -109,7 +116,36 @@ function extractText(message: { message_type?: string; content?: string }): stri
109
116
  text = text.replace(/&nbsp;/gi, " ");
110
117
  return text.trim();
111
118
  }
112
- return "";
119
+
120
+ if (message.message_type === "post") {
121
+ return formatPostContent(content);
122
+ }
123
+
124
+ // 其他类型(image, file, audio, media, sticker)直接给原始 JSON
125
+ return contentStr;
126
+ }
127
+
128
+ function formatPostContent(content: Record<string, unknown>): string {
129
+ const paragraphs = content.content as unknown[][];
130
+ if (!Array.isArray(paragraphs)) return "";
131
+
132
+ const parts: string[] = [];
133
+ for (const line of paragraphs) {
134
+ if (!Array.isArray(line)) continue;
135
+ for (const elem of line) {
136
+ const el = elem as Record<string, unknown>;
137
+ if (!el || typeof el !== "object") continue;
138
+ const t = typeof el.text === "string" ? el.text : "";
139
+
140
+ if (el.tag === "code_block") {
141
+ const lang = typeof el.language === "string" ? el.language : "";
142
+ parts.push("```" + lang + "\n" + t + "\n```");
143
+ } else if (el.tag === "p" || el.tag === "text") {
144
+ if (t) parts.push(t);
145
+ }
146
+ }
147
+ }
148
+ return parts.join("\n").trim();
113
149
  }
114
150
 
115
151
  // ---------------------------------------------------------------------------
@@ -139,8 +175,12 @@ function parseCardAction(data: unknown): CardActionResult | null {
139
175
  }
140
176
  if (!cmd) return null;
141
177
 
142
- const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
143
- const text = CMD_MAP[cmd] ?? "";
178
+ const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
179
+ let text = CMD_MAP[cmd] ?? "";
180
+ if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
181
+ const path = (action.value as Record<string, string>).path;
182
+ if (path) text = `/cd ${path}`;
183
+ }
144
184
  if (!text) return null;
145
185
 
146
186
  const chatId =
@@ -189,9 +229,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
189
229
  let sessionCwd: string | undefined;
190
230
  try {
191
231
  const chatInfo = await getChatInfo(cdToken, chatId);
192
- const sid = extractSessionId(chatInfo.description);
193
- if (sid) {
194
- const info = await getAdapter().getSessionInfo(sid);
232
+ const sessionInfoResult = extractSessionInfo(chatInfo.description);
233
+ if (sessionInfoResult) {
234
+ const adapter = getAdapterForTool(sessionInfoResult.tool);
235
+ const info = await adapter.getSessionInfo(sessionInfoResult.sessionId);
195
236
  sessionCwd = info?.cwd;
196
237
  }
197
238
  } catch { /* 非会话群或获取失败,不显示 */ }
@@ -224,6 +265,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
224
265
  const isUpdate = !!arg && targetDir !== currentDir;
225
266
  if (isUpdate) {
226
267
  await setDefaultCwd(targetDir);
268
+ await addRecentDir(targetDir);
227
269
  }
228
270
 
229
271
  // Read directory entries
@@ -248,12 +290,39 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
248
290
  return a.name.localeCompare(b.name);
249
291
  });
250
292
 
251
- const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
252
- await sendCardReply(cdToken, chatId, "新会话工作路径", content, "blue");
293
+ if (!arg) {
294
+ // /cd 无参数:展示卡片(含最近使用路径按钮)
295
+ const recentDirs = await getRecentDirs();
296
+ const card = buildCdCard(targetDir, withStats, recentDirs, sessionCwd);
297
+ const resp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
298
+ method: "POST",
299
+ headers: {
300
+ Authorization: `Bearer ${cdToken}`,
301
+ "Content-Type": "application/json",
302
+ },
303
+ body: JSON.stringify({ receive_id: chatId, msg_type: "interactive", content: card }),
304
+ });
305
+ const respData: Record<string, any> = await resp.json().catch(() => ({}));
306
+ console.log(`[${ts()}] [CD] card sent, code=${respData.code}, msgId=${respData.data?.message_id ?? "N/A"}, recentDirs=${recentDirs.length}`);
307
+ } else {
308
+ // /cd <path>:切换目录,发送文本卡片
309
+ const content = buildCdContent(targetDir, withStats, isUpdate, sessionCwd);
310
+ await sendCardReply(cdToken, chatId, "新会话工作路径", content, "blue");
311
+ }
253
312
  return;
254
313
  }
255
314
 
256
- if (text === "/new") {
315
+ if (text === "/new" || text.startsWith("/new ")) {
316
+ const toolArg = text.slice(5).trim();
317
+ const tool = toolArg || "claude";
318
+ const validTools = ["claude", "cursor"];
319
+ if (!validTools.includes(tool)) {
320
+ const warnToken = await getTenantAccessToken();
321
+ await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor)。`, "red");
322
+ return;
323
+ }
324
+ const toolLabel = toolDisplayName(tool);
325
+
257
326
  if (!openId) {
258
327
  console.log(`[${ts()}] [WARN] Cannot get sender open_id`);
259
328
  const warnToken = await getTenantAccessToken();
@@ -265,13 +334,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
265
334
 
266
335
  let sessionId: string;
267
336
  try {
268
- sessionId = await initClaudeSession();
269
- console.log(`[${ts()}] [STEP 1/4] Claude SDK session created: ${sessionId} → OK`);
337
+ sessionId = await initClaudeSession(tool);
338
+ console.log(`[${ts()}] [STEP 1/4] ${toolLabel} session created: ${sessionId} → OK`);
270
339
  } catch (err) {
271
340
  console.error(`[${ts()}] [STEP 1/4] FAIL: ${(err as Error).message}`);
272
341
  await sendCardReply(
273
342
  freshToken, chatId, "Error",
274
- `Failed to initialize Claude session:\n${(err as Error).message}`,
343
+ `Failed to initialize ${toolLabel} session:\n${(err as Error).message}`,
275
344
  "red"
276
345
  );
277
346
  return;
@@ -289,8 +358,9 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
289
358
 
290
359
  try {
291
360
  const initialName = `新会话-${sessionId}`;
292
- await updateChatInfo(freshToken, newChatId, initialName, `Claude Session: ${sessionId}`);
293
- console.log(`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" → OK`);
361
+ const descPrefix = sessionPrefixForTool(tool);
362
+ await updateChatInfo(freshToken, newChatId, initialName, `${descPrefix} ${sessionId}`);
363
+ console.log(`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" (${toolLabel}) → OK`);
294
364
  } catch (err) {
295
365
  console.error(`[${ts()}] [STEP 3/4] FAIL: ${(err as Error).message}`);
296
366
  await sendCardReply(freshToken, chatId, "Error", `Group created but rename failed:\n${(err as Error).message}`, "yellow");
@@ -298,9 +368,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
298
368
  }
299
369
 
300
370
  const cwd = await getDefaultCwd();
371
+ const adapter = getAdapterForTool(tool);
301
372
  await sendCardReply(
302
- freshToken, newChatId, "Claude Session Ready",
303
- `群聊已创建,这是你的 Claude 会话群。\n\n**Session ID:** ${sessionId}\n**工作目录:** \`${cwd}\`\n\n直接在这里发消息即可与 Claude 对话。\n\n发送 **/sessions** 查看所有会话状态。`,
373
+ freshToken, newChatId, `${toolLabel} Session Ready`,
374
+ `群聊已创建,这是你的 **${toolLabel}** 会话群。\n\n**Session ID:** ${sessionId}\n**工作目录:** \`${cwd}\`\n\n直接在这里发消息即可与 ${toolLabel} 对话。\n\n发送 **/sessions** 查看所有会话状态。`,
304
375
  "green"
305
376
  );
306
377
 
@@ -313,10 +384,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
313
384
  const token = await getTenantAccessToken();
314
385
  const chatInfo = await getChatInfo(token, chatId);
315
386
  const description = chatInfo.description;
316
- const sessionId = extractSessionId(description);
387
+ const sessionInfo = extractSessionInfo(description);
317
388
 
318
- if (sessionId) {
319
- console.log(`[${ts()}] [RESUME] 克劳德会话群 detected, session=${sessionId}`);
389
+ if (sessionInfo) {
390
+ const sessionId = sessionInfo.sessionId;
391
+ const descriptionTool = sessionInfo.tool;
392
+ const toolLabel = toolDisplayName(descriptionTool);
393
+ console.log(`[${ts()}] [RESUME] ${toolLabel} session group detected, session=${sessionId} tool=${descriptionTool}`);
320
394
 
321
395
  const freshToken = await getTenantAccessToken();
322
396
 
@@ -352,6 +426,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
352
426
  const isActive = running && !running.stopped;
353
427
  const statusText = [
354
428
  `**Session ID:** \`${status?.sessionId ?? sessionId}\``,
429
+ `**工具:** ${toolLabel}`,
355
430
  `**状态:** ${isActive ? "🟢 运行中" : "⚪ 空闲"}`,
356
431
  `**已对话轮数:** ${status?.turnCount ?? 0}`,
357
432
  `**模型:** ${status?.model ?? anthropicConfigDisplay(CLAUDE_MODEL)}`,
@@ -390,6 +465,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
390
465
  turnCount: s.turnCount,
391
466
  elapsedSeconds: s.active ? Math.floor((now - s.startTime) / 1000) : null,
392
467
  model: s.model,
468
+ tool: s.tool,
393
469
  }));
394
470
  const card = buildSessionsCard(cardData);
395
471
  const sessionsResp = await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
@@ -434,13 +510,14 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
434
510
  }
435
511
 
436
512
  try {
437
- await resumeAndPrompt(sessionId, text, freshToken, chatId, msgTimestamp);
513
+ await resumeAndPrompt(sessionId, text, freshToken, chatId, msgTimestamp, descriptionTool);
438
514
  console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
439
515
  } catch (err) {
440
516
  console.error(`[${ts()}] [RESUME] FAIL: ${(err as Error).message}`);
517
+ fileLog.flush();
441
518
  await sendCardReply(
442
519
  freshToken, chatId, "Error",
443
- `Failed to resume Claude session:\n${(err as Error).message}`,
520
+ `Failed to resume ${toolLabel} session:\n${(err as Error).message}`,
444
521
  "red"
445
522
  );
446
523
  }
@@ -580,7 +657,7 @@ async function main(): Promise<void> {
580
657
  }
581
658
  }
582
659
 
583
- const text = extractText(message);
660
+ const text = formatMessageContent(message);
584
661
  const sender = event.sender;
585
662
  const openId = sender?.sender_id?.open_id ?? "";
586
663
  const chatId = message.chat_id ?? "";
@@ -649,7 +726,6 @@ async function main(): Promise<void> {
649
726
  });
650
727
 
651
728
  if (USE_LOCAL) {
652
- initAdapter();
653
729
  console.log(`\n[启动 6/7] 本地区 relay 模式:正在连接 ${LOCAL_RELAY_URL} …`);
654
730
  console.log(" 若失败:请先在 SDK 模式下启动主进程,或确认本机中继已在该地址监听。");
655
731
  let localRelayOpened = false;
@@ -673,7 +749,7 @@ async function main(): Promise<void> {
673
749
  const event = getInnerEvent(data);
674
750
  const message = event.message;
675
751
  if (!message) return;
676
- const text = extractText(message);
752
+ const text = formatMessageContent(message);
677
753
  const openId = event.sender?.sender_id?.open_id ?? "";
678
754
  const chatId = message.chat_id ?? "";
679
755
  appendChatLog(chatId, openId, text);
@@ -695,13 +771,12 @@ async function main(): Promise<void> {
695
771
  });
696
772
  } else {
697
773
  resetState();
698
- initAdapter();
699
774
 
700
775
  const wsClient = new WSClient({
701
776
  appId: APP_ID,
702
777
  appSecret: APP_SECRET,
703
- onReady: () => { resetState(); initAdapter(); },
704
- onReconnected: () => { resetState(); initAdapter(); },
778
+ onReady: () => { resetState(); },
779
+ onReconnected: () => { resetState(); },
705
780
  });
706
781
 
707
782
  console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
package/src/session.ts CHANGED
@@ -1,10 +1,16 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
1
4
  import {
2
5
  CLAUDE_EFFORT,
3
6
  CLAUDE_MODEL,
7
+ SESSIONS_FILE,
8
+ addRecentDir,
4
9
  anthropicConfigDisplay,
5
10
  fileLog,
6
11
  getDefaultCwd,
7
12
  isSdkAnthropicDefault,
13
+ toolDisplayName,
8
14
  ts,
9
15
  } from "./config.ts";
10
16
  import { buildProgressCard, getToolEmoji, truncateContent } from "./cards.ts";
@@ -17,6 +23,7 @@ import { sendTextReply } from "./feishu-api.ts";
17
23
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
18
24
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
19
25
  import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
26
+ import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
20
27
 
21
28
  // ---------------------------------------------------------------------------
22
29
  // Shared state (imported by index.ts)
@@ -46,6 +53,7 @@ export const sessionInfoMap = new Map<string, {
46
53
  startTime: number;
47
54
  model: string;
48
55
  effort: string;
56
+ tool: string;
49
57
  }>();
50
58
 
51
59
  export function resetState(): void {
@@ -60,23 +68,67 @@ export function resetState(): void {
60
68
  }
61
69
 
62
70
  // ---------------------------------------------------------------------------
63
- // Adapter singleton
71
+ // Adapter: 按 tool 创建并缓存
64
72
  // ---------------------------------------------------------------------------
65
73
 
66
- let adapter: ToolAdapter | null = null;
74
+ const adapterCache = new Map<string, ToolAdapter>();
67
75
 
68
- export function initAdapter(): ToolAdapter {
69
- adapter = createClaudeAdapter({
70
- model: CLAUDE_MODEL,
71
- effort: CLAUDE_EFFORT,
72
- isDefault: isSdkAnthropicDefault,
73
- });
76
+ export function getAdapterForTool(tool: string): ToolAdapter {
77
+ const cached = adapterCache.get(tool);
78
+ if (cached) return cached;
79
+
80
+ let adapter: ToolAdapter;
81
+ if (tool === "cursor") {
82
+ adapter = createCursorAdapter();
83
+ } else {
84
+ adapter = createClaudeAdapter({
85
+ model: CLAUDE_MODEL,
86
+ effort: CLAUDE_EFFORT,
87
+ isDefault: isSdkAnthropicDefault,
88
+ });
89
+ }
90
+ adapterCache.set(tool, adapter);
74
91
  return adapter;
75
92
  }
76
93
 
77
- export function getAdapter(): ToolAdapter {
78
- if (!adapter) throw new Error("Adapter not initialized. Call initAdapter() first.");
79
- return adapter;
94
+ // ---------------------------------------------------------------------------
95
+ // Session tool persistence (.claude/sessions.json)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ interface SessionToolRecord {
99
+ tool: string;
100
+ createdAt: number;
101
+ }
102
+
103
+ async function loadSessionTools(): Promise<Record<string, SessionToolRecord>> {
104
+ try {
105
+ const raw = await readFile(SESSIONS_FILE, "utf-8");
106
+ return JSON.parse(raw);
107
+ } catch {
108
+ return {};
109
+ }
110
+ }
111
+
112
+ async function saveSessionTools(data: Record<string, SessionToolRecord>): Promise<void> {
113
+ try {
114
+ await mkdir(dirname(SESSIONS_FILE), { recursive: true });
115
+ await writeFile(SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
116
+ } catch (err) {
117
+ console.error(`[${ts()}] Failed to save sessions.json: ${(err as Error).message}`);
118
+ fileLog.flush();
119
+ }
120
+ }
121
+
122
+ export async function saveSessionTool(sessionId: string, tool: string): Promise<void> {
123
+ const data = await loadSessionTools();
124
+ data[sessionId] = { tool, createdAt: Date.now() };
125
+ await saveSessionTools(data);
126
+ }
127
+
128
+ export async function getSessionTool(sessionId: string): Promise<string | null> {
129
+ const data = await loadSessionTools();
130
+ const record = data[sessionId];
131
+ return record?.tool ?? null;
80
132
  }
81
133
 
82
134
  // ---------------------------------------------------------------------------
@@ -153,16 +205,21 @@ export function accumulateBlockContent(
153
205
  // Claude session management
154
206
  // ---------------------------------------------------------------------------
155
207
 
156
- export async function initClaudeSession(): Promise<string> {
208
+ export async function initClaudeSession(tool: string): Promise<string> {
157
209
  const cwd = await getDefaultCwd();
210
+ const adapter = getAdapterForTool(tool);
158
211
  console.log(
159
- `[${ts()}] [STEP 1/5] Creating ${getAdapter().displayName} session (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
212
+ `[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
160
213
  );
161
214
 
162
- const result = await getAdapter().createSession(cwd);
215
+ const result = await adapter.createSession(cwd);
163
216
  const sessionId = result.sessionId;
164
217
  console.log(`[${ts()}] → sessionId: ${sessionId}`);
165
218
 
219
+ await saveSessionTool(sessionId, tool);
220
+
221
+ await addRecentDir(cwd);
222
+
166
223
  return sessionId;
167
224
  }
168
225
 
@@ -171,12 +228,14 @@ export async function resumeAndPrompt(
171
228
  userText: string,
172
229
  token: string,
173
230
  chatId: string,
174
- msgTimestamp: number
231
+ msgTimestamp: number,
232
+ tool: string,
175
233
  ): Promise<void> {
176
- const info = await getAdapter().getSessionInfo(sessionId);
234
+ const adapter = getAdapterForTool(tool);
235
+ const info = await adapter.getSessionInfo(sessionId);
177
236
  const cwd = info?.cwd ?? (await getDefaultCwd());
178
237
  console.log(
179
- `[${ts()}] Resuming ${getAdapter().displayName} session: ${sessionId} (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
238
+ `[${ts()}] Resuming ${adapter.displayName} session: ${sessionId} (model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}, cwd=${cwd})`
180
239
  );
181
240
 
182
241
  const controller = new AbortController();
@@ -204,6 +263,7 @@ export async function resumeAndPrompt(
204
263
  startTime: now,
205
264
  model: anthropicConfigDisplay(CLAUDE_MODEL),
206
265
  effort: anthropicConfigDisplay(CLAUDE_EFFORT),
266
+ tool,
207
267
  });
208
268
 
209
269
  let cardId: string | null = null;
@@ -304,7 +364,7 @@ export async function resumeAndPrompt(
304
364
  }
305
365
 
306
366
  try {
307
- for await (const unifiedMsg of getAdapter().prompt(sessionId, userText, cwd, controller.signal)) {
367
+ for await (const unifiedMsg of adapter.prompt(sessionId, userText, cwd, controller.signal)) {
308
368
  for (const block of unifiedMsg.blocks) {
309
369
  accumulateBlockContent(block, state);
310
370
 
@@ -425,6 +485,7 @@ export function getAllSessionsStatus(): Array<{
425
485
  startTime: number;
426
486
  model: string;
427
487
  effort: string;
488
+ tool: string;
428
489
  }> {
429
490
  const result: Array<{
430
491
  chatId: string;
@@ -434,6 +495,7 @@ export function getAllSessionsStatus(): Array<{
434
495
  startTime: number;
435
496
  model: string;
436
497
  effort: string;
498
+ tool: string;
437
499
  }> = [];
438
500
  for (const [chatId, info] of sessionInfoMap) {
439
501
  const active = chatSessionMap.get(chatId);
@@ -445,6 +507,7 @@ export function getAllSessionsStatus(): Array<{
445
507
  startTime: info.startTime,
446
508
  model: info.model,
447
509
  effort: info.effort,
510
+ tool: info.tool,
448
511
  });
449
512
  }
450
513
  return result;