chatccc 0.2.4 → 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/README.md +39 -14
- package/package.json +1 -1
- package/src/__tests__/adapter-interface.test.ts +152 -0
- package/src/__tests__/cards.test.ts +122 -5
- package/src/__tests__/claude-adapter.test.ts +529 -0
- package/src/__tests__/cursor-adapter.test.ts +250 -0
- package/src/__tests__/session.test.ts +147 -1
- package/src/adapters/adapter-interface.ts +127 -0
- package/src/adapters/claude-adapter.ts +258 -0
- package/src/adapters/cursor-adapter.ts +229 -0
- package/src/cards.ts +121 -22
- package/src/config.ts +63 -2
- package/src/feishu-api.ts +22 -8
- package/src/index.ts +107 -29
- package/src/session.ts +513 -455
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ChatCCC
|
|
2
2
|
|
|
3
|
-
**飞书聊天控制 Claude Code,未来支持
|
|
3
|
+
**飞书聊天控制 Claude Code / Cursor,未来支持 Codex**
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
传统的 Claude Code(尤其是使用第三方 API 的,如 DeepSeek),需要坐在电脑桌前才能用。**离开电脑就没法用了。**
|
|
10
10
|
|
|
11
|
-
ChatCCC 把 Claude Code 接入了飞书群聊:
|
|
11
|
+
ChatCCC 把 Claude Code 和 Cursor Agent 接入了飞书群聊:
|
|
12
12
|
|
|
13
|
-
- **手机上也能用 Claude Code** —— 在飞书群里发消息就等于在终端输入指令,
|
|
14
|
-
- **多会话并行** —— 一个群就是一个
|
|
15
|
-
-
|
|
13
|
+
- **手机上也能用 Claude Code / Cursor** —— 在飞书群里发消息就等于在终端输入指令,AI 的思考和回复会流式展示在群卡片里
|
|
14
|
+
- **多会话并行** —— 一个群就是一个 AI 会话,完全隔离、互不干扰,并行工作效率更高
|
|
15
|
+
- **多工具切换** —— `/new claude` 创建 Claude Code 会话,`/new cursor` 创建 Cursor 会话,各取所长
|
|
16
16
|
|
|
17
|
-
一句话:**在任何设备上打开飞书,就能让 Claude 帮你写代码、排查问题、分析项目。**
|
|
17
|
+
一句话:**在任何设备上打开飞书,就能让 Claude Code 或 Cursor 帮你写代码、排查问题、分析项目。**
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
20
|
<img src="images/img_readme_0.jpg" alt="飞书群聊中使用 ChatCCC" width="280" />
|
|
@@ -61,6 +61,28 @@ npm run dev
|
|
|
61
61
|
|
|
62
62
|
启动后机器人通过 WebSocket 连接飞书服务器,日志会写入 `logs/` 目录。
|
|
63
63
|
|
|
64
|
+
#### Cursor Agent CLI(使用 Cursor 会话时需要)
|
|
65
|
+
|
|
66
|
+
ChatCCC **不捆绑** Cursor Agent CLI,需要用户自行安装。Cursor Agent CLI 目前主要有两种来源:
|
|
67
|
+
|
|
68
|
+
1. **Cursor IDE 自带**:安装 [Cursor IDE](https://cursor.com) 后,部分版本会自带 `agent` 或 `cursor-agent` 命令
|
|
69
|
+
2. **独立安装**:Cursor 正在逐步提供独立的 CLI 安装方式,安装后 `agent` 命令会出现在系统 PATH 中
|
|
70
|
+
|
|
71
|
+
验证是否已安装:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
agent --version
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
若 `agent` 不在 PATH 中,可通过环境变量指定自定义命令:
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
CHATCCC_CURSOR_COMMAND=/path/to/agent
|
|
81
|
+
CHATCCC_CURSOR_ARGS=-p --output-format stream-json --stream-partial-output
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> **说明**:只使用 Claude Code(`/new claude` 或 `/new`)的用户无需安装 Cursor CLI。
|
|
85
|
+
|
|
64
86
|
### 2. 创建飞书应用
|
|
65
87
|
|
|
66
88
|
打开 [飞书开放平台](https://open.feishu.cn),创建一个**企业自建应用**。
|
|
@@ -168,18 +190,21 @@ CHATCCC_PORT=18081
|
|
|
168
190
|
|
|
169
191
|
### 5. 开始使用
|
|
170
192
|
|
|
171
|
-
在飞书中找到你的机器人,发送 `/new
|
|
193
|
+
在飞书中找到你的机器人,发送 `/new`(默认 Claude Code)或 `/new claude` / `/new cursor`,机器人会自动创建一个群聊并把 AI 会话绑定到该群。之后直接在群里发消息就能对话。
|
|
172
194
|
|
|
173
195
|
### 可用指令
|
|
174
196
|
|
|
175
197
|
|
|
176
|
-
| 指令
|
|
177
|
-
|
|
|
178
|
-
| `/new`
|
|
179
|
-
| `/
|
|
180
|
-
| `/
|
|
181
|
-
| `/
|
|
182
|
-
| `/
|
|
198
|
+
| 指令 | 作用 |
|
|
199
|
+
| --------------- | ---------------------------- |
|
|
200
|
+
| `/new` | 创建新的 Claude Code 会话(默认) |
|
|
201
|
+
| `/new claude` | 创建新的 Claude Code 会话 |
|
|
202
|
+
| `/new cursor` | 创建新的 Cursor 会话 |
|
|
203
|
+
| `/stop` | 停止当前正在生成的回复 |
|
|
204
|
+
| `/status` | 查看当前会话的状态(轮数、模型、上下文 token 等) |
|
|
205
|
+
| `/cd` | 查看/切换工作目录 |
|
|
206
|
+
| `/sessions` | 查看所有会话状态 |
|
|
207
|
+
| `/restart` | 重启机器人进程 |
|
|
183
208
|
|
|
184
209
|
|
|
185
210
|
---
|
package/package.json
CHANGED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
UnifiedBlock,
|
|
4
|
+
UnifiedStreamMessage,
|
|
5
|
+
UnifiedThinkingBlock,
|
|
6
|
+
UnifiedTextBlock,
|
|
7
|
+
UnifiedToolUseBlock,
|
|
8
|
+
UnifiedToolResultBlock,
|
|
9
|
+
UnifiedRedactedThinkingBlock,
|
|
10
|
+
UnifiedSearchResultBlock,
|
|
11
|
+
UnifiedCompactBoundaryBlock,
|
|
12
|
+
CreateSessionResult,
|
|
13
|
+
SessionInfo,
|
|
14
|
+
ToolAdapter,
|
|
15
|
+
} from "../adapters/adapter-interface.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 编译期类型验证:确保 UnifiedBlock 联合类型覆盖了所有预期的 block 形状
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
describe("UnifiedBlock union type coverage", () => {
|
|
22
|
+
it("accepts thinking block", () => {
|
|
23
|
+
const b: UnifiedBlock = { type: "thinking", thinking: "Let me think..." };
|
|
24
|
+
expect(b.type).toBe("thinking");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("accepts text block", () => {
|
|
28
|
+
const b: UnifiedBlock = { type: "text", text: "Hello" };
|
|
29
|
+
expect(b.type).toBe("text");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("accepts tool_use block", () => {
|
|
33
|
+
const b: UnifiedBlock = { type: "tool_use", name: "Read", input: { file_path: "/tmp" } };
|
|
34
|
+
expect(b.type).toBe("tool_use");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("accepts tool_result block with string content", () => {
|
|
38
|
+
const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "result" };
|
|
39
|
+
expect(b.type).toBe("tool_result");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("accepts tool_result block with error flag", () => {
|
|
43
|
+
const b: UnifiedBlock = { type: "tool_result", tool_use_id: "abc123", content: "error", is_error: true };
|
|
44
|
+
expect(b.is_error).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("accepts redacted_thinking block", () => {
|
|
48
|
+
const b: UnifiedBlock = { type: "redacted_thinking" };
|
|
49
|
+
expect(b.type).toBe("redacted_thinking");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("accepts search_result block", () => {
|
|
53
|
+
const b: UnifiedBlock = { type: "search_result", query: "what is TypeScript" };
|
|
54
|
+
expect(b.type).toBe("search_result");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("accepts compact_boundary block", () => {
|
|
58
|
+
const b: UnifiedBlock = { type: "compact_boundary", trigger: "auto", pre_tokens: 10000, post_tokens: 5000 };
|
|
59
|
+
expect(b.type).toBe("compact_boundary");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("accepts compact_boundary block without post_tokens", () => {
|
|
63
|
+
const b: UnifiedBlock = { type: "compact_boundary", trigger: "manual", pre_tokens: 20000 };
|
|
64
|
+
expect(b.type).toBe("compact_boundary");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// UnifiedStreamMessage
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe("UnifiedStreamMessage", () => {
|
|
73
|
+
it("accepts assistant message with blocks", () => {
|
|
74
|
+
const m: UnifiedStreamMessage = {
|
|
75
|
+
type: "assistant",
|
|
76
|
+
blocks: [
|
|
77
|
+
{ type: "thinking", thinking: "..." },
|
|
78
|
+
{ type: "text", text: "answer" },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
expect(m.type).toBe("assistant");
|
|
82
|
+
expect(m.blocks).toHaveLength(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("accepts user message with tool_result blocks", () => {
|
|
86
|
+
const m: UnifiedStreamMessage = {
|
|
87
|
+
type: "user",
|
|
88
|
+
blocks: [{ type: "tool_result", tool_use_id: "abc", content: "ok" }],
|
|
89
|
+
};
|
|
90
|
+
expect(m.type).toBe("user");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("accepts system message", () => {
|
|
94
|
+
const m: UnifiedStreamMessage = {
|
|
95
|
+
type: "system",
|
|
96
|
+
blocks: [{ type: "compact_boundary", trigger: "auto", pre_tokens: 100 }],
|
|
97
|
+
};
|
|
98
|
+
expect(m.type).toBe("system");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// CreateSessionResult
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
describe("CreateSessionResult", () => {
|
|
107
|
+
it("accepts valid result", () => {
|
|
108
|
+
const r: CreateSessionResult = { sessionId: "uuid-12345" };
|
|
109
|
+
expect(r.sessionId).toBe("uuid-12345");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// SessionInfo
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
describe("SessionInfo", () => {
|
|
118
|
+
it("accepts full info", () => {
|
|
119
|
+
const info: SessionInfo = {
|
|
120
|
+
sessionId: "abc",
|
|
121
|
+
cwd: "/home/user",
|
|
122
|
+
summary: "A session",
|
|
123
|
+
lastModified: 1234567890,
|
|
124
|
+
};
|
|
125
|
+
expect(info.sessionId).toBe("abc");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("accepts minimal info (only sessionId)", () => {
|
|
129
|
+
const info: SessionInfo = { sessionId: "minimal" };
|
|
130
|
+
expect(info.cwd).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// ToolAdapter interface 结构断言
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe("ToolAdapter interface structure", () => {
|
|
139
|
+
it("has correct property names via mock object", () => {
|
|
140
|
+
// 如果 ToolAdapter 接口的字段名或函数签名变更,该对象字面量将无法编译
|
|
141
|
+
const mock: ToolAdapter = {
|
|
142
|
+
displayName: "Test",
|
|
143
|
+
sessionDescPrefix: "Test Session:",
|
|
144
|
+
createSession: async () => ({ sessionId: "s" }),
|
|
145
|
+
prompt: async function* () {},
|
|
146
|
+
getSessionInfo: async () => undefined,
|
|
147
|
+
closeSession: async () => {},
|
|
148
|
+
};
|
|
149
|
+
expect(mock.displayName).toBe("Test");
|
|
150
|
+
expect(mock.sessionDescPrefix).toBe("Test Session:");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildProgressCard,
|
|
4
4
|
buildHelpCard,
|
|
5
5
|
buildCdContent,
|
|
6
|
+
buildCdCard,
|
|
6
7
|
buildSessionsCard,
|
|
7
8
|
buildStatusCard,
|
|
8
9
|
buildButtons,
|
|
@@ -148,9 +149,9 @@ describe("buildHelpCard", () => {
|
|
|
148
149
|
it("includes action buttons", () => {
|
|
149
150
|
const card = buildHelpCard("test");
|
|
150
151
|
const parsed = JSON.parse(card);
|
|
151
|
-
const action = parsed.elements[
|
|
152
|
+
const action = parsed.elements[2];
|
|
152
153
|
expect(action.tag).toBe("action");
|
|
153
|
-
expect(action.actions).toHaveLength(
|
|
154
|
+
expect(action.actions).toHaveLength(4);
|
|
154
155
|
});
|
|
155
156
|
});
|
|
156
157
|
|
|
@@ -199,6 +200,99 @@ describe("buildCdContent", () => {
|
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// buildCdCard
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
describe("buildCdCard", () => {
|
|
208
|
+
const entries = [
|
|
209
|
+
{ name: "src", isDir: true },
|
|
210
|
+
{ name: "README.md", isDir: false },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
it("returns valid JSON with correct schema", () => {
|
|
214
|
+
const card = buildCdCard("/home/project", entries, []);
|
|
215
|
+
const parsed = JSON.parse(card);
|
|
216
|
+
expect(parsed.schema).toBe("2.0");
|
|
217
|
+
expect(parsed.header.title.content).toBe("工作路径");
|
|
218
|
+
expect(parsed.header.template).toBe("blue");
|
|
219
|
+
expect(parsed.config.wide_screen_mode).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("shows current working directory in markdown", () => {
|
|
223
|
+
const card = buildCdCard("/home/project", entries, []);
|
|
224
|
+
const parsed = JSON.parse(card);
|
|
225
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
226
|
+
const cwdMd = mdElements.find((e: any) => e.content.includes("新会话默认工作路径"));
|
|
227
|
+
expect(cwdMd).toBeDefined();
|
|
228
|
+
expect(cwdMd.content).toContain("/home/project");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("shows sessionCwd when provided", () => {
|
|
232
|
+
const card = buildCdCard("/default", entries, [], "/session/path");
|
|
233
|
+
const parsed = JSON.parse(card);
|
|
234
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
235
|
+
const sessionMd = mdElements.find((e: any) => e.content.includes("当前会话工作路径"));
|
|
236
|
+
expect(sessionMd).toBeDefined();
|
|
237
|
+
expect(sessionMd.content).toContain("/session/path");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("does not show sessionCwd when not provided", () => {
|
|
241
|
+
const card = buildCdCard("/default", entries, []);
|
|
242
|
+
const parsed = JSON.parse(card);
|
|
243
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
244
|
+
const sessionMd = mdElements.find((e: any) => e.content.includes("当前会话工作路径"));
|
|
245
|
+
expect(sessionMd).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("shows recent dirs section with buttons", () => {
|
|
249
|
+
const recentDirs = ["/home/user/project1", "/home/user/project2"];
|
|
250
|
+
const card = buildCdCard("/current", entries, recentDirs);
|
|
251
|
+
const parsed = JSON.parse(card);
|
|
252
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
253
|
+
const recentMd = mdElements.find((e: any) => e.content.includes("最近使用过的路径"));
|
|
254
|
+
expect(recentMd).toBeDefined();
|
|
255
|
+
|
|
256
|
+
const actionElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "action");
|
|
257
|
+
expect(actionElements.length).toBeGreaterThanOrEqual(1);
|
|
258
|
+
const recentAction = actionElements.find((e: any) =>
|
|
259
|
+
e.actions.some((a: any) => a.value?.action === "cd")
|
|
260
|
+
);
|
|
261
|
+
expect(recentAction).toBeDefined();
|
|
262
|
+
expect(recentAction.actions).toHaveLength(2);
|
|
263
|
+
expect(recentAction.actions[0].value.action).toBe("cd");
|
|
264
|
+
expect(recentAction.actions[0].value.path).toBe("/home/user/project1");
|
|
265
|
+
expect(recentAction.actions[1].value.path).toBe("/home/user/project2");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("does not show recent dirs section when empty", () => {
|
|
269
|
+
const card = buildCdCard("/current", entries, []);
|
|
270
|
+
const parsed = JSON.parse(card);
|
|
271
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
272
|
+
const recentMd = mdElements.find((e: any) => e.content.includes("最近使用过的路径"));
|
|
273
|
+
expect(recentMd).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("truncates long paths in button text", () => {
|
|
277
|
+
const longPath = "/home/user/very/long/path/that/exceeds/thirty/six/chars";
|
|
278
|
+
const card = buildCdCard("/current", entries, [longPath]);
|
|
279
|
+
const parsed = JSON.parse(card);
|
|
280
|
+
const action: any = parsed.body.elements.find((e: any) => e.tag === "action");
|
|
281
|
+
const btnText = action.actions[0].text.content;
|
|
282
|
+
expect(btnText.startsWith("...")).toBe(true);
|
|
283
|
+
expect(btnText.length).toBeLessThanOrEqual(36);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("shows directory listing", () => {
|
|
287
|
+
const card = buildCdCard("/current", entries, []);
|
|
288
|
+
const parsed = JSON.parse(card);
|
|
289
|
+
const mdElements: any[] = parsed.body.elements.filter((e: any) => e.tag === "markdown");
|
|
290
|
+
const listingMd = mdElements.find((e: any) => e.content.includes("📁 src/"));
|
|
291
|
+
expect(listingMd).toBeDefined();
|
|
292
|
+
expect(listingMd.content).toContain("📄 README.md");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
202
296
|
// ---------------------------------------------------------------------------
|
|
203
297
|
// buildSessionsCard
|
|
204
298
|
// ---------------------------------------------------------------------------
|
|
@@ -208,21 +302,23 @@ describe("buildSessionsCard", () => {
|
|
|
208
302
|
const card = buildSessionsCard([]);
|
|
209
303
|
const parsed = JSON.parse(card);
|
|
210
304
|
expect(parsed.elements[0].text.content).toContain("没有会话记录");
|
|
305
|
+
expect(parsed.elements[0].text.content).toContain("/new");
|
|
211
306
|
});
|
|
212
307
|
|
|
213
308
|
it("returns valid JSON with session listing", () => {
|
|
214
309
|
const card = buildSessionsCard([
|
|
215
|
-
{ sessionId: "abc123", active: true, turnCount: 5, elapsedSeconds: 120, model: "Claude Opus 4.7" },
|
|
310
|
+
{ sessionId: "abc123", active: true, turnCount: 5, elapsedSeconds: 120, model: "Claude Opus 4.7", tool: "claude" },
|
|
216
311
|
]);
|
|
217
312
|
const parsed = JSON.parse(card);
|
|
218
313
|
expect(parsed.elements[0].text.content).toContain("共 **1** 个会话");
|
|
219
314
|
expect(parsed.elements[0].text.content).toContain("abc123");
|
|
220
315
|
expect(parsed.elements[0].text.content).toContain("🟢 活跃");
|
|
316
|
+
expect(parsed.elements[0].text.content).toContain("Claude Code");
|
|
221
317
|
});
|
|
222
318
|
|
|
223
319
|
it("shows idle status for inactive sessions", () => {
|
|
224
320
|
const card = buildSessionsCard([
|
|
225
|
-
{ sessionId: "xyz", active: false, turnCount: 0, elapsedSeconds: null, model: "Claude Sonnet 4.6" },
|
|
321
|
+
{ sessionId: "xyz", active: false, turnCount: 0, elapsedSeconds: null, model: "Claude Sonnet 4.6", tool: "claude" },
|
|
226
322
|
]);
|
|
227
323
|
const parsed = JSON.parse(card);
|
|
228
324
|
expect(parsed.elements[0].text.content).toContain("⚪ 空闲");
|
|
@@ -230,12 +326,33 @@ describe("buildSessionsCard", () => {
|
|
|
230
326
|
|
|
231
327
|
it("shows elapsed time for active sessions", () => {
|
|
232
328
|
const card = buildSessionsCard([
|
|
233
|
-
{ sessionId: "active123", active: true, turnCount: 3, elapsedSeconds: 95, model: "Claude Opus 4.7" },
|
|
329
|
+
{ sessionId: "active123", active: true, turnCount: 3, elapsedSeconds: 95, model: "Claude Opus 4.7", tool: "claude" },
|
|
234
330
|
]);
|
|
235
331
|
const parsed = JSON.parse(card);
|
|
236
332
|
expect(parsed.elements[0].text.content).toContain("1分35秒");
|
|
237
333
|
});
|
|
238
334
|
|
|
335
|
+
it("separates Claude Code and Cursor sessions", () => {
|
|
336
|
+
const card = buildSessionsCard([
|
|
337
|
+
{ sessionId: "c1", active: false, turnCount: 1, elapsedSeconds: null, model: "default", tool: "claude" },
|
|
338
|
+
{ sessionId: "c2", active: false, turnCount: 2, elapsedSeconds: null, model: "default", tool: "cursor" },
|
|
339
|
+
]);
|
|
340
|
+
const parsed = JSON.parse(card);
|
|
341
|
+
const content: string = parsed.elements[0].text.content;
|
|
342
|
+
expect(content).toContain("Claude Code 会话");
|
|
343
|
+
expect(content).toContain("Cursor 会话");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("omits Cursor section when no Cursor sessions", () => {
|
|
347
|
+
const card = buildSessionsCard([
|
|
348
|
+
{ sessionId: "c1", active: false, turnCount: 1, elapsedSeconds: null, model: "default", tool: "claude" },
|
|
349
|
+
]);
|
|
350
|
+
const parsed = JSON.parse(card);
|
|
351
|
+
const content: string = parsed.elements[0].text.content;
|
|
352
|
+
expect(content).toContain("Claude Code 会话");
|
|
353
|
+
expect(content).not.toContain("Cursor 会话");
|
|
354
|
+
});
|
|
355
|
+
|
|
239
356
|
it("includes close button", () => {
|
|
240
357
|
const card = buildSessionsCard([]);
|
|
241
358
|
const parsed = JSON.parse(card);
|