@trim21/personal-pi-extensions 0.0.186 → 0.0.188
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 +44 -0
- package/package.json +3 -2
- package/src/question.ts +167 -0
- package/src/todowrite.ts +164 -0
- package/src/todo-pendant.ts +0 -89
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
| [opencode-edit](#opencode-edit) | 替换内置 edit 工具,使用 opencode 的 schema 和匹配引擎 |
|
|
12
12
|
| [bash-default-timeout](#bash-default-timeout) | 为 bash 工具设置默认超时(180 秒) |
|
|
13
13
|
| [vision-agent](#vision-agent) | 视觉代理:主模型不支持视觉时,spawn 子 agent 识别图片 |
|
|
14
|
+
| [todowrite](#todowrite) | opencode 风格的任务列表工具,完整列表替换语义 |
|
|
15
|
+
| [question](#question) | opencode 风格的提问工具,阻塞式询问用户选择 |
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
@@ -169,6 +171,48 @@ pi -e ./src/vision-agent.ts
|
|
|
169
171
|
|
|
170
172
|
---
|
|
171
173
|
|
|
174
|
+
## todowrite
|
|
175
|
+
|
|
176
|
+
opencode 风格的任务列表工具,参数与语义和 opencode 的 [`todowrite`](https://github.com/anomalyco/opencode) 工具一致。取代原 `todo-pendant.ts` 的 widget 输出方式,改用 `details.pendant.markdown` 渲染(与 vision-agent 相同的 pendant 约定)。
|
|
177
|
+
|
|
178
|
+
- **完整列表替换语义**:模型每次调用都传完整的 todo 列表,工具整体替换当前列表
|
|
179
|
+
- **参数**:`todos: Array<{ content, status, priority }>`
|
|
180
|
+
- `status`:`pending` | `in_progress` | `completed` | `cancelled`
|
|
181
|
+
- `priority`:`high` | `medium` | `low`
|
|
182
|
+
- **持久化**:列表存进工具结果 `details.todos`,跟随会话分支自动恢复
|
|
183
|
+
- **渲染**:每次调用用完整 markdown 列表输出对应的任务(pendant 面板自动展开)
|
|
184
|
+
|
|
185
|
+
与 pi 内置 `todo` 工具(`create`/`update`/`list`/… 单条动作)不同,本工具没有单条增删改动作,模型必须每次都传完整列表。
|
|
186
|
+
|
|
187
|
+
### 使用
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pi -e ./src/todowrite.ts
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## question
|
|
196
|
+
|
|
197
|
+
opencode 风格的提问工具,参数与语义和 opencode 的 [`question`](https://github.com/anomalyco/opencode) 工具一致。阻塞式执行:工具调用挂起,等用户作答后才把答案返回给模型。
|
|
198
|
+
|
|
199
|
+
- **参数**:`questions: Array<{ question, header, options, multiple? }>`
|
|
200
|
+
- `options` 每项为 `{ label, description }`
|
|
201
|
+
- `multiple` 缺省为单选,`true` 时循环用 `ui.select` 逐个勾选直到「✓ Done」
|
|
202
|
+
- **自定义答案**:每个问题自动追加 `Type your own answer.` 选项,选中后走 `ui.input` 自由输入
|
|
203
|
+
- **返回值**:每个问题一个 label 数组(`Answer = string[]`),跳过的为空数组
|
|
204
|
+
- **输出**:与 opencode 一致 —— `User has answered your questions: "q"="a", "q2"="Unanswered"...`
|
|
205
|
+
|
|
206
|
+
交互全部走 pi 内置的 `ctx.ui.select` / `ctx.ui.input`,不写自定义 TUI 渲染;`option.description` 不显示在对话框里,仅保留在 `details` 中。
|
|
207
|
+
|
|
208
|
+
### 使用
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pi -e ./src/question.ts
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
172
216
|
## 安装
|
|
173
217
|
|
|
174
218
|
### 通过 npm/git 包
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.188",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"src/bash-default-timeout.ts",
|
|
65
65
|
"src/gh-readonly.ts",
|
|
66
66
|
"src/spawn-agent.ts",
|
|
67
|
-
"src/
|
|
67
|
+
"src/todowrite.ts",
|
|
68
|
+
"src/question.ts"
|
|
68
69
|
]
|
|
69
70
|
},
|
|
70
71
|
"lint-staged": {
|
package/src/question.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* question —— opencode 风格的提问工具
|
|
3
|
+
*
|
|
4
|
+
* 参数与语义和 opencode 的 `question` 工具一致:
|
|
5
|
+
* questions 数组,每项含 question / header / options / multiple:
|
|
6
|
+
* - options 每项为 label / description
|
|
7
|
+
* - 单选(默认):用户在选项里选一个,也可选「Type your own answer.」自由输入
|
|
8
|
+
* - 多选(multiple: true):循环用 ui.select 逐个勾选,直到「✓ Done」
|
|
9
|
+
* - 每个问题返回一个 label 数组(Answer = string[]),跳过的为空数组
|
|
10
|
+
* - 输出与 opencode 一致:
|
|
11
|
+
* User has answered your questions: "q"="a", "q2"="Unanswered"...
|
|
12
|
+
*
|
|
13
|
+
* 交互全部走 pi 内置的 ctx.ui.select / ctx.ui.input,不写自定义 TUI 渲染。
|
|
14
|
+
* 注意:ui.select 只接受 string 标签,option.description 不显示在对话框里,
|
|
15
|
+
* 仅保留在 details 中(schema 仍与 opencode 对齐)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { Type } from "typebox";
|
|
20
|
+
|
|
21
|
+
// ── constants ────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const TOOL_NAME = "question";
|
|
24
|
+
/** opencode 会自动追加的自定义答案选项;用户选中后走 ctx.ui.input */
|
|
25
|
+
export const CUSTOM_LABEL = "Type your own answer.";
|
|
26
|
+
/** 多选模式下结束勾选的哨兵选项 */
|
|
27
|
+
export const DONE_LABEL = "✓ Done";
|
|
28
|
+
|
|
29
|
+
/** 与 opencode question.txt 语义一致的描述 */
|
|
30
|
+
export const QUESTION_DESCRIPTION = [
|
|
31
|
+
"Use this tool when you need to ask the user questions during execution. This allows you to:",
|
|
32
|
+
"1. Gather user preferences or requirements",
|
|
33
|
+
"2. Clarify ambiguous instructions",
|
|
34
|
+
"3. Get decisions on implementation choices as you work",
|
|
35
|
+
"4. Offer choices to the user about what direction to take.",
|
|
36
|
+
"",
|
|
37
|
+
"Usage notes:",
|
|
38
|
+
'- A "Type your own answer" option is added automatically; don\'t include "Other" or catch-all options',
|
|
39
|
+
"- Answers are returned as arrays of labels; set multiple to true to allow selecting more than one",
|
|
40
|
+
'- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label',
|
|
41
|
+
].join("\n");
|
|
42
|
+
|
|
43
|
+
// ── types ────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface QuestionOption {
|
|
46
|
+
label: string;
|
|
47
|
+
description: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 与 schema 对齐的输入形状(multiple 可选) */
|
|
51
|
+
export interface QuestionInput {
|
|
52
|
+
question: string;
|
|
53
|
+
header: string;
|
|
54
|
+
options: QuestionOption[];
|
|
55
|
+
multiple?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 规整后的问题(multiple 已默认化为 boolean) */
|
|
59
|
+
export interface Question extends Omit<QuestionInput, "multiple"> {
|
|
60
|
+
multiple: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 每个问题的答案:选中的 label 数组(自定义输入就是单元素的 [text]) */
|
|
64
|
+
export type Answer = string[];
|
|
65
|
+
|
|
66
|
+
export interface QuestionDetails {
|
|
67
|
+
questions: Question[];
|
|
68
|
+
answers: Answer[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── schema(与 opencode 的 Question.Prompt 一致)──────────────────────────────
|
|
72
|
+
|
|
73
|
+
const optionSchema = Type.Object({
|
|
74
|
+
label: Type.String({ description: "Display text (concise, 1-5 words)" }),
|
|
75
|
+
description: Type.String({ description: "Explanation of what this choice means" }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const questionSchema = Type.Object({
|
|
79
|
+
question: Type.String({ description: "The complete question to ask" }),
|
|
80
|
+
header: Type.String({ description: "Very short label (max 30 chars)" }),
|
|
81
|
+
options: Type.Array(optionSchema, { description: "Available choices" }),
|
|
82
|
+
multiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple choices" })),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const questionParamsSchema = Type.Object({
|
|
86
|
+
questions: Type.Array(questionSchema, { description: "Questions to ask" }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── 纯函数(可测试)──────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** trim 各字段并把可选的 multiple 默认化为 false */
|
|
92
|
+
export function normalizeQuestions(questions: readonly QuestionInput[]): Question[] {
|
|
93
|
+
return questions.map((q) => ({
|
|
94
|
+
question: q.question.trim(),
|
|
95
|
+
header: q.header.trim(),
|
|
96
|
+
options: q.options.map((o) => ({ label: o.label.trim(), description: o.description.trim() })),
|
|
97
|
+
multiple: q.multiple === true,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 与 opencode 一致的输出:每个问题 "q"="a, b",空答案显示 Unanswered */
|
|
102
|
+
export function formatAnswers(questions: readonly Question[], answers: readonly Answer[]): string {
|
|
103
|
+
const formatted = questions
|
|
104
|
+
.map((q, i) => `"${q.question}"="${answers[i]?.length ? answers[i].join(", ") : "Unanswered"}"`)
|
|
105
|
+
.join(", ");
|
|
106
|
+
return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dialogTitle(q: Question): string {
|
|
110
|
+
return q.header ? `${q.header}: ${q.question}` : q.question;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function askSingle(q: Question, ctx: ExtensionContext): Promise<Answer> {
|
|
114
|
+
const title = dialogTitle(q);
|
|
115
|
+
const options = [...q.options.map((o) => o.label), CUSTOM_LABEL];
|
|
116
|
+
const choice = await ctx.ui.select(title, options);
|
|
117
|
+
if (choice === undefined) return [];
|
|
118
|
+
if (choice === CUSTOM_LABEL) {
|
|
119
|
+
const typed = await ctx.ui.input(title, "Type your answer…");
|
|
120
|
+
return typed?.trim() ? [typed.trim()] : [];
|
|
121
|
+
}
|
|
122
|
+
return [choice];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function askMultiple(q: Question, ctx: ExtensionContext): Promise<Answer> {
|
|
126
|
+
const title = dialogTitle(q);
|
|
127
|
+
const selected: string[] = [];
|
|
128
|
+
const remaining = new Set(q.options.map((o) => o.label));
|
|
129
|
+
while (remaining.size > 0) {
|
|
130
|
+
const choice = await ctx.ui.select(title, [...remaining, DONE_LABEL]);
|
|
131
|
+
if (choice === undefined || choice === DONE_LABEL) break;
|
|
132
|
+
if (!remaining.has(choice)) continue;
|
|
133
|
+
selected.push(choice);
|
|
134
|
+
remaining.delete(choice);
|
|
135
|
+
}
|
|
136
|
+
return selected;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── extension ────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export default function question(pi: ExtensionAPI) {
|
|
142
|
+
pi.registerTool<typeof questionParamsSchema, QuestionDetails>({
|
|
143
|
+
name: TOOL_NAME,
|
|
144
|
+
label: "Question",
|
|
145
|
+
description: QUESTION_DESCRIPTION,
|
|
146
|
+
parameters: questionParamsSchema,
|
|
147
|
+
// 交互式提问不能与其他工具并行弹出,逐个串行执行
|
|
148
|
+
executionMode: "sequential",
|
|
149
|
+
|
|
150
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
151
|
+
if (!ctx.hasUI) {
|
|
152
|
+
throw new Error("Cannot ask questions: interactive UI is not available in this mode");
|
|
153
|
+
}
|
|
154
|
+
const questions = normalizeQuestions(params.questions);
|
|
155
|
+
|
|
156
|
+
const answers: Answer[] = [];
|
|
157
|
+
for (const q of questions) {
|
|
158
|
+
answers.push(q.multiple ? await askMultiple(q, ctx) : await askSingle(q, ctx));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: formatAnswers(questions, answers) }],
|
|
163
|
+
details: { questions, answers },
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
package/src/todowrite.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todowrite —— opencode 风格的任务列表工具
|
|
3
|
+
*
|
|
4
|
+
* 完整列表替换语义(与 opencode 的 todowrite 工具一致):
|
|
5
|
+
* 每次调用用给定的 todos 数组整体替换当前任务列表。
|
|
6
|
+
* todos 数组,每项含 content / status / priority 三个字段:
|
|
7
|
+
* status: pending | in_progress | completed | cancelled
|
|
8
|
+
* priority: high | medium | low
|
|
9
|
+
*
|
|
10
|
+
* 状态存在工具结果 details 里(跟随会话分支),同时把任务列表渲染成
|
|
11
|
+
* pendant.markdown(与 vision-agent 相同的 pendant 约定),每次调用都用
|
|
12
|
+
* 完整的 markdown 列表输出对应的任务 —— 取代原 todo-pendant.ts 里
|
|
13
|
+
* setWidget 的 widget 输出方式。
|
|
14
|
+
*
|
|
15
|
+
* 与 pi 内置 todo 工具(create/update/list/... 动作)不同,本工具没有
|
|
16
|
+
* 单条增删改动作 —— 模型每次都要传完整的 todo 列表,语义与 opencode 对齐。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Type } from "typebox";
|
|
22
|
+
|
|
23
|
+
// ── constants ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export const TOOL_NAME = "todowrite";
|
|
26
|
+
|
|
27
|
+
export const TODO_STATUSES = ["pending", "in_progress", "completed", "cancelled"] as const;
|
|
28
|
+
export const TODO_PRIORITIES = ["high", "medium", "low"] as const;
|
|
29
|
+
|
|
30
|
+
export type TodoStatus = (typeof TODO_STATUSES)[number];
|
|
31
|
+
export type TodoPriority = (typeof TODO_PRIORITIES)[number];
|
|
32
|
+
|
|
33
|
+
/** opencode 的 todowrite 描述(语义等价),补上「整体替换」这一关键规则 */
|
|
34
|
+
export const TODOWRITE_DESCRIPTION = [
|
|
35
|
+
"Create and maintain a structured task list for the current coding session. Tracks progress, organizes multi-step work, and surfaces status to the user.",
|
|
36
|
+
"",
|
|
37
|
+
"This tool REPLACES the entire todo list: pass the full updated list of todos on every call.",
|
|
38
|
+
"",
|
|
39
|
+
"## When to use",
|
|
40
|
+
"Use proactively when:",
|
|
41
|
+
"- The task requires 3+ distinct steps or actions (not just 3 tool calls for a single conceptual step)",
|
|
42
|
+
"- The work is non-trivial and benefits from planning",
|
|
43
|
+
"- The user provides multiple tasks (numbered or comma-separated) or explicitly asks for a todo list",
|
|
44
|
+
"- New instructions arrive - capture them as todos",
|
|
45
|
+
"- You start a task - mark it `in_progress` (only one at a time) before working",
|
|
46
|
+
"- You finish a task - mark it `completed` and add any follow-ups discovered during the work",
|
|
47
|
+
"",
|
|
48
|
+
"## When NOT to use",
|
|
49
|
+
"Skip when:",
|
|
50
|
+
"- The work is a single, straightforward task (or <3 trivial steps)",
|
|
51
|
+
"- The request is purely informational or conversational",
|
|
52
|
+
"- Tracking adds no organizational value",
|
|
53
|
+
"",
|
|
54
|
+
"## States",
|
|
55
|
+
"- `pending` - not started",
|
|
56
|
+
"- `in_progress` - actively working (exactly ONE at a time)",
|
|
57
|
+
"- `completed` - finished successfully",
|
|
58
|
+
"- `cancelled` - no longer needed",
|
|
59
|
+
"",
|
|
60
|
+
"## Rules",
|
|
61
|
+
"- Update status in real time; don't batch completions",
|
|
62
|
+
"- Mark `completed` only after the required work is actually done, including any required verification. Never based on intent.",
|
|
63
|
+
"- Keep exactly one `in_progress` while work remains",
|
|
64
|
+
"- If blocked or partial, keep it `in_progress` and add a follow-up todo describing the blocker",
|
|
65
|
+
"- Preserve user-provided commands verbatim (flags, args, order)",
|
|
66
|
+
"- Items should be specific and actionable; break large work into smaller steps",
|
|
67
|
+
].join("\n");
|
|
68
|
+
|
|
69
|
+
// ── types ────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export interface TodoInfo {
|
|
72
|
+
content: string;
|
|
73
|
+
status: TodoStatus;
|
|
74
|
+
priority: TodoPriority;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TodoDetails {
|
|
78
|
+
todos: TodoInfo[];
|
|
79
|
+
pendant: { markdown: string; expanded: boolean };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── schema(与 opencode 的 Parameters 一致)──────────────────────────────────
|
|
83
|
+
|
|
84
|
+
const todoInfoSchema = Type.Object({
|
|
85
|
+
content: Type.String({ description: "Brief description of the task" }),
|
|
86
|
+
status: StringEnum(TODO_STATUSES, {
|
|
87
|
+
description: "Current status of the task: pending, in_progress, completed, cancelled",
|
|
88
|
+
}),
|
|
89
|
+
priority: StringEnum(TODO_PRIORITIES, {
|
|
90
|
+
description: "Priority level of the task: high, medium, low",
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const todowriteSchema = Type.Object({
|
|
95
|
+
todos: Type.Array(todoInfoSchema, { description: "The updated todo list" }),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── 纯函数(可测试)──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/** trim content 并返回干净副本;字段类型已由 typebox schema 推断,无需运行时校验 */
|
|
101
|
+
export function normalizeTodos(todos: readonly TodoInfo[]): TodoInfo[] {
|
|
102
|
+
return todos.map((t) => ({ ...t, content: t.content.trim() }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** opencode 返回标题里的计数:未完成(status !== completed)的任务数 */
|
|
106
|
+
export function countOpen(todos: readonly TodoInfo[]): number {
|
|
107
|
+
return todos.filter((t) => t.status !== "completed").length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 与 opencode 一致的输出:格式化的 JSON 字符串 */
|
|
111
|
+
export function serializeTodos(todos: readonly TodoInfo[]): string {
|
|
112
|
+
return JSON.stringify(todos, null, 2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const STATUS_MARK: Record<TodoStatus, string> = {
|
|
116
|
+
pending: " ",
|
|
117
|
+
in_progress: ">",
|
|
118
|
+
completed: "x",
|
|
119
|
+
cancelled: "-",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 把任务列表整理成 pendant 渲染用的 markdown。
|
|
124
|
+
* 每个任务一行,与 vision-agent 的 pendant.markdown 约定一致。
|
|
125
|
+
*/
|
|
126
|
+
export function buildTodoMarkdown(todos: readonly TodoInfo[]): string {
|
|
127
|
+
const lines = ["## Tasks", "", `**${countOpen(todos)} open · ${todos.length} total**`];
|
|
128
|
+
if (todos.length === 0) {
|
|
129
|
+
lines.push("", "_No todos_");
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
for (const t of todos) {
|
|
134
|
+
lines.push(`- [${STATUS_MARK[t.status]}] ${t.content} \`${t.priority}\``);
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── extension ────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export default function todowrite(pi: ExtensionAPI) {
|
|
142
|
+
pi.registerTool<typeof todowriteSchema, TodoDetails>({
|
|
143
|
+
name: TOOL_NAME,
|
|
144
|
+
label: "Todo Write",
|
|
145
|
+
description: TODOWRITE_DESCRIPTION,
|
|
146
|
+
parameters: todowriteSchema,
|
|
147
|
+
|
|
148
|
+
execute(_toolCallId, params) {
|
|
149
|
+
// 整体替换(opencode 语义)。
|
|
150
|
+
// 纯同步逻辑,用 Promise.resolve 满足 execute 的 Promise 返回类型。
|
|
151
|
+
const todos = normalizeTodos(params.todos);
|
|
152
|
+
return Promise.resolve({
|
|
153
|
+
content: [{ type: "text", text: serializeTodos(todos) }],
|
|
154
|
+
details: {
|
|
155
|
+
todos,
|
|
156
|
+
pendant: {
|
|
157
|
+
markdown: buildTodoMarkdown(todos),
|
|
158
|
+
expanded: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
package/src/todo-pendant.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Todo Pendant Extension
|
|
3
|
-
*
|
|
4
|
-
* Intercepts `todo` tool results and renders the task list as a widget
|
|
5
|
-
* above the editor in Pendant's UI. Compatible with pi's built-in todo
|
|
6
|
-
* tool's four-status task model (pending / in_progress / completed / deleted).
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* pi -e src/todo-pendant.ts
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Types matching pi's built-in todo tool TaskDetails
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
type TaskStatus = "pending" | "in_progress" | "completed" | "deleted";
|
|
19
|
-
|
|
20
|
-
interface Task {
|
|
21
|
-
id: number;
|
|
22
|
-
subject: string;
|
|
23
|
-
description?: string;
|
|
24
|
-
activeForm?: string;
|
|
25
|
-
status: TaskStatus;
|
|
26
|
-
blockedBy?: number[];
|
|
27
|
-
owner?: string;
|
|
28
|
-
metadata?: Record<string, unknown>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface TaskDetails {
|
|
32
|
-
action: string;
|
|
33
|
-
params: Record<string, unknown>;
|
|
34
|
-
tasks: Task[];
|
|
35
|
-
nextId: number;
|
|
36
|
-
error?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isTaskDetails(d: unknown): d is TaskDetails {
|
|
40
|
-
return typeof d === "object" && d !== null && "tasks" in d && Array.isArray(d.tasks);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Helpers
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
const STATUS_MARK = {
|
|
48
|
-
pending: " ",
|
|
49
|
-
in_progress: " ",
|
|
50
|
-
deleted: " ",
|
|
51
|
-
completed: "x",
|
|
52
|
-
} as const;
|
|
53
|
-
|
|
54
|
-
function formatTaskLine(t: Task): string {
|
|
55
|
-
const mark = STATUS_MARK[t.status];
|
|
56
|
-
const form = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : "";
|
|
57
|
-
let line = `- [${mark}] #${t.id} ${t.subject}${form}`;
|
|
58
|
-
if (t.blockedBy?.length) {
|
|
59
|
-
line += ` ⛓ ${t.blockedBy.map((id) => `#${id}`).join(", ")}`;
|
|
60
|
-
}
|
|
61
|
-
return line;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Extension
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
export default function todoPendant(pi: ExtensionAPI) {
|
|
69
|
-
pi.on("tool_result", (event, ctx) => {
|
|
70
|
-
if (event.toolName !== "todo") return;
|
|
71
|
-
|
|
72
|
-
const details = event.details;
|
|
73
|
-
if (!isTaskDetails(details) || details.tasks.length === 0) {
|
|
74
|
-
ctx.ui.setWidget("todo-pendant", undefined);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const visible = details.tasks.filter((t) => t.status !== "deleted");
|
|
79
|
-
if (visible.length === 0) {
|
|
80
|
-
ctx.ui.setWidget("todo-pendant", undefined);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
ctx.ui.setWidget(
|
|
85
|
-
"todo-pendant",
|
|
86
|
-
visible.map((t) => formatTaskLine(t)),
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
}
|