@trim21/personal-pi-extensions 0.0.290 → 0.0.293
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 +1 -1
- package/src/claude-code/session-tools.ts +12 -22
- package/src/lib/ui.ts +48 -0
- package/src/opencode/question.ts +10 -13
- package/src/spawn-agent.ts +0 -10
- package/src/system-prompt/prompt.md +0 -3
package/package.json
CHANGED
|
@@ -3,11 +3,12 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import { type Static, Type } from "typebox";
|
|
4
4
|
import { Value } from "typebox/value";
|
|
5
5
|
|
|
6
|
-
import { selectWithOptionalInput } from "../lib/ui.js";
|
|
6
|
+
import { selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
|
|
7
7
|
|
|
8
8
|
const TODO_STATUSES = ["pending", "in_progress", "completed"] as const;
|
|
9
9
|
const OTHER_OPTION = "Other";
|
|
10
|
-
|
|
10
|
+
/** 多选模式下用户手动确认提交的哨兵选项 */
|
|
11
|
+
const DONE_OPTION = "Submit";
|
|
11
12
|
|
|
12
13
|
/** AskUserQuestion guidance, kept as plain text like the tool .md files. */
|
|
13
14
|
const ASK_PROMPT = [
|
|
@@ -104,26 +105,15 @@ async function askMultiple(
|
|
|
104
105
|
signal: AbortSignal | undefined,
|
|
105
106
|
): Promise<string> {
|
|
106
107
|
const title = `${question.header}: ${question.question}`;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
],
|
|
117
|
-
ctx.ui,
|
|
118
|
-
{ signal },
|
|
119
|
-
);
|
|
120
|
-
if (result === undefined || result.label === DONE_OPTION) break;
|
|
121
|
-
if (result.prompted) {
|
|
122
|
-
if (result.input) selected.push(result.input);
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
if (remaining.delete(result.label)) selected.push(result.label);
|
|
126
|
-
}
|
|
108
|
+
const selected = await selectMultiple(
|
|
109
|
+
title,
|
|
110
|
+
[
|
|
111
|
+
...question.options.map((option) => ({ label: option.label })),
|
|
112
|
+
{ label: OTHER_OPTION, inputPrompt: "Type your answer" },
|
|
113
|
+
],
|
|
114
|
+
ctx.ui,
|
|
115
|
+
{ signal, doneLabel: DONE_OPTION },
|
|
116
|
+
);
|
|
127
117
|
return selected.length > 0 ? selected.join(", ") : "Unanswered";
|
|
128
118
|
}
|
|
129
119
|
|
package/src/lib/ui.ts
CHANGED
|
@@ -52,3 +52,51 @@ export async function selectWithOptionalInput(
|
|
|
52
52
|
input: answer === undefined ? undefined : answer.trim(),
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
const CHECKED_PREFIX = "[X]: ";
|
|
57
|
+
const UNCHECKED_PREFIX = "[ ]: ";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Toggle-style multi-select loop built on `ui.select`.
|
|
61
|
+
*
|
|
62
|
+
* Every round lists ALL `entries` — already-selected ones render with a
|
|
63
|
+
* `[X]: ` checkbox marker, the rest with `[ ]: ` — so re-selecting an entry
|
|
64
|
+
* unchecks it. Picking an entry with `inputPrompt` opens an input dialog and,
|
|
65
|
+
* when non-empty, adds the typed text and ends the loop. Picking `doneLabel`
|
|
66
|
+
* (or dismissing the dialog) ends the loop and returns the selected labels in
|
|
67
|
+
* selection order. Display text is mapped back to the original label via an
|
|
68
|
+
* explicit table so a `[ ]:` / `[X]:` prefix inside a label is unambiguous.
|
|
69
|
+
*/
|
|
70
|
+
export async function selectMultiple(
|
|
71
|
+
title: string,
|
|
72
|
+
entries: readonly SelectAction[],
|
|
73
|
+
ui: ExtensionContext["ui"],
|
|
74
|
+
opts: { signal?: AbortSignal; doneLabel: string },
|
|
75
|
+
): Promise<string[]> {
|
|
76
|
+
const selected: string[] = [];
|
|
77
|
+
while (true) {
|
|
78
|
+
const selectedSet = new Set(selected);
|
|
79
|
+
const displayToLabel = new Map<string, string>();
|
|
80
|
+
const round = entries.map((entry) => {
|
|
81
|
+
const display = `${selectedSet.has(entry.label) ? CHECKED_PREFIX : UNCHECKED_PREFIX}${entry.label}`;
|
|
82
|
+
displayToLabel.set(display, entry.label);
|
|
83
|
+
return { ...entry, label: display };
|
|
84
|
+
});
|
|
85
|
+
const result = await selectWithOptionalInput(
|
|
86
|
+
title,
|
|
87
|
+
[...round, { label: opts.doneLabel }],
|
|
88
|
+
ui,
|
|
89
|
+
opts,
|
|
90
|
+
);
|
|
91
|
+
if (result === undefined || result.label === opts.doneLabel) break;
|
|
92
|
+
if (result.prompted) {
|
|
93
|
+
if (result.input) selected.push(result.input);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const label = displayToLabel.get(result.label);
|
|
97
|
+
if (label === undefined) continue;
|
|
98
|
+
if (selectedSet.has(label)) selected.splice(selected.indexOf(label), 1);
|
|
99
|
+
else selected.push(label);
|
|
100
|
+
}
|
|
101
|
+
return selected;
|
|
102
|
+
}
|
package/src/opencode/question.ts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Aligned with opencode commit 999be62662 (v1.2.25-1672-g999be62662, 2026-08-12):
|
|
5
5
|
* https://github.com/anomalyco/opencode/blob/999be62662/packages/opencode/src/tool/question.ts
|
|
6
6
|
* 与 opencode 的差异:opencode 输出 title "Asked N question(s)",这里未设置;
|
|
7
|
-
* 多选交互是平台差异(opencode 用 checkbox,这里循环 ctx.ui.select
|
|
7
|
+
* 多选交互是平台差异(opencode 用 checkbox,这里循环 ctx.ui.select 勾选,
|
|
8
|
+
* 已选项显示 `[X]: ` 前缀,再次选择即反选,最后选「✓ Done」提交)。
|
|
8
9
|
*
|
|
9
10
|
* 参数与语义和 opencode 的 `question` 工具一致:
|
|
10
11
|
* questions 数组,每项含 question / header / options / multiple:
|
|
11
12
|
* - options 每项为 label / description
|
|
12
13
|
* - 单选(默认):用户在选项里选一个,也可选「Type your own answer.」自由输入
|
|
13
|
-
* - 多选(multiple: true):循环用 ui.select
|
|
14
|
+
* - 多选(multiple: true):循环用 ui.select 勾选/反选([X] 标记已选),直到「✓ Done」
|
|
14
15
|
* - 每个问题返回一个 label 数组(Answer = string[]),跳过的为空数组
|
|
15
16
|
* - 输出与 opencode 一致:
|
|
16
17
|
* User has answered your questions: "q"="a", "q2"="Unanswered"...
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
25
|
import { type Static, Type } from "typebox";
|
|
25
26
|
|
|
26
|
-
import { selectWithOptionalInput } from "../lib/ui.js";
|
|
27
|
+
import { selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
|
|
27
28
|
|
|
28
29
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
29
30
|
|
|
@@ -124,16 +125,12 @@ async function askSingle(q: Question, ctx: ExtensionContext): Promise<Answer> {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
async function askMultiple(q: Question, ctx: ExtensionContext): Promise<Answer> {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!remaining.has(choice)) continue;
|
|
134
|
-
selected.push(choice);
|
|
135
|
-
remaining.delete(choice);
|
|
136
|
-
}
|
|
128
|
+
const selected = await selectMultiple(
|
|
129
|
+
dialogTitle(q),
|
|
130
|
+
q.options.map((o) => ({ label: o.label })),
|
|
131
|
+
ctx.ui,
|
|
132
|
+
{ doneLabel: DONE_LABEL },
|
|
133
|
+
);
|
|
137
134
|
return selected;
|
|
138
135
|
}
|
|
139
136
|
|
package/src/spawn-agent.ts
CHANGED
|
@@ -98,8 +98,6 @@ const spawnAgentSchema = Type.Object({
|
|
|
98
98
|
// ── result types ─────────────────────────────────────────────────────────────
|
|
99
99
|
|
|
100
100
|
interface UsageStats {
|
|
101
|
-
input: number;
|
|
102
|
-
output: number;
|
|
103
101
|
cost: number;
|
|
104
102
|
contextTokens: number;
|
|
105
103
|
turns: number;
|
|
@@ -155,8 +153,6 @@ function formatTokens(count: number): string {
|
|
|
155
153
|
function formatUsageStats(usage: UsageStats, model?: string): string {
|
|
156
154
|
const parts: string[] = [];
|
|
157
155
|
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
158
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
159
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
160
156
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
161
157
|
if (usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
162
158
|
if (model) parts.push(model);
|
|
@@ -341,8 +337,6 @@ export async function runAgent(
|
|
|
341
337
|
messages: [],
|
|
342
338
|
stderr: "",
|
|
343
339
|
usage: {
|
|
344
|
-
input: 0,
|
|
345
|
-
output: 0,
|
|
346
340
|
cost: 0,
|
|
347
341
|
contextTokens: 0,
|
|
348
342
|
turns: 0,
|
|
@@ -492,8 +486,6 @@ export async function runAgent(
|
|
|
492
486
|
result.messages.push(msg);
|
|
493
487
|
if (msg.role === "assistant") {
|
|
494
488
|
result.usage.turns++;
|
|
495
|
-
result.usage.input += msg.usage.input;
|
|
496
|
-
result.usage.output += msg.usage.output;
|
|
497
489
|
result.usage.cost += msg.usage.cost.total;
|
|
498
490
|
result.usage.contextTokens = msg.usage.totalTokens;
|
|
499
491
|
if (!result.model) result.model = msg.model;
|
|
@@ -646,8 +638,6 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
646
638
|
messages: [],
|
|
647
639
|
stderr: "",
|
|
648
640
|
usage: {
|
|
649
|
-
input: 0,
|
|
650
|
-
output: 0,
|
|
651
641
|
cost: 0,
|
|
652
642
|
contextTokens: 0,
|
|
653
643
|
turns: 0,
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
You are an expert coding assistant operating inside pi, a coding agent harness. You are an interactive agent that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
2
2
|
|
|
3
|
-
IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.
|
|
4
|
-
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
5
|
-
|
|
6
3
|
Available tools:
|
|
7
4
|
{{tools}}
|
|
8
5
|
|