@zhushanwen/pi-todo 0.7.0 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-todo",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI-driven todo list for Pi — stateful task management with session persistence and /todos command.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { buildGui, type Todo } from "../model";
4
4
 
5
- describe("buildGui", () => {
6
- it("maps 3 statuses to list-tree with correct icons", () => {
5
+ describe("buildGui(v1.1 meta head 架构)", () => {
6
+ it("内容根 = numbered list-tree:行首序号由 ListTree 渲染,label 纯文本(无 #N 前缀)", () => {
7
7
  const todos: Todo[] = [
8
8
  { id: 1, text: "pending task", status: "pending" },
9
9
  { id: 2, text: "active task", status: "in_progress" },
@@ -12,20 +12,54 @@ describe("buildGui", () => {
12
12
  const gui = buildGui(todos);
13
13
  expect(gui.v).toBe(1);
14
14
  expect(gui.component.type).toBe("list-tree");
15
+ expect(gui.component.props.numbered).toBe(true);
15
16
  const items = gui.component.props.items;
16
17
  expect(items).toHaveLength(3);
17
- // pending → dot, no status(guiResult 的 stripUndefined 删除 undefined 键)
18
- expect(items[0]).toMatchObject({ icon: "dot", label: "#1: pending task", depth: 0 });
19
- expect(items[0]).not.toHaveProperty("status");
20
- // in_progress circle, running
21
- expect(items[1]).toMatchObject({ icon: "circle", label: "#2: active task", status: "running", depth: 0 });
22
- // completed check, done
23
- expect(items[2]).toMatchObject({ icon: "check", label: "#3: done task", status: "done", depth: 0 });
18
+ // pending → status(guiResult 的 stripUndefined 删除 undefined 键),无 icon(状态由圆点单一表达)
19
+ expect(items[0]).toEqual({ label: "pending task", depth: 0 });
20
+ // in_progress → running
21
+ expect(items[1]).toEqual({ label: "active task", status: "running", depth: 0 });
22
+ // completed done
23
+ expect(items[2]).toEqual({ label: "done task", status: "done", depth: 0 });
24
24
  });
25
25
 
26
- it("empty todos empty list-tree", () => {
27
- const gui = buildGui([]);
28
- expect(gui.component.props.items).toEqual([]);
26
+ it("meta:title=Todo,progress=current/total 计数(head 渲染,body 不再有 progress-bar)", () => {
27
+ const todos: Todo[] = [
28
+ { id: 1, text: "a", status: "completed" },
29
+ { id: 2, text: "b", status: "in_progress" },
30
+ { id: 3, text: "c", status: "pending" },
31
+ ];
32
+ const gui = buildGui(todos);
33
+ expect(gui.meta).toEqual({
34
+ title: "Todo",
35
+ status: "running",
36
+ progress: { current: 1, total: 3 },
37
+ });
38
+ });
39
+
40
+ it("全部完成 → meta.status=done", () => {
41
+ const todos: Todo[] = [
42
+ { id: 1, text: "a", status: "completed" },
43
+ { id: 2, text: "b", status: "completed" },
44
+ ];
45
+ expect(buildGui(todos).meta).toEqual({
46
+ title: "Todo",
47
+ status: "done",
48
+ progress: { current: 2, total: 2 },
49
+ });
29
50
  });
30
51
 
52
+ it("有 pending 无 in_progress → status=idle;empty todos → 无 progress", () => {
53
+ const pendingOnly: Todo[] = [{ id: 1, text: "a", status: "pending" }];
54
+ expect(buildGui(pendingOnly).meta).toEqual({
55
+ title: "Todo",
56
+ status: "idle",
57
+ progress: { current: 0, total: 1 },
58
+ });
59
+ expect(buildGui([]).meta).toEqual({ title: "Todo", status: "idle" });
60
+ // 空 list:numbered 仍开(items 空,无行渲染)
61
+ const emptyGui = buildGui([]);
62
+ expect(emptyGui.component.type).toBe("list-tree");
63
+ expect(emptyGui.component.props.items).toEqual([]);
64
+ });
31
65
  });
@@ -1,10 +1,13 @@
1
- // Schema 强约束回归(T4/TC3/TC4):TodoParams discriminated union(按 action),
2
- // 每个分支只声明自己的参数且 additionalProperties:false。用 typebox Value.Check
3
- // 验证:缺失必填、多余字段、已删除的 action 都在 schema 层被拒绝,不依赖运行时 throw。
1
+ // Schema 顶层合规回归(OpenAI function calling:parameters 顶层必须是 type:"object",
2
+ // 顶层 union 会被严格网关 400 拒绝整个会话启动)。
4
3
  //
5
- // Value.Check 而非 ajv:typebox 自带 Value 校验器与其 schema 语义一致;
6
- // spike 确认 Value.Check 与 plain ajv 对本 schema 的拒绝结论一致(ajv 的
7
- // discriminator:true 选项会编译失败,故不依赖该选项)。
4
+ // 扁平化后 TodoParams 是单一 Type.Object + action 字段级 union(参考 scheduler
5
+ // ScheduleControlParams)。语义变更:所有非 action 字段都是 Optional,缺失必填
6
+ // (如 {action:'add'} 缺 texts、delete 缺 ids)不再被 schema 拒绝——改由 handler 运行时
7
+ // 校验(见 tool-detectors.test.ts)。schema 仍强约束:action 枚举、status 枚举、
8
+ // additionalProperties:false(拒绝未知字段)。
9
+ //
10
+ // 选 Value.Check 而非 ajv:typebox 自带 Value 校验器与其 schema 语义一致。
8
11
 
9
12
  import { describe, expect, it } from "vitest";
10
13
 
@@ -12,9 +15,21 @@ import { Value } from "typebox/value";
12
15
 
13
16
  import { TodoParams } from "../tool";
14
17
 
15
- describe("TodoParams discriminated union schema", () => {
18
+ describe("TodoParams 扁平 schema(顶层 type:object 合规)", () => {
19
+ describe("顶层合规(OpenAI function calling)", () => {
20
+ it("type === object(非顶层 union)", () => {
21
+ expect(TodoParams.type).toBe("object");
22
+ });
23
+ it("无顶层 anyOf(discriminated union 已消除)", () => {
24
+ expect(TodoParams.anyOf).toBeUndefined();
25
+ });
26
+ it("additionalProperties: false", () => {
27
+ expect(TodoParams.additionalProperties).toBe(false);
28
+ });
29
+ });
30
+
16
31
  describe("合法 payload 通过", () => {
17
- it("list(无参)", () => {
32
+ it("list", () => {
18
33
  expect(Value.Check(TodoParams, { action: "list" })).toBe(true);
19
34
  });
20
35
  it("add + texts", () => {
@@ -31,34 +46,18 @@ describe("TodoParams discriminated union schema", () => {
31
46
  });
32
47
  });
33
48
 
34
- describe("TC4: 缺失必填被 schema 拒绝", () => {
35
- it("add texts", () => {
36
- expect(Value.Check(TodoParams, { action: "add" })).toBe(false);
37
- });
38
- it("update 缺 id 且缺 updates", () => {
39
- expect(Value.Check(TodoParams, { action: "update" })).toBe(false);
40
- });
41
- it("delete 缺 ids", () => {
42
- expect(Value.Check(TodoParams, { action: "delete" })).toBe(false);
49
+ describe("action 枚举强约束", () => {
50
+ it("缺 action 被拒绝", () => {
51
+ expect(Value.Check(TodoParams, {})).toBe(false);
43
52
  });
44
- });
45
-
46
- describe("TC3: 已删除的 clear action 被拒绝", () => {
47
- it("clear 不在 action 枚举内", () => {
53
+ it("未知 action(clear 不在 union 内)被拒绝", () => {
48
54
  expect(Value.Check(TodoParams, { action: "clear" })).toBe(false);
49
55
  });
50
56
  });
51
57
 
52
- describe("额外属性被拒绝(additionalProperties:false)", () => {
53
- it("TC7: add 同时传 text+texts(text 是多余字段)→ 拒绝", () => {
54
- // schema 层拒绝双形陷阱;handler 层另有 defense-in-depth throw(见 tool-detectors)
55
- expect(Value.Check(TodoParams, { action: "add", texts: ["y"], text: "x" })).toBe(false);
56
- });
57
- it("list 携带多余 texts → 拒绝", () => {
58
- expect(Value.Check(TodoParams, { action: "list", texts: ["x"] })).toBe(false);
59
- });
60
- it("update 单条携带多余 ids → 拒绝", () => {
61
- expect(Value.Check(TodoParams, { action: "update", id: 1, ids: [1] })).toBe(false);
58
+ describe("额外字段被拒绝(additionalProperties:false)", () => {
59
+ it("list 携带未知字段 foo 拒绝", () => {
60
+ expect(Value.Check(TodoParams, { action: "list", foo: 1 })).toBe(false);
62
61
  });
63
62
  });
64
63
 
@@ -66,11 +65,24 @@ describe("TodoParams discriminated union schema", () => {
66
65
  it("合法三态 status 通过", () => {
67
66
  expect(Value.Check(TodoParams, { action: "update", id: 1, status: "completed" })).toBe(true);
68
67
  });
69
- it("TC2: cancelled 不再合法", () => {
68
+ it("cancelled 不在 VALID_STATUSES → 拒绝", () => {
70
69
  expect(Value.Check(TodoParams, { action: "update", id: 1, status: "cancelled" })).toBe(false);
71
70
  });
72
- it("非法 status 被拒绝", () => {
71
+ it("非法 status → 拒绝", () => {
73
72
  expect(Value.Check(TodoParams, { action: "update", id: 1, status: "banana" })).toBe(false);
74
73
  });
75
74
  });
75
+
76
+ describe("缺失必填 / 双形陷阱降级为 handler 运行时校验", () => {
77
+ // 扁平化后 {action:"add"} 缺 texts 不再被 schema 拒绝(texts 是 Optional)。
78
+ // 必填报错改由 handler 运行时校验,见 tool-detectors.test.ts。
79
+ it("{action:'add'} 缺 texts → schema 放行(handler 校验)", () => {
80
+ expect(Value.Check(TodoParams, { action: "add" })).toBe(true);
81
+ });
82
+ // 双形陷阱(add 同时传 text+texts)从 schema 层降级为运行时 handler 检测,
83
+ // 见 tool-detectors.test.ts。
84
+ it("{action:'add', texts:['y'], text:'x'} 双形 → schema 放行(handler 检测)", () => {
85
+ expect(Value.Check(TodoParams, { action: "add", texts: ["y"], text: "x" })).toBe(true);
86
+ });
87
+ });
76
88
  });
@@ -14,7 +14,7 @@
14
14
  import { describe, expect, it } from "vitest";
15
15
 
16
16
  import { createTodoSessionState } from "../state";
17
- import { handleAdd, handleDelete } from "../tool";
17
+ import { handleAdd, handleDelete, handleSingleUpdate } from "../tool";
18
18
 
19
19
  describe("handleAdd — text/texts dual-form detection", () => {
20
20
  it("triggers dual-form error when singular 'text' used instead of 'texts'", () => {
@@ -66,3 +66,17 @@ describe("handleDelete — id/ids dual-form detection", () => {
66
66
  expect(() => handleDelete(state, { action: "delete", ids: [1] })).not.toThrow();
67
67
  });
68
68
  });
69
+
70
+ describe("handleSingleUpdate — id/status/text required guards", () => {
71
+ it("throws 'requires id' when id missing", () => {
72
+ const state = createTodoSessionState();
73
+ expect(() => handleSingleUpdate(state, { action: "update" })).toThrow(/requires id/);
74
+ });
75
+
76
+ it("throws 'at least status or text' when id given but status+text missing", () => {
77
+ const state = createTodoSessionState();
78
+ expect(() => handleSingleUpdate(state, { action: "update", id: 1 })).toThrow(
79
+ /at least status or text/,
80
+ );
81
+ });
82
+ });
@@ -1,31 +1,33 @@
1
1
  /**
2
- * executeTodoAction handler 级测试 —— 覆盖 `if (ctx.mode === "rpc")` 分支
3
- * 和非 RPC 模式不附加 __gui__ 的路径。对齐 ask-user R-1~R-7 handler 级范式。
2
+ * executeTodoAction handler 级测试 —— M17 后的两条路径:
3
+ * 1. tool result __gui__(全模式统一——状态展示不再进 details)
4
+ * 2. refreshDisplay GUI widget 推送(rpc 推 marker 编码 / tui 推纯文本行)
4
5
  *
5
6
  * 策略:executeTodoAction 未导出,通过 registerTodoTool + mock pi 捕获
6
- * 已注册 tool,再以不同 ctx.mode 调 execute。每个用例新建 state(隔离),
7
- * 无模块级状态需重置。
7
+ * 已注册 tool,再以不同 ctx.mode 调 execute。setup 第三参传真实
8
+ * makeRefreshDisplay(state)(与 index.ts 工厂共用同一实现,不测复制品)。
9
+ * 每个用例新建 state(隔离),无模块级状态需重置。
8
10
  */
9
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { describe, expect, it } from "vitest";
13
+ import { GUI_WIDGET_MARKER } from "@xyz-agent/extension-protocol";
14
+ import { describe, expect, it, vi, type Mock } from "vitest";
12
15
 
16
+ import { makeRefreshDisplay } from "../index";
13
17
  import { createTodoSessionState, type TodoSessionState } from "../state";
14
18
  import { registerTodoTool } from "../tool";
15
19
 
16
20
  // ── Types for the registered tool ───────────────────────
17
21
  type TestMode = "tui" | "rpc" | "json" | "print";
18
22
 
23
+ type SetWidgetFn = (name: string, content: string[] | undefined) => void;
24
+
19
25
  interface ExecuteResult {
20
26
  content: Array<{ type: "text"; text: string }>;
21
27
  details: {
22
28
  action: string;
23
29
  todos: Array<{ id: number; text: string; status: string }>;
24
30
  nextId: number;
25
- __gui__?: {
26
- v: number;
27
- component: { type: string; props: { items: unknown[] } };
28
- };
29
31
  };
30
32
  }
31
33
 
@@ -47,7 +49,7 @@ interface MockPi {
47
49
  registerTool(tool: RegisteredTool): void;
48
50
  }
49
51
 
50
- /** 捕获注册的 tool,返回 + 暴露 state 供断言。 */
52
+ /** 捕获注册的 tool,返回 + 暴露 state 供断言。refreshDisplay 传真实实现(makeRefreshDisplay)。 */
51
53
  function setup(): { tool: RegisteredTool; state: TodoSessionState } {
52
54
  const state = createTodoSessionState();
53
55
  const pi: MockPi = {
@@ -55,7 +57,7 @@ function setup(): { tool: RegisteredTool; state: TodoSessionState } {
55
57
  this.tool = tool;
56
58
  },
57
59
  };
58
- registerTodoTool(pi as unknown as ExtensionAPI, state, () => {});
60
+ registerTodoTool(pi as unknown as ExtensionAPI, state, makeRefreshDisplay(state));
59
61
  if (!pi.tool) throw new Error("registerTodoTool did not register a tool");
60
62
  return { tool: pi.tool, state };
61
63
  }
@@ -76,95 +78,65 @@ const stubTheme = {
76
78
  getBashModeBorderColor: () => (text: string) => text,
77
79
  } as unknown as Theme;
78
80
 
79
- /** RPC 模式 ctx:hasUI=falserefreshDisplay 调用 ui.theme/setStatus/setWidget。 */
80
- const makeRpcCtx = () => ({
81
- mode: "rpc" as const,
82
- hasUI: false,
83
- ui: {
84
- theme: stubTheme,
85
- setStatus: () => {},
86
- setWidget: () => {},
87
- },
88
- });
81
+ /** 构造指定 mode ctx,setWidget vi.fn 供断言(refreshDisplay 推送出口)。 */
82
+ function makeCtx(mode: TestMode, hasUI: boolean): {
83
+ ctx: { mode: TestMode; hasUI: boolean; ui: { theme: Theme; setStatus: Mock; setWidget: Mock<SetWidgetFn> } };
84
+ setWidget: Mock<SetWidgetFn>;
85
+ } {
86
+ const setWidget = vi.fn<SetWidgetFn>();
87
+ return {
88
+ ctx: {
89
+ mode,
90
+ hasUI,
91
+ ui: { theme: stubTheme, setStatus: vi.fn(), setWidget },
92
+ },
93
+ setWidget,
94
+ };
95
+ }
96
+
97
+ /** RPC 模式 ctx:hasUI=false。 */
98
+ const makeRpcCtx = () => makeCtx("rpc", false);
89
99
 
90
100
  /** TUI 模式 ctx:hasUI=true。 */
91
- const makeTuiCtx = () => ({
92
- mode: "tui" as const,
93
- hasUI: true,
94
- ui: {
95
- theme: stubTheme,
96
- setStatus: () => {},
97
- setWidget: () => {},
98
- },
99
- });
101
+ const makeTuiCtx = () => makeCtx("tui", true);
102
+
103
+ // ── tool result:无 __gui__(M17 后全模式统一)─────────
100
104
 
101
- // ── RPC 模式:附加 __gui__ ────────────────────────────
105
+ describe("executeTodoAction tool result __gui__(全模式)", () => {
106
+ const MODES: TestMode[] = ["rpc", "tui", "json", "print"];
102
107
 
103
- describe("executeTodoAction RPC mode attaches __gui__", () => {
104
- it("R-1: rpc + add → details.__gui__ exists, type is list-tree", async () => {
108
+ it.each(MODES)("%s + add details __gui__ 字段,仍含 action/todos/nextId", async (mode) => {
105
109
  const { tool } = setup();
106
110
  const result = await tool.execute(
107
111
  "id",
108
- { action: "add", texts: ["task A", "task B"] },
112
+ { action: "add", texts: ["task A"] },
109
113
  undefined,
110
114
  undefined,
111
- makeRpcCtx(),
115
+ makeCtx(mode, mode === "tui").ctx,
112
116
  );
113
- expect(result.details.__gui__).toBeDefined();
114
- expect(result.details.__gui__!.v).toBe(1);
115
- expect(result.details.__gui__!.component.type).toBe("list-tree");
116
- // 两条新增 todo 反映在 items 中
117
- const items = result.details.__gui__!.component.props.items as Array<{
118
- label: string;
119
- icon: string;
120
- }>;
121
- expect(items).toHaveLength(2);
122
- expect(items[0]).toMatchObject({ label: "#1: task A", icon: "dot" });
123
- expect(items[1]).toMatchObject({ label: "#2: task B", icon: "dot" });
117
+ expect("__gui__" in result.details).toBe(false);
118
+ // details 仍带原生文本路径数据(todos / nextId)
119
+ expect(result.details.action).toBe("add");
120
+ expect(result.details.todos).toHaveLength(1);
121
+ expect(result.content[0].text).toContain("Added");
124
122
  });
125
123
 
126
- it("R-2: rpc + list → __gui__ reflects current todo state", async () => {
124
+ it.each(MODES)("%s + list → details __gui__ 字段,文本内容可读", async (mode) => {
127
125
  const { tool, state } = setup();
128
- // 预置状态(绕开 add,直接构造 todos)
129
- state.todos = [
130
- { id: 1, text: "pending task", status: "pending" },
131
- { id: 2, text: "active task", status: "in_progress" },
132
- { id: 3, text: "done task", status: "completed" },
133
- ];
134
- state.nextId = 4;
126
+ state.todos = [{ id: 1, text: "x", status: "pending" }];
127
+ state.nextId = 2;
135
128
  const result = await tool.execute(
136
129
  "id",
137
130
  { action: "list" },
138
131
  undefined,
139
132
  undefined,
140
- makeRpcCtx(),
133
+ makeCtx(mode, mode === "tui").ctx,
141
134
  );
142
- expect(result.details.__gui__).toBeDefined();
143
- expect(result.details.__gui__!.component.type).toBe("list-tree");
144
- const items = result.details.__gui__!.component.props.items as Array<{
145
- label: string;
146
- icon: string;
147
- status?: string;
148
- }>;
149
- expect(items).toHaveLength(3);
150
- // pending → dot 无 status
151
- expect(items[0]).toMatchObject({ label: "#1: pending task", icon: "dot" });
152
- expect(items[0]).not.toHaveProperty("status");
153
- // in_progress → circle / running
154
- expect(items[1]).toMatchObject({
155
- label: "#2: active task",
156
- icon: "circle",
157
- status: "running",
158
- });
159
- // completed → check / done
160
- expect(items[2]).toMatchObject({
161
- label: "#3: done task",
162
- icon: "check",
163
- status: "done",
164
- });
135
+ expect("__gui__" in result.details).toBe(false);
136
+ expect(result.content[0].text).toContain("#1");
165
137
  });
166
138
 
167
- it("R-3: rpc + update → __gui__ reflects post-update status", async () => {
139
+ it("rpc + update → details __gui__ 字段,状态变更仍生效", async () => {
168
140
  const { tool, state } = setup();
169
141
  state.todos = [{ id: 1, text: "item", status: "pending" }];
170
142
  state.nextId = 2;
@@ -173,82 +145,76 @@ describe("executeTodoAction — RPC mode attaches __gui__", () => {
173
145
  { action: "update", updates: [{ id: 1, status: "in_progress" }] },
174
146
  undefined,
175
147
  undefined,
176
- makeRpcCtx(),
148
+ makeRpcCtx().ctx,
177
149
  );
178
- expect(result.details.__gui__).toBeDefined();
179
- const items = result.details.__gui__!.component.props.items as Array<{
180
- icon: string;
181
- status?: string;
182
- }>;
183
- expect(items[0]).toMatchObject({ icon: "circle", status: "running" });
150
+ expect("__gui__" in result.details).toBe(false);
151
+ expect(result.details.todos[0]!.status).toBe("in_progress");
184
152
  });
185
153
  });
186
154
 
187
- // ── TUI 模式:不附加 __gui__ ──────────────────────────
155
+ // ── refreshDisplay:GUI widget 推送(M17,真实实现)────
188
156
 
189
- describe("executeTodoActionnon-RPC modes omit __gui__", () => {
190
- it("T-1: tui + add → details.__gui__ is undefined", async () => {
157
+ describe("refreshDisplayGUI widget 推送(setup 传真实实现)", () => {
158
+ it("G-1: rpc + add → setWidget 收到 ('todo', [GUI_WIDGET_MARKER + JSON]),解析后为 v1.1 信封(list-tree + meta)", async () => {
191
159
  const { tool } = setup();
192
- const result = await tool.execute(
193
- "id",
194
- { action: "add", texts: ["task A"] },
195
- undefined,
196
- undefined,
197
- makeTuiCtx(),
198
- );
199
- expect(result.details.__gui__).toBeUndefined();
200
- // details 仍带原生文本路径数据(todos / nextId)
201
- expect(result.details.todos).toHaveLength(1);
202
- expect(result.content[0].text).toContain("Added");
203
- });
204
-
205
- it("T-2: tui + list → details.__gui__ is undefined", async () => {
206
- const { tool, state } = setup();
207
- state.todos = [{ id: 1, text: "x", status: "pending" }];
208
- state.nextId = 2;
209
- const result = await tool.execute(
160
+ const { ctx, setWidget } = makeRpcCtx();
161
+ await tool.execute(
210
162
  "id",
211
- { action: "list" },
163
+ { action: "add", texts: ["task A", "task B"] },
212
164
  undefined,
213
165
  undefined,
214
- makeTuiCtx(),
166
+ ctx,
215
167
  );
216
- expect(result.details.__gui__).toBeUndefined();
217
- // 文本内容仍可读
218
- expect(result.content[0].text).toContain("#1");
168
+ expect(setWidget).toHaveBeenCalledTimes(1);
169
+ const [key, value] = setWidget.mock.calls[0]!;
170
+ expect(key).toBe("todo");
171
+ expect(value).toHaveLength(1);
172
+ const encoded = value![0]!;
173
+ // marker 前缀用协议常量断言(不手写编码)
174
+ expect(encoded.startsWith(GUI_WIDGET_MARKER)).toBe(true);
175
+ const parsed = JSON.parse(encoded.slice(GUI_WIDGET_MARKER.length)) as {
176
+ v: number;
177
+ component: { type: string; props: { numbered: boolean; items: Array<{ label: string }> } };
178
+ meta: { title: string; progress: { current: number; total: number } };
179
+ };
180
+ // v1.1 wire:GuiRenderResult 信封(component + meta 宿主元数据)
181
+ expect(parsed.v).toBe(1);
182
+ expect(parsed.component.type).toBe("list-tree");
183
+ expect(parsed.component.props.numbered).toBe(true);
184
+ expect(parsed.component.props.items).toHaveLength(2);
185
+ expect(parsed.component.props.items[0]).toMatchObject({ label: "task A" });
186
+ expect(parsed.component.props.items[1]).toMatchObject({ label: "task B" });
187
+ expect(parsed.meta).toMatchObject({ title: "Todo", progress: { current: 0, total: 2 } });
219
188
  });
220
189
 
221
- it("T-3: print mode + adddetails.__gui__ is undefined", async () => {
190
+ it("G-2: rpc + delete 清空列表 setWidget 收到 ('todo', undefined)(清除语义)", async () => {
222
191
  const { tool } = setup();
223
- const result = await tool.execute(
224
- "id",
225
- { action: "add", texts: ["task A"] },
226
- undefined,
227
- undefined,
228
- { mode: "print", hasUI: false, ui: { theme: stubTheme, setStatus: () => {}, setWidget: () => {} } },
229
- );
230
- expect(result.details.__gui__).toBeUndefined();
192
+ const { ctx, setWidget } = makeRpcCtx();
193
+ await tool.execute("id", { action: "add", texts: ["only"] }, undefined, undefined, ctx);
194
+ await tool.execute("id", { action: "delete", ids: [1] }, undefined, undefined, ctx);
195
+ expect(setWidget).toHaveBeenLastCalledWith("todo", undefined);
231
196
  });
232
197
 
233
- it("T-4: json mode + add → details.__gui__ is undefined", async () => {
198
+ it("G-3: tui + add → setWidget 收到纯文本行数组(无 marker 前缀)", async () => {
234
199
  const { tool } = setup();
235
- const result = await tool.execute(
236
- "id",
237
- { action: "add", texts: ["task A"] },
238
- undefined,
239
- undefined,
240
- { mode: "json", hasUI: false, ui: { theme: stubTheme, setStatus: () => {}, setWidget: () => {} } },
241
- );
242
- expect(result.details.__gui__).toBeUndefined();
200
+ const { ctx, setWidget } = makeTuiCtx();
201
+ await tool.execute("id", { action: "add", texts: ["task A"] }, undefined, undefined, ctx);
202
+ expect(setWidget).toHaveBeenCalledTimes(1);
203
+ const [, value] = setWidget.mock.calls[0]!;
204
+ expect(value!.length).toBeGreaterThan(0);
205
+ for (const line of value!) {
206
+ // isGuiCapable 外层判定生效:TUI 行不含 GUI marker 编码
207
+ expect(line.startsWith(GUI_WIDGET_MARKER)).toBe(false);
208
+ }
243
209
  });
244
210
  });
245
211
 
246
- // ── 共享 state:rpc 附加但 details.todos 是快照副本 ────
212
+ // ── 共享 state:details.todos 是快照副本 ───────────────
247
213
 
248
214
  describe("executeTodoAction — state isolation & snapshot", () => {
249
215
  it("S-1: each setup() yields independent state (no module-level leak)", async () => {
250
216
  const { tool: tool1 } = setup();
251
- await tool1.execute("id", { action: "add", texts: ["first"] }, undefined, undefined, makeRpcCtx());
217
+ await tool1.execute("id", { action: "add", texts: ["first"] }, undefined, undefined, makeRpcCtx().ctx);
252
218
  // 第二个 setup 起步,不应看到第一个的 todos
253
219
  const { tool: tool2, state: state2 } = setup();
254
220
  expect(state2.todos).toHaveLength(0);
@@ -257,29 +223,26 @@ describe("executeTodoAction — state isolation & snapshot", () => {
257
223
  { action: "list" },
258
224
  undefined,
259
225
  undefined,
260
- makeRpcCtx(),
226
+ makeRpcCtx().ctx,
261
227
  );
262
228
  expect(result.content[0].text).toBe("No todos");
263
- // 空 list 仍走 buildGui([])(rpc 分支无条件 attach)
264
- expect(result.details.__gui__).toBeDefined();
265
- expect(result.details.__gui__!.component.props.items).toEqual([]);
266
229
  });
267
230
 
268
231
  it("S-2: details.todos is a shallow array copy (splice-safe, element-shared)", async () => {
269
232
  // executeTodoAction 用 [...state.todos] 做浅拷贝:数组独立、元素共享。
270
233
  // add/delete 改数组长度时旧 details.todos 不受影响;但原地改元素会共享。
271
234
  const { tool } = setup();
272
- await tool.execute("id", { action: "add", texts: ["a", "b"] }, undefined, undefined, makeRpcCtx());
235
+ await tool.execute("id", { action: "add", texts: ["a", "b"] }, undefined, undefined, makeRpcCtx().ctx);
273
236
  const before = (await tool.execute(
274
237
  "id",
275
238
  { action: "list" },
276
239
  undefined,
277
240
  undefined,
278
- makeRpcCtx(),
241
+ makeRpcCtx().ctx,
279
242
  )).details.todos;
280
243
  expect(before).toHaveLength(2);
281
244
  // delete 改 state.todos 数组,已发出的 before 快照仍为 2 项
282
- await tool.execute("id", { action: "delete", ids: [1, 2] }, undefined, undefined, makeRpcCtx());
245
+ await tool.execute("id", { action: "delete", ids: [1, 2] }, undefined, undefined, makeRpcCtx().ctx);
283
246
  expect(before).toHaveLength(2);
284
247
  });
285
248
  });
package/src/index.ts CHANGED
@@ -14,20 +14,54 @@
14
14
  * - render.ts: 状态栏(status line)/ widget(单双列自适应)/ tool result 三层渲染
15
15
  * - component.ts: /todos 命令的 TodoListComponent TUI 视图(只读双列)
16
16
  * - commands.ts: /todos 命令注册
17
- * - index.ts(本文件): 工厂入口(创建 state + 注册 tool/command/event + refreshDisplay
17
+ * - index.ts(本文件): 工厂入口(创建 state + 注册 tool/command/event + makeRefreshDisplay
18
18
  *
19
19
  * 错误处理:handler 失败直接 throw(见 CLAUDE.md「Tool 设计」),不返回错误成功模式。
20
20
  * model 层纯函数返回 Result 对象(合法),dispatcher 拿到 error 时 throw。
21
21
  */
22
22
 
23
23
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
+ import { guiSetWidget, isGuiCapable, type GuiContext } from "@xyz-agent/extension-protocol";
24
25
 
25
26
  import { registerTodosCommand } from "./commands";
26
27
  import { registerTodoEventHandlers } from "./handlers";
28
+ import { buildGui } from "./model";
27
29
  import { renderStatusText, renderWidgetLines } from "./render";
28
- import { createTodoSessionState } from "./state";
30
+ import { createTodoSessionState, type TodoSessionState } from "./state";
29
31
  import { registerTodoTool } from "./tool";
30
32
 
33
+ // ── 刷新显示(导出供测试,生产路径与测试共用同一实现)──────
34
+
35
+ /**
36
+ * 构造依赖 TodoSessionState 的 refreshDisplay(M17 widget 面板推送)。
37
+ *
38
+ * 类型断言根因:pi 的 ExtensionContext.ui.custom 是泛型方法,参数逆变使其
39
+ * 与 GuiContext 不兼容,直接传参需断言收窄;双步 unknown 中转沿用 goal
40
+ * adapters/ports.ts setGuiWidget 同款先例。
41
+ *
42
+ * isGuiCapable 外层判定不可省略:guiSetWidget 内部无 isGui 守卫
43
+ * (extension-protocol helpers.ts 仅查 ctx.ui?.setWidget 存在性),
44
+ * TUI 模式误调会把 marker 编码行推给原生 widget 造成乱码。
45
+ */
46
+ export function makeRefreshDisplay(state: TodoSessionState): (ctx: ExtensionContext) => void {
47
+ return function refreshDisplay(ctx: ExtensionContext): void {
48
+ const statusText = renderStatusText(state.todos, ctx.ui.theme);
49
+ ctx.ui.setStatus("todo", statusText || undefined);
50
+ const isGui = isGuiCapable(ctx as unknown as GuiContext);
51
+ if (state.todos.length === 0) {
52
+ if (isGui) {
53
+ guiSetWidget(ctx as unknown as GuiContext, "todo", undefined);
54
+ } else {
55
+ ctx.ui.setWidget("todo", undefined);
56
+ }
57
+ } else if (isGui) {
58
+ guiSetWidget(ctx as unknown as GuiContext, "todo", buildGui(state.todos));
59
+ } else {
60
+ ctx.ui.setWidget("todo", renderWidgetLines(state.todos, ctx.ui.theme));
61
+ }
62
+ };
63
+ }
64
+
31
65
  // ── 扩展入口 ─────────────────────────────────────────
32
66
 
33
67
  export default function (pi: ExtensionAPI) {
@@ -37,16 +71,7 @@ export default function (pi: ExtensionAPI) {
37
71
  // 全解耦:不再暴露 pi.__todoGetList 跨扩展 API(goal 不再读 todo 状态)。
38
72
  // todo 进度由 AI 自行管理,goal 不做强制检查。
39
73
 
40
- // ── 刷新显示(依赖闭包 state) ─────────────────────
41
- function refreshDisplay(ctx: ExtensionContext): void {
42
- const statusText = renderStatusText(state.todos, ctx.ui.theme);
43
- ctx.ui.setStatus("todo", statusText || undefined);
44
- if (state.todos.length === 0) {
45
- ctx.ui.setWidget("todo", undefined);
46
- } else {
47
- ctx.ui.setWidget("todo", renderWidgetLines(state.todos, ctx.ui.theme));
48
- }
49
- }
74
+ const refreshDisplay = makeRefreshDisplay(state);
50
75
 
51
76
  // ── 注册所有 handler / tool / command ──────────────
52
77
  registerTodoEventHandlers(pi, state, refreshDisplay);
package/src/model.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  * 三态: pending → in_progress → completed
4
4
  */
5
5
 
6
- import { guiComponent, type GuiRenderResult, guiResult, type TreeItem } from "@xyz-agent/extension-protocol";
6
+ import {
7
+ type GuiRenderResult,
8
+ guiComponent,
9
+ guiResult,
10
+ type TreeItem,
11
+ type WidgetMeta,
12
+ } from "@xyz-agent/extension-protocol";
7
13
 
8
14
  // ── 数据模型 ─────────────────────────────────────────
9
15
 
@@ -17,8 +23,6 @@ export interface TodoDetails {
17
23
  action: "list" | "add" | "update" | "delete";
18
24
  todos: Todo[];
19
25
  nextId: number;
20
- /** GUI 渲染结果(仅 RPC 模式填充,前端 list-tree 渲染)。对齐 extension-protocol@0.2.0。 */
21
- __gui__?: GuiRenderResult;
22
26
  }
23
27
 
24
28
  export const VALID_STATUSES = ["pending", "in_progress", "completed"] as const;
@@ -67,34 +71,46 @@ export function migrateTodo(raw: unknown): Todo {
67
71
  // ── GUI 渲染辅助 ─────────────────────────────────────
68
72
 
69
73
  /**
70
- * 把 todos 映射为 list-tree GuiRenderResult(对齐 extension-protocol@0.2.0)。
71
- * status → icon/status 映射:
72
- * pending → dot / status
73
- * in_progress → circle / running
74
- * completed → check / done
74
+ * 把 todos 组装为 GuiRenderResult(v1.1 meta head 架构,对齐 extension-protocol@0.3.0)。
75
+ *
76
+ * - meta(标题/状态/进度)由宿主壳层渲染成唯一 head:进度计数 "N/M" + mini bar
77
+ * 替代 body 内 progress-bar(精简 body),全完成 status=done(head 绿点 + bar 变绿)。
78
+ * - 内容根 = numbered list-tree:行首弱化序号(编辑器行号范式,ListTree 渲染),
79
+ * id 不再烧进 label——update/delete 锚点由模型经 list action 获取,用户引用
80
+ * 「第 N 项」即可;状态由行尾圆点单一表达(无 icon,v6 单一信息源裁决)。
81
+ *
82
+ * status → 圆点映射:
83
+ * pending → 无圆点(常态归零)
84
+ * in_progress → running(accent)
85
+ * completed → done(success + label 弱化)
75
86
  */
76
87
  export function buildGui(todos: Todo[]): GuiRenderResult {
77
- const items: TreeItem[] = todos.map((t) => {
78
- const icon =
79
- t.status === "completed"
80
- ? "check"
81
- : t.status === "in_progress"
82
- ? "circle"
83
- : "dot"; // pending
84
- const status =
88
+ const total = todos.length;
89
+ const completed = todos.filter((t) => t.status === "completed").length;
90
+ const inProgress = todos.filter((t) => t.status === "in_progress").length;
91
+
92
+ const status: WidgetMeta["status"] =
93
+ total > 0 && completed === total ? "done" : inProgress > 0 ? "running" : "idle";
94
+
95
+ const items: TreeItem[] = todos.map((t) => ({
96
+ label: t.text,
97
+ status:
85
98
  t.status === "in_progress"
86
99
  ? "running"
87
100
  : t.status === "completed"
88
101
  ? "done"
89
- : undefined; // pending 无 status
90
- return {
91
- icon,
92
- label: `#${t.id}: ${t.text}`,
102
+ : undefined, // pending 无 status
103
+ depth: 0,
104
+ }));
105
+
106
+ return guiResult(
107
+ guiComponent("list-tree", { numbered: true, items }),
108
+ {
109
+ title: "Todo",
93
110
  status,
94
- depth: 0,
95
- };
96
- });
97
- return guiResult(guiComponent("list-tree", { items }));
111
+ progress: total > 0 ? { current: completed, total } : undefined,
112
+ },
113
+ );
98
114
  }
99
115
 
100
116
  export function getDisplayStatus(t: Todo): string {
package/src/tool.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Todo tool 注册 + execute dispatcher + 4 个 action handler。
3
3
  *
4
- * Schema 设计(T4):TodoParams discriminated union(按 action 区分),每个分支
5
- * 只声明自己的参数且 additionalProperties:false。这样缺失必填(如 {action:'add'}
6
- * texts)在 schema 层就被拒绝,不依赖运行时 handler throw。实测 typebox Value.Check
7
- * 与 ajvplain,不开 discriminator 选项)均正确拒绝;故不使用 discriminator keyword
8
- * (typebox 输出 anyOf,ajv discriminator 选项要求 oneOf 会编译失败)。
4
+ * Schema 设计(OpenAI 兼容):TodoParams 为扁平 Type.Object(顶层 type:"object",
5
+ * 满足 OpenAI function calling 规范——顶层 union 会被严格网关 400 拒绝整个会话启动)。
6
+ * action 字段是字面量 union(list/add/update/delete),其余字段全 Optional;必填校验
7
+ * (add texts、delete ids、双形陷阱 text/texts、id/ids)由 handler 运行时承担
8
+ * (见 tool-detectors.test.ts)。范式参考 scheduler ScheduleControlParams;设计
9
+ * 文档见 docs/extensions/tool-schema-openai-compat.md。
9
10
  */
10
11
 
11
12
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -15,7 +16,6 @@ import { type Static, Type } from "typebox";
15
16
 
16
17
  import {
17
18
  addTodos,
18
- buildGui,
19
19
  formatTodoList,
20
20
  type Todo,
21
21
  type TodoDetails,
@@ -25,75 +25,43 @@ import {
25
25
  import { renderTodoResult } from "./render";
26
26
  import type { TodoSessionState } from "./state";
27
27
 
28
- // ── Action 参数类型(运行时)──────────────────────────
29
- // 刻意保持为宽松 interface(全部字段可选)而非 strict discriminated union
30
- // handler 需要检测「双形陷阱」(add 同时传 text+texts 等错误输入),schema 层虽已用
31
- // additionalProperties:false 拒绝,但 handler 作为 defense-in-depth 仍需能访问/判断
32
- // 这些字段。类型严格性由 TodoParams schema(discriminated union)承担。
33
-
34
- export interface TodoActionParams {
35
- action: string;
36
- text?: string;
37
- id?: number;
38
- texts?: string[];
39
- ids?: number[];
40
- status?: string;
41
- updates?: Array<{ id: number; status?: string; text?: string }>;
42
- }
43
-
44
- // ── TodoParams schema(discriminated union by action)──────────
28
+ // ── TodoParams schema(扁平 Type.Object,OpenAI 兼容)──────────
29
+ // 顶层必须是 type:"object"(OpenAI function calling 规范——顶层 union 会被严格
30
+ // 网关 400 拒绝)。action 字段是字面量 union;其余字段全 Optional,必填校验交给
31
+ // handler(见 tool-detectors.test.ts)。TodoParamsT schema 派生,handler 签名
32
+ // 统一用它——双形陷阱检测在全 optional 类型上语义不变,且能消除 execute 里的 cast。
45
33
 
46
34
  const StatusSchema = StringEnum(VALID_STATUSES);
47
35
 
48
- const ListParams = Type.Object(
49
- { action: Type.Literal("list") },
50
- { additionalProperties: false },
51
- );
52
- const AddParams = Type.Object(
36
+ export const TodoParams = Type.Object(
53
37
  {
54
- action: Type.Literal("add"),
55
- texts: Type.Array(Type.String(), { description: "待添加的 todo 文本数组" }),
56
- },
57
- { additionalProperties: false },
58
- );
59
- const UpdateSingleParams = Type.Object(
60
- {
61
- action: Type.Literal("update"),
62
- id: Type.Number({ description: "要更新的 todo id" }),
63
- status: Type.Optional(StatusSchema),
38
+ action: Type.Union(
39
+ [Type.Literal("list"), Type.Literal("add"), Type.Literal("update"), Type.Literal("delete")],
40
+ { description: "list | add | update | delete" },
41
+ ),
64
42
  text: Type.Optional(Type.String({ description: "新文本(trim 后不可为空)" })),
65
- },
66
- { additionalProperties: false },
67
- );
68
- const UpdateBatchParams = Type.Object(
69
- {
70
- action: Type.Literal("update"),
71
- updates: Type.Array(
72
- Type.Object({
73
- id: Type.Number({ description: "要更新的 todo id" }),
74
- status: Type.Optional(StatusSchema),
75
- text: Type.Optional(Type.String({ description: "新文本(trim 后不可为空)" })),
76
- }),
77
- { description: "批量更新数组(优先于单条 id/status/text)" },
43
+ texts: Type.Optional(Type.Array(Type.String(), { description: "待添加的 todo 文本数组" })),
44
+ id: Type.Optional(Type.Number({ description: "要更新的 todo id" })),
45
+ ids: Type.Optional(Type.Array(Type.Number(), { description: "要删除的 todo id 数组" })),
46
+ status: Type.Optional(StatusSchema),
47
+ updates: Type.Optional(
48
+ Type.Array(
49
+ Type.Object(
50
+ {
51
+ id: Type.Number({ description: "要更新的 todo id" }),
52
+ status: Type.Optional(StatusSchema),
53
+ text: Type.Optional(Type.String({ description: "新文本(trim 后不可为空)" })),
54
+ },
55
+ { additionalProperties: false },
56
+ ),
57
+ { description: "批量更新数组(优先于单条 id/status/text)" },
58
+ ),
78
59
  ),
79
60
  },
80
61
  { additionalProperties: false },
81
62
  );
82
- const DeleteParams = Type.Object(
83
- {
84
- action: Type.Literal("delete"),
85
- ids: Type.Array(Type.Number(), { description: "要删除的 todo id 数组" }),
86
- },
87
- { additionalProperties: false },
88
- );
89
63
 
90
- export const TodoParams = Type.Union([
91
- ListParams,
92
- AddParams,
93
- UpdateSingleParams,
94
- UpdateBatchParams,
95
- DeleteParams,
96
- ]);
64
+ export type TodoParamsT = Static<typeof TodoParams>;
97
65
 
98
66
  // ── 4 个 action handler ──────────────────────────────
99
67
  // 错误处理约定(见 CLAUDE.md「Tool 设计」):handler 失败直接 throw,
@@ -107,7 +75,7 @@ function handleList(state: TodoSessionState): string {
107
75
  }
108
76
 
109
77
  /** add action — 失败抛错。export 供 behavioral 测试(text/texts 双形陷阱检测)。 */
110
- export function handleAdd(state: TodoSessionState, params: TodoActionParams): string {
78
+ export function handleAdd(state: TodoSessionState, params: TodoParamsT): string {
111
79
  // 双形陷阱:同时传 text 和 texts → throw(TC7)
112
80
  if (params.text !== undefined && params.texts !== undefined) {
113
81
  throw new Error('add only accepts texts array; do not also pass singular "text"');
@@ -131,7 +99,7 @@ export function handleAdd(state: TodoSessionState, params: TodoActionParams): st
131
99
  }
132
100
 
133
101
  /** update action: batch — 失败抛错 */
134
- function handleBatchUpdate(state: TodoSessionState, params: TodoActionParams): string {
102
+ function handleBatchUpdate(state: TodoSessionState, params: TodoParamsT): string {
135
103
  const r = updateTodos(state.todos, params.updates ?? []);
136
104
  if (r.error) throw new Error(r.resultText);
137
105
  state.todos = r.updatedTodos;
@@ -139,7 +107,7 @@ function handleBatchUpdate(state: TodoSessionState, params: TodoActionParams): s
139
107
  }
140
108
 
141
109
  /** update action: single — 失败抛错 */
142
- export function handleSingleUpdate(state: TodoSessionState, params: TodoActionParams): string {
110
+ export function handleSingleUpdate(state: TodoSessionState, params: TodoParamsT): string {
143
111
  if (params.id === undefined)
144
112
  throw new Error(
145
113
  'update requires id parameter. Correct: {"action":"update","id":<n>,"status":"in_progress"}',
@@ -171,14 +139,14 @@ export function handleSingleUpdate(state: TodoSessionState, params: TodoActionPa
171
139
  }
172
140
 
173
141
  /** update action: dispatcher — batch 优先于 single */
174
- function handleUpdate(state: TodoSessionState, params: TodoActionParams): string {
142
+ function handleUpdate(state: TodoSessionState, params: TodoParamsT): string {
175
143
  if (params.updates && params.updates.length > 0) return handleBatchUpdate(state, params);
176
144
  return handleSingleUpdate(state, params);
177
145
  }
178
146
 
179
147
  /** delete action — 失败抛错;部分 id 缺失则整体拒绝(原子性)。
180
148
  * export 供 behavioral 测试(id/ids 双形陷阱检测)。 */
181
- export function handleDelete(state: TodoSessionState, params: TodoActionParams): string {
149
+ export function handleDelete(state: TodoSessionState, params: TodoParamsT): string {
182
150
  if (!params.ids || params.ids.length === 0) {
183
151
  // 双形陷阱:弱模型 delete 时误用单数 id(那是 update 的字段)
184
152
  if (params.id !== undefined) {
@@ -209,7 +177,7 @@ export function handleDelete(state: TodoSessionState, params: TodoActionParams):
209
177
  // ── Dispatcher ───────────────────────────────────────
210
178
 
211
179
  function executeTodoAction(
212
- params: TodoActionParams,
180
+ params: TodoParamsT,
213
181
  state: TodoSessionState,
214
182
  ctx: ExtensionContext,
215
183
  refreshDisplay: (ctx: ExtensionContext) => void,
@@ -253,11 +221,8 @@ function executeTodoAction(
253
221
  todos: [...state.todos],
254
222
  nextId: state.nextId,
255
223
  };
256
- // RPC 模式(xyz-agent GUI)附加 __gui__,前端按 list-tree 渲染。
257
- // TUI/print/json 模式走原生文本渲染(contentText 已在 content 中)。
258
- if (ctx.mode === "rpc") {
259
- details.__gui__ = buildGui(state.todos);
260
- }
224
+ // 状态展示不再进 tool result(GUI 渲染字段已移除):GUI refreshDisplay 的
225
+ // guiSetWidget 推送(M17 widget 面板),TUI 走原生文本渲染(contentText 已在 content 中)。
261
226
  return {
262
227
  content: [{ type: "text" as const, text: contentText }],
263
228
  details,
@@ -296,9 +261,9 @@ export function registerTodoTool(
296
261
  executionMode: "sequential",
297
262
  parameters: TodoParams,
298
263
 
299
- async execute(_toolCallId: string, params: Static<typeof TodoParams>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
264
+ async execute(_toolCallId: string, params: TodoParamsT, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
300
265
  if (signal?.aborted) throw new Error("Todo call aborted by signal.");
301
- return executeTodoAction(params as TodoActionParams, state, ctx, refreshDisplay);
266
+ return executeTodoAction(params, state, ctx, refreshDisplay);
302
267
  },
303
268
 
304
269
  renderCall(args: Record<string, unknown>, theme: Theme, _context?: unknown) {