@ryan_nookpi/pi-extension-todo-write-overlay 0.2.5 → 0.3.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/README.md +20 -0
- package/index.ts +112 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ pi install npm:@ryan_nookpi/pi-extension-todo-write-overlay
|
|
|
51
51
|
|
|
52
52
|
## 사용 예
|
|
53
53
|
|
|
54
|
+
처음 목록을 만들거나 큰 폭으로 재편할 때는 `op` 없이(기본 `replace`) 전체를 보냅니다.
|
|
55
|
+
|
|
54
56
|
```json
|
|
55
57
|
{
|
|
56
58
|
"todos": [
|
|
@@ -61,6 +63,24 @@ pi install npm:@ryan_nookpi/pi-extension-todo-write-overlay
|
|
|
61
63
|
}
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
이미 목록이 있고 일부만 바꿀 때는 `op: "patch"`로 변경분만 보냅니다. 작업 수가 많을수록 args 크기가 변경분에만 비례해 저렴합니다.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"op": "patch",
|
|
71
|
+
"set": [
|
|
72
|
+
{ "id": "task-1", "status": "completed" },
|
|
73
|
+
{ "id": "task-2", "status": "in_progress", "activeForm": "구현 중" }
|
|
74
|
+
],
|
|
75
|
+
"add": [{ "content": "문서화", "status": "pending" }],
|
|
76
|
+
"remove": ["task-5"]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `set`/`add`/`remove`는 함께 쓸 수 있으며 remove → set → add 순서로 적용됩니다.
|
|
81
|
+
- `id`는 직전 `todo_write` 결과에 표시된 `task-N` 값을 참조합니다. 존재하지 않는 id는 결과 텍스트에 경고로 표시되고 무시됩니다.
|
|
82
|
+
- `add`로 추가되는 항목은 기존 id와 겹치지 않는 새 `task-N` id를 받습니다.
|
|
83
|
+
|
|
64
84
|
## 상태 필드
|
|
65
85
|
|
|
66
86
|
- `content`: 작업 설명
|
package/index.ts
CHANGED
|
@@ -39,14 +39,32 @@ const InputTask = Type.Object({
|
|
|
39
39
|
notes: Type.Optional(Type.String({ description: "추가 맥락 또는 메모" })),
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
const PatchSetTask = Type.Object({
|
|
43
|
+
id: Type.String({ description: "갱신할 작업 id. 직전 todo_write 결과에 표시된 task-N 값을 사용" }),
|
|
44
|
+
content: Type.Optional(Type.String({ description: "작업 설명 (변경 시에만)" })),
|
|
45
|
+
status: Type.Optional(StatusEnum),
|
|
46
|
+
activeForm: Type.Optional(Type.String({ description: "진행 중 표시용 현재진행형 문구" })),
|
|
47
|
+
notes: Type.Optional(Type.String({ description: "추가 맥락 또는 메모" })),
|
|
48
|
+
});
|
|
49
|
+
|
|
42
50
|
const TodoWriteParams = Type.Object(
|
|
43
51
|
{
|
|
44
|
-
|
|
52
|
+
op: Type.Optional(
|
|
53
|
+
Type.Union([Type.Literal("replace"), Type.Literal("patch")], {
|
|
54
|
+
description:
|
|
55
|
+
"replace(기본): todos로 전체 목록 교체. patch: set/add/remove로 변경분만 전달. 항목이 많을 때는 patch가 저렴합니다.",
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
todos: Type.Optional(Type.Array(InputTask, { description: "op=replace일 때: 전체 todo 목록" })),
|
|
59
|
+
set: Type.Optional(Type.Array(PatchSetTask, { description: "op=patch일 때: 기존 항목의 부분 갱신 목록" })),
|
|
60
|
+
add: Type.Optional(Type.Array(InputTask, { description: "op=patch일 때: 새로 추가할 항목 목록" })),
|
|
61
|
+
remove: Type.Optional(Type.Array(Type.String(), { description: "op=patch일 때: 제거할 작업 id 목록" })),
|
|
45
62
|
},
|
|
46
63
|
{ additionalProperties: true },
|
|
47
64
|
);
|
|
48
65
|
|
|
49
|
-
type
|
|
66
|
+
type InputTaskType = Static<typeof InputTask>;
|
|
67
|
+
type PatchSetTaskType = Static<typeof PatchSetTask>;
|
|
50
68
|
|
|
51
69
|
const todoStateStore = new Map<string, TodoState>();
|
|
52
70
|
const todoOverlayStore = new Map<string, TodoOverlayRecord>();
|
|
@@ -130,7 +148,7 @@ export function getTodoOverlayVisibility(
|
|
|
130
148
|
};
|
|
131
149
|
}
|
|
132
150
|
|
|
133
|
-
export function applyTodoWrite(todos:
|
|
151
|
+
export function applyTodoWrite(todos: InputTaskType[]): {
|
|
134
152
|
state: TodoState;
|
|
135
153
|
} {
|
|
136
154
|
const tasks: TodoTask[] = todos.map((todo, index) => ({
|
|
@@ -144,6 +162,66 @@ export function applyTodoWrite(todos: TodoWriteParamsType["todos"]): {
|
|
|
144
162
|
return { state: { tasks } };
|
|
145
163
|
}
|
|
146
164
|
|
|
165
|
+
function nextTaskId(tasks: TodoTask[]): string {
|
|
166
|
+
let max = 0;
|
|
167
|
+
for (const task of tasks) {
|
|
168
|
+
const match = /^task-(\d+)$/.exec(task.id);
|
|
169
|
+
if (match?.[1]) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
170
|
+
}
|
|
171
|
+
return `task-${max + 1}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type TodoPatch = {
|
|
175
|
+
set?: PatchSetTaskType[];
|
|
176
|
+
add?: InputTaskType[];
|
|
177
|
+
remove?: string[];
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export function applyTodoPatch(
|
|
181
|
+
state: TodoState,
|
|
182
|
+
patch: TodoPatch,
|
|
183
|
+
): {
|
|
184
|
+
state: TodoState;
|
|
185
|
+
warnings: string[];
|
|
186
|
+
} {
|
|
187
|
+
const tasks = cloneTasks(state.tasks);
|
|
188
|
+
const warnings: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (const id of patch.remove ?? []) {
|
|
191
|
+
const index = tasks.findIndex((task) => task.id === id);
|
|
192
|
+
if (index === -1) {
|
|
193
|
+
warnings.push(`제거할 항목을 찾지 못했습니다: ${id}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
tasks.splice(index, 1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const update of patch.set ?? []) {
|
|
200
|
+
const task = tasks.find((candidate) => candidate.id === update.id);
|
|
201
|
+
if (!task) {
|
|
202
|
+
warnings.push(`갱신할 항목을 찾지 못했습니다: ${update.id}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (update.content !== undefined) task.content = update.content;
|
|
206
|
+
if (update.status !== undefined) task.status = update.status;
|
|
207
|
+
if (update.activeForm !== undefined) task.activeForm = update.activeForm;
|
|
208
|
+
if (update.notes !== undefined) task.notes = update.notes;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const todo of patch.add ?? []) {
|
|
212
|
+
tasks.push({
|
|
213
|
+
id: nextTaskId(tasks),
|
|
214
|
+
content: todo.content,
|
|
215
|
+
status: todo.status,
|
|
216
|
+
activeForm: todo.activeForm,
|
|
217
|
+
notes: todo.notes,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
normalizeInProgressTask(tasks);
|
|
222
|
+
return { state: { tasks }, warnings };
|
|
223
|
+
}
|
|
224
|
+
|
|
147
225
|
export function renderTodoOverlayPlainLines(state: TodoState): string[] {
|
|
148
226
|
return state.tasks.map((task) => {
|
|
149
227
|
const marker = task.status === "completed" ? "✓" : task.status === "in_progress" ? "→" : "○";
|
|
@@ -597,6 +675,14 @@ export default function todoWriteOverlayExtension(pi: ExtensionAPI): void {
|
|
|
597
675
|
- 완전히 끝난 일만 completed로 표시하고, 막혔으면 in_progress 유지
|
|
598
676
|
- 요구사항이 바뀌면 계속 진행하기 전에 todo 목록부터 갱신
|
|
599
677
|
|
|
678
|
+
## 업데이트 방식 (op)
|
|
679
|
+
- 처음 목록을 만들거나 큰 폭으로 재편할 때만 op=replace(기본)로 todos 전체를 보냅니다.
|
|
680
|
+
- 이미 목록이 있고 일부만 바뀌면 op=patch로 변경분만 보내세요. 항목 수가 많을수록 강력히 권장됩니다.
|
|
681
|
+
- set: 기존 항목의 상태/내용 부분 갱신. 예: 하나 완료 + 다음 시작 => set: [{id:"task-1", status:"completed"}, {id:"task-2", status:"in_progress"}]
|
|
682
|
+
- add: 새 항목 추가
|
|
683
|
+
- remove: 항목 id 제거
|
|
684
|
+
- id는 임의로 지어내지 말고, 직전 todo_write 결과에 표시된 task-N 값을 그대로 참조하세요.
|
|
685
|
+
|
|
600
686
|
## 필드 설명
|
|
601
687
|
- content: 명령형 작업 문구 (예: "테스트 실행")
|
|
602
688
|
- status: pending | in_progress | completed
|
|
@@ -604,14 +690,31 @@ export default function todoWriteOverlayExtension(pi: ExtensionAPI): void {
|
|
|
604
690
|
- notes: (선택) 추가 맥락`,
|
|
605
691
|
parameters: TodoWriteParams,
|
|
606
692
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
693
|
+
const op = params.op ?? "replace";
|
|
694
|
+
let state: TodoState;
|
|
695
|
+
let warnings: string[] = [];
|
|
696
|
+
if (op === "patch") {
|
|
697
|
+
const result = applyTodoPatch(readTodoWriteState(ctx), {
|
|
698
|
+
set: params.set,
|
|
699
|
+
add: params.add,
|
|
700
|
+
remove: params.remove,
|
|
701
|
+
});
|
|
702
|
+
state = result.state;
|
|
703
|
+
warnings = result.warnings;
|
|
704
|
+
} else {
|
|
705
|
+
state = applyTodoWrite(params.todos ?? []).state;
|
|
706
|
+
}
|
|
707
|
+
const summary = renderTodoWriteSummary(state);
|
|
708
|
+
writeTodoWriteState(ctx, state);
|
|
709
|
+
persistTodoWriteStateEntry(pi, state);
|
|
611
710
|
await syncTodoOverlay(ctx, pi);
|
|
711
|
+
const text =
|
|
712
|
+
warnings.length > 0
|
|
713
|
+
? `${summary}\n\n주의:\n${warnings.map((warning) => ` - ${warning}`).join("\n")}`
|
|
714
|
+
: summary;
|
|
612
715
|
return {
|
|
613
|
-
content: [{ type: "text" as const, text
|
|
614
|
-
details: { tasks:
|
|
716
|
+
content: [{ type: "text" as const, text }],
|
|
717
|
+
details: { tasks: state.tasks, summary, warnings },
|
|
615
718
|
};
|
|
616
719
|
},
|
|
617
720
|
renderResult(result, { expanded }, theme) {
|