@ryan_nookpi/pi-extension-until 0.1.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 +17 -0
- package/index.ts +488 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @ryan_nookpi/pi-extension-until
|
|
2
|
+
|
|
3
|
+
Standalone pi package for the `until` extension.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@ryan_nookpi/pi-extension-until
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it provides
|
|
12
|
+
|
|
13
|
+
- `/until` command
|
|
14
|
+
- `/untils` command
|
|
15
|
+
- `/until-cancel` command
|
|
16
|
+
- `until_report` tool
|
|
17
|
+
- `./index.ts` entry
|
package/index.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Box, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
|
|
5
|
+
const CUSTOM_TYPE = "until";
|
|
6
|
+
const PROMPT_MESSAGE_TYPE = "until-prompt";
|
|
7
|
+
const STATUS_KEY = "until-footer";
|
|
8
|
+
|
|
9
|
+
const MAX_TASKS = 3;
|
|
10
|
+
const MIN_INTERVAL_MS = 60_000; // 1분
|
|
11
|
+
const MAX_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24시간
|
|
12
|
+
const JITTER_RATIO = 0.1; // ±10%
|
|
13
|
+
|
|
14
|
+
const INTERVAL_RE = /^(\d+(?:\.\d+)?)\s*(?:(m|h|분|시간)(?:마다)?)\s*$/i;
|
|
15
|
+
|
|
16
|
+
interface UntilTask {
|
|
17
|
+
id: number;
|
|
18
|
+
prompt: string;
|
|
19
|
+
displayPrompt: string;
|
|
20
|
+
intervalMs: number;
|
|
21
|
+
intervalLabel: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
nextRunAt: number;
|
|
25
|
+
runCount: number;
|
|
26
|
+
inFlight: boolean;
|
|
27
|
+
lastSummary?: string;
|
|
28
|
+
timer: ReturnType<typeof setTimeout>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface UntilPromptMessageDetails {
|
|
32
|
+
taskId: number;
|
|
33
|
+
runCount: number;
|
|
34
|
+
intervalLabel: string;
|
|
35
|
+
elapsed: string;
|
|
36
|
+
displayPrompt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface UntilReportDetails {
|
|
40
|
+
done: boolean;
|
|
41
|
+
summary: string;
|
|
42
|
+
taskId: number;
|
|
43
|
+
runCount: number;
|
|
44
|
+
nextRunAt?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatKoreanDuration(ms: number): string {
|
|
48
|
+
if (ms < 60_000) return `${Math.max(1, Math.round(ms / 1000))}초`;
|
|
49
|
+
if (ms < 3_600_000) return `${Math.max(1, Math.round(ms / 60_000))}분`;
|
|
50
|
+
|
|
51
|
+
const hours = Math.floor(ms / 3_600_000);
|
|
52
|
+
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
|
53
|
+
if (minutes === 0) return `${hours}시간`;
|
|
54
|
+
return `${hours}시간 ${minutes}분`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatClock(ts: number): string {
|
|
58
|
+
return new Date(ts).toLocaleTimeString("ko-KR", {
|
|
59
|
+
hour: "2-digit",
|
|
60
|
+
minute: "2-digit",
|
|
61
|
+
second: "2-digit",
|
|
62
|
+
hour12: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseInterval(raw: string): { ms: number; label: string } | null {
|
|
67
|
+
const trimmed = raw.trim();
|
|
68
|
+
if (!trimmed) return null;
|
|
69
|
+
|
|
70
|
+
const match = trimmed.match(INTERVAL_RE);
|
|
71
|
+
if (!match) return null;
|
|
72
|
+
|
|
73
|
+
const amount = Number(match[1]);
|
|
74
|
+
const unitRaw = match[2].toLowerCase();
|
|
75
|
+
|
|
76
|
+
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
77
|
+
|
|
78
|
+
switch (unitRaw) {
|
|
79
|
+
case "m":
|
|
80
|
+
case "분":
|
|
81
|
+
return { ms: amount * 60 * 1000, label: `${amount}분` };
|
|
82
|
+
case "h":
|
|
83
|
+
case "시간":
|
|
84
|
+
return { ms: amount * 60 * 60 * 1000, label: `${amount}시간` };
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function (pi: ExtensionAPI) {
|
|
91
|
+
const tasks = new Map<number, UntilTask>();
|
|
92
|
+
let nextTaskId = 1;
|
|
93
|
+
let agentRunning = false;
|
|
94
|
+
let latestCtx: ExtensionContext | undefined;
|
|
95
|
+
|
|
96
|
+
const notify = (ctx: ExtensionContext | undefined, message: string, level: "info" | "warning" | "error") => {
|
|
97
|
+
if (!ctx?.hasUI) return;
|
|
98
|
+
ctx.ui.notify(message, level);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const clearAllTasks = () => {
|
|
102
|
+
for (const task of tasks.values()) clearTimeout(task.timer);
|
|
103
|
+
tasks.clear();
|
|
104
|
+
updateFooter();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const removeTask = (id: number) => {
|
|
108
|
+
const task = tasks.get(id);
|
|
109
|
+
if (!task) return;
|
|
110
|
+
clearTimeout(task.timer);
|
|
111
|
+
tasks.delete(id);
|
|
112
|
+
updateFooter();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const updateFooter = () => {
|
|
116
|
+
if (!latestCtx?.hasUI) return;
|
|
117
|
+
const theme = latestCtx.ui.theme;
|
|
118
|
+
|
|
119
|
+
if (tasks.size === 0) {
|
|
120
|
+
latestCtx.ui.setStatus(STATUS_KEY, undefined);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let nearestRun = Number.POSITIVE_INFINITY;
|
|
125
|
+
for (const task of tasks.values()) {
|
|
126
|
+
if (task.nextRunAt < nearestRun) nearestRun = task.nextRunAt;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const nextLabel = nearestRun < Number.POSITIVE_INFINITY ? formatClock(nearestRun) : "—";
|
|
130
|
+
const text = theme.fg("accent", `⏳ until ×${tasks.size}`) + theme.fg("dim", ` | next ${nextLabel}`);
|
|
131
|
+
latestCtx.ui.setStatus(STATUS_KEY, text);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const jitter = (ms: number): number => {
|
|
135
|
+
const offset = ms * JITTER_RATIO * (Math.random() * 2 - 1);
|
|
136
|
+
return Math.max(MIN_INTERVAL_MS, Math.round(ms + offset));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const scheduleNext = (id: number) => {
|
|
140
|
+
const task = tasks.get(id);
|
|
141
|
+
if (!task) return;
|
|
142
|
+
|
|
143
|
+
clearTimeout(task.timer);
|
|
144
|
+
|
|
145
|
+
const delay = jitter(task.intervalMs);
|
|
146
|
+
task.nextRunAt = Date.now() + delay;
|
|
147
|
+
task.timer = setTimeout(() => executeRun(id), delay);
|
|
148
|
+
updateFooter();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const executeRun = (id: number) => {
|
|
152
|
+
const task = tasks.get(id);
|
|
153
|
+
if (!task) return;
|
|
154
|
+
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
if (now >= task.expiresAt) {
|
|
157
|
+
notify(latestCtx, `⏳ until #${task.id} 만료됨 (24시간 초과)`, "warning");
|
|
158
|
+
pi.sendMessage({
|
|
159
|
+
customType: CUSTOM_TYPE,
|
|
160
|
+
content: `[until #${task.id}] 24시간 만료로 자동 종료됨\n마지막 상태: ${task.lastSummary ?? "없음"}`,
|
|
161
|
+
display: true,
|
|
162
|
+
});
|
|
163
|
+
removeTask(id);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (task.inFlight) {
|
|
168
|
+
scheduleNext(id);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
task.runCount += 1;
|
|
173
|
+
|
|
174
|
+
const elapsed = formatKoreanDuration(now - task.createdAt);
|
|
175
|
+
const wrappedPrompt = [
|
|
176
|
+
`[until #${task.id} — 실행 ${task.runCount}회차, 경과 ${elapsed}, 간격 ${task.intervalLabel}]`,
|
|
177
|
+
"",
|
|
178
|
+
task.prompt,
|
|
179
|
+
"",
|
|
180
|
+
"작업을 수행한 뒤, 반드시 until_report 도구를 호출하여 결과를 보고하세요.",
|
|
181
|
+
`- taskId: ${task.id} (이 값을 그대로 전달)`,
|
|
182
|
+
"- done: true (조건 충족, 반복 종료) 또는 done: false (미충족, 계속 반복)",
|
|
183
|
+
"- summary: 현재 상태를 한 줄로 요약",
|
|
184
|
+
].join("\n");
|
|
185
|
+
|
|
186
|
+
notify(latestCtx, `⏳ until #${task.id} 실행 ${task.runCount}회차`, "info");
|
|
187
|
+
task.inFlight = true;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
pi.sendMessage(
|
|
191
|
+
{
|
|
192
|
+
customType: PROMPT_MESSAGE_TYPE,
|
|
193
|
+
content: wrappedPrompt,
|
|
194
|
+
display: true,
|
|
195
|
+
details: {
|
|
196
|
+
taskId: task.id,
|
|
197
|
+
runCount: task.runCount,
|
|
198
|
+
intervalLabel: task.intervalLabel,
|
|
199
|
+
elapsed,
|
|
200
|
+
displayPrompt: task.displayPrompt,
|
|
201
|
+
} satisfies UntilPromptMessageDetails,
|
|
202
|
+
},
|
|
203
|
+
agentRunning ? { deliverAs: "followUp", triggerTurn: true } : { triggerTurn: true },
|
|
204
|
+
);
|
|
205
|
+
} catch {
|
|
206
|
+
task.inFlight = false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
scheduleNext(id);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const registerTask = (
|
|
213
|
+
intervalMs: number,
|
|
214
|
+
intervalLabel: string,
|
|
215
|
+
prompt: string,
|
|
216
|
+
ctx: ExtensionContext,
|
|
217
|
+
displayPrompt = prompt,
|
|
218
|
+
): boolean => {
|
|
219
|
+
if (tasks.size >= MAX_TASKS) {
|
|
220
|
+
notify(ctx, `최대 ${MAX_TASKS}개까지만 등록할 수 있어. /until-cancel로 정리해줘.`, "error");
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (intervalMs < MIN_INTERVAL_MS) {
|
|
225
|
+
notify(ctx, `최소 간격은 1분이야. ${formatKoreanDuration(intervalMs)}은 너무 짧아.`, "error");
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const id = nextTaskId++;
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
const task: UntilTask = {
|
|
232
|
+
id,
|
|
233
|
+
prompt,
|
|
234
|
+
displayPrompt,
|
|
235
|
+
intervalMs,
|
|
236
|
+
intervalLabel,
|
|
237
|
+
createdAt: now,
|
|
238
|
+
expiresAt: now + MAX_EXPIRY_MS,
|
|
239
|
+
nextRunAt: now,
|
|
240
|
+
runCount: 0,
|
|
241
|
+
inFlight: false,
|
|
242
|
+
timer: setTimeout(() => executeRun(id), 0),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
tasks.set(id, task);
|
|
246
|
+
pi.sendMessage({
|
|
247
|
+
customType: CUSTOM_TYPE,
|
|
248
|
+
content: `[until #${id}] 등록됨: ${intervalLabel}마다 반복\n만료: ${formatClock(task.expiresAt)}\nTask: ${displayPrompt}`,
|
|
249
|
+
display: true,
|
|
250
|
+
details: { id, prompt, displayPrompt, intervalMs, intervalLabel },
|
|
251
|
+
});
|
|
252
|
+
notify(ctx, `⏳ until #${id} 등록됨 (${intervalLabel}마다)`, "info");
|
|
253
|
+
updateFooter();
|
|
254
|
+
return true;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
pi.registerTool({
|
|
258
|
+
name: "until_report",
|
|
259
|
+
label: "Until Report",
|
|
260
|
+
description: "until 반복 작업의 결과를 보고합니다. 조건 충족 시 done: true로 반복을 종료합니다.",
|
|
261
|
+
promptSnippet: "Report until-loop result: done (condition met?) + summary",
|
|
262
|
+
promptGuidelines: ["until 반복 작업 프롬프트를 받으면, 작업 수행 후 반드시 until_report를 호출하세요."],
|
|
263
|
+
parameters: Type.Object({
|
|
264
|
+
taskId: Type.Number({
|
|
265
|
+
description: "until task ID (프롬프트의 #N)",
|
|
266
|
+
}),
|
|
267
|
+
done: Type.Boolean({
|
|
268
|
+
description: "조건이 충족되었으면 true, 아니면 false",
|
|
269
|
+
}),
|
|
270
|
+
summary: Type.String({
|
|
271
|
+
description: "현재 상태를 한 줄로 요약",
|
|
272
|
+
}),
|
|
273
|
+
}),
|
|
274
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
275
|
+
const task = tasks.get(params.taskId);
|
|
276
|
+
if (!task) {
|
|
277
|
+
throw new Error(`until #${params.taskId} 작업을 찾을 수 없습니다. 이미 완료/취소/만료되었을 수 있습니다.`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
task.inFlight = false;
|
|
281
|
+
task.lastSummary = params.summary;
|
|
282
|
+
|
|
283
|
+
if (params.done) {
|
|
284
|
+
const elapsed = formatKoreanDuration(Date.now() - task.createdAt);
|
|
285
|
+
pi.sendMessage({
|
|
286
|
+
customType: CUSTOM_TYPE,
|
|
287
|
+
content: `[until #${task.id}] ✅ 조건 충족! (${task.runCount}회 실행, ${elapsed} 경과)\n결과: ${params.summary}`,
|
|
288
|
+
display: true,
|
|
289
|
+
});
|
|
290
|
+
notify(latestCtx, `✅ until #${task.id} 완료: ${params.summary}`, "info");
|
|
291
|
+
removeTask(task.id);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
content: [
|
|
295
|
+
{
|
|
296
|
+
type: "text" as const,
|
|
297
|
+
text: `until #${task.id} 조건 충족으로 종료됨. ${params.summary}`,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
details: {
|
|
301
|
+
done: true,
|
|
302
|
+
summary: params.summary,
|
|
303
|
+
taskId: task.id,
|
|
304
|
+
runCount: task.runCount,
|
|
305
|
+
} satisfies UntilReportDetails,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
content: [
|
|
311
|
+
{
|
|
312
|
+
type: "text" as const,
|
|
313
|
+
text: `until #${task.id} 계속 반복. 다음 실행: ${formatClock(task.nextRunAt)}. ${params.summary}`,
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
details: {
|
|
317
|
+
done: false,
|
|
318
|
+
summary: params.summary,
|
|
319
|
+
taskId: task.id,
|
|
320
|
+
nextRunAt: task.nextRunAt,
|
|
321
|
+
runCount: task.runCount,
|
|
322
|
+
} satisfies UntilReportDetails,
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
pi.registerCommand("until", {
|
|
328
|
+
description: "조건 충족까지 주기적 실행. 사용법: /until <간격> <프롬프트>",
|
|
329
|
+
handler: async (args, ctx) => {
|
|
330
|
+
latestCtx = ctx;
|
|
331
|
+
const raw = (args ?? "").trim();
|
|
332
|
+
if (!raw) {
|
|
333
|
+
notify(ctx, "사용법: /until <간격> <프롬프트>\n예: /until 5m PR 코멘트 확인해줘", "warning");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const spaceIdx = raw.indexOf(" ");
|
|
338
|
+
if (spaceIdx === -1) {
|
|
339
|
+
notify(ctx, "프롬프트가 필요해. 예: /until 5m PR 코멘트 확인해줘", "error");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const firstToken = raw.slice(0, spaceIdx);
|
|
344
|
+
const rest = raw.slice(spaceIdx + 1).trim();
|
|
345
|
+
const parsed = parseInterval(firstToken);
|
|
346
|
+
if (!parsed) {
|
|
347
|
+
notify(
|
|
348
|
+
ctx,
|
|
349
|
+
`인터벌 "${firstToken}"을 파싱할 수 없어.\n지원 형식: 5m, 1h, 5분, 1시간, 5분마다, 1시간마다`,
|
|
350
|
+
"error",
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!rest) {
|
|
356
|
+
notify(ctx, "프롬프트가 필요해. 예: /until 5m PR 코멘트 확인해줘", "error");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
registerTask(parsed.ms, parsed.label, rest, ctx);
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
pi.registerCommand("untils", {
|
|
365
|
+
description: "활성 until 목록 보기",
|
|
366
|
+
handler: async (_args, ctx) => {
|
|
367
|
+
latestCtx = ctx;
|
|
368
|
+
if (tasks.size === 0) {
|
|
369
|
+
notify(ctx, "활성 until 작업이 없어.", "info");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
const lines = [...tasks.values()]
|
|
375
|
+
.sort((a, b) => a.nextRunAt - b.nextRunAt)
|
|
376
|
+
.map((task) => {
|
|
377
|
+
const remain = formatKoreanDuration(Math.max(0, task.nextRunAt - now));
|
|
378
|
+
const elapsed = formatKoreanDuration(now - task.createdAt);
|
|
379
|
+
const summary = task.lastSummary ? `\n 최근: ${task.lastSummary}` : "";
|
|
380
|
+
return ` #${task.id} · ${task.intervalLabel}마다 · 실행 ${task.runCount}회 · 경과 ${elapsed} · 다음 ${remain} 후${summary}\n ${task.displayPrompt}`;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
pi.sendMessage({
|
|
384
|
+
customType: CUSTOM_TYPE,
|
|
385
|
+
content: `활성 until 목록 (${tasks.size}개)\n\n${lines.join("\n\n")}`,
|
|
386
|
+
display: true,
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
pi.registerCommand("until-cancel", {
|
|
392
|
+
description: "until 취소. 사용법: /until-cancel <id|all>",
|
|
393
|
+
handler: async (args, ctx) => {
|
|
394
|
+
latestCtx = ctx;
|
|
395
|
+
const raw = (args ?? "").trim().toLowerCase();
|
|
396
|
+
if (!raw) {
|
|
397
|
+
notify(ctx, "사용법: /until-cancel <id|all>", "info");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (raw === "all") {
|
|
402
|
+
const count = tasks.size;
|
|
403
|
+
clearAllTasks();
|
|
404
|
+
notify(ctx, `until ${count}개 취소됨`, "info");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const id = Number(raw);
|
|
409
|
+
if (!Number.isInteger(id)) {
|
|
410
|
+
notify(ctx, "id는 숫자여야 해. 예: /until-cancel 3", "warning");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const task = tasks.get(id);
|
|
415
|
+
if (!task) {
|
|
416
|
+
notify(ctx, `until #${id} 없음`, "warning");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
removeTask(id);
|
|
421
|
+
notify(ctx, `until #${id} 취소됨`, "info");
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
pi.registerMessageRenderer<UntilPromptMessageDetails>(PROMPT_MESSAGE_TYPE, (message, { expanded }, theme) => {
|
|
426
|
+
const details = message.details;
|
|
427
|
+
const header = theme.fg(
|
|
428
|
+
"accent",
|
|
429
|
+
`[until #${details?.taskId ?? "?"} — 실행 ${details?.runCount ?? "?"}회차, 경과 ${details?.elapsed ?? "?"}, 간격 ${details?.intervalLabel ?? "?"}]`,
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
433
|
+
box.addChild(new Text(header, 0, 0));
|
|
434
|
+
box.addChild(new Spacer(1));
|
|
435
|
+
|
|
436
|
+
if (!expanded) {
|
|
437
|
+
const summary = details?.displayPrompt ? `Task: ${details.displayPrompt}` : "Task: (unknown)";
|
|
438
|
+
box.addChild(new Text(theme.fg("customMessageText", summary), 0, 0));
|
|
439
|
+
box.addChild(new Spacer(1));
|
|
440
|
+
box.addChild(new Text(theme.fg("dim", "전체 프롬프트는 접혀 있음 · 확장해서 확인 가능"), 0, 0));
|
|
441
|
+
return box;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const text =
|
|
445
|
+
typeof message.content === "string"
|
|
446
|
+
? message.content
|
|
447
|
+
: message.content
|
|
448
|
+
.filter((content) => content.type === "text")
|
|
449
|
+
.map((content) => content.text)
|
|
450
|
+
.join("\n");
|
|
451
|
+
|
|
452
|
+
box.addChild(
|
|
453
|
+
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
454
|
+
color: (value) => theme.fg("customMessageText", value),
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
return box;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
461
|
+
agentRunning = true;
|
|
462
|
+
latestCtx = ctx;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
466
|
+
agentRunning = false;
|
|
467
|
+
latestCtx = ctx;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
pi.on("context", async (event) => {
|
|
471
|
+
const filtered = event.messages.filter(
|
|
472
|
+
(message) => !(message.role === "custom" && (message as { customType?: string }).customType === CUSTOM_TYPE),
|
|
473
|
+
);
|
|
474
|
+
if (filtered.length === event.messages.length) return;
|
|
475
|
+
return { messages: filtered };
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
479
|
+
agentRunning = false;
|
|
480
|
+
latestCtx = ctx;
|
|
481
|
+
clearAllTasks();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
pi.on("session_shutdown", async () => {
|
|
485
|
+
agentRunning = false;
|
|
486
|
+
clearAllTasks();
|
|
487
|
+
});
|
|
488
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ryan_nookpi/pi-extension-until",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Until loop extension for pi.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"pi": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./index.ts"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
20
|
+
"@mariozechner/pi-tui": "*",
|
|
21
|
+
"@sinclair/typebox": "*"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|