@firstpick/pi-package-webui 0.2.2 → 0.2.4

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.
@@ -0,0 +1,231 @@
1
+ import { formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
2
+
3
+ const HELPER_COMMAND = "webui-helper";
4
+ const RESPONSE_PREFIX = "__PI_WEBUI_HELPER_RESPONSE__:";
5
+ const TOOLS_CONFIG_TYPE = "webui-tools-config";
6
+ const SKILLS_CONFIG_TYPE = "webui-skills-config";
7
+
8
+ function responseMessage(payload) {
9
+ return `${RESPONSE_PREFIX}${JSON.stringify(payload)}`;
10
+ }
11
+
12
+ function safeSourceInfo(sourceInfo) {
13
+ if (!sourceInfo || typeof sourceInfo !== "object") return undefined;
14
+ return {
15
+ path: typeof sourceInfo.path === "string" ? sourceInfo.path : undefined,
16
+ source: typeof sourceInfo.source === "string" ? sourceInfo.source : undefined,
17
+ scope: typeof sourceInfo.scope === "string" ? sourceInfo.scope : undefined,
18
+ origin: typeof sourceInfo.origin === "string" ? sourceInfo.origin : undefined,
19
+ baseDir: typeof sourceInfo.baseDir === "string" ? sourceInfo.baseDir : undefined,
20
+ };
21
+ }
22
+
23
+ function lastBranchConfig(ctx, customType) {
24
+ let found;
25
+ for (const entry of ctx.sessionManager.getBranch()) {
26
+ if (entry?.type === "custom" && entry.customType === customType && entry.data && typeof entry.data === "object") {
27
+ found = entry.data;
28
+ }
29
+ }
30
+ return found;
31
+ }
32
+
33
+ function normalizeNameList(value) {
34
+ if (!Array.isArray(value)) return [];
35
+ const names = [];
36
+ const seen = new Set();
37
+ for (const item of value) {
38
+ const name = String(item || "").trim();
39
+ if (!name || seen.has(name)) continue;
40
+ seen.add(name);
41
+ names.push(name);
42
+ }
43
+ return names;
44
+ }
45
+
46
+ function parseHelperArgs(args) {
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(args || "{}");
50
+ } catch (error) {
51
+ throw new Error(`Invalid ${HELPER_COMMAND} payload: ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ if (!parsed || typeof parsed !== "object") throw new Error(`${HELPER_COMMAND} payload must be an object`);
54
+ const requestId = String(parsed.requestId || "").trim();
55
+ const action = String(parsed.action || "").trim();
56
+ if (!requestId) throw new Error(`${HELPER_COMMAND} payload requires requestId`);
57
+ if (!action) throw new Error(`${HELPER_COMMAND} payload requires action`);
58
+ return { requestId, action, payload: parsed.payload && typeof parsed.payload === "object" ? parsed.payload : {} };
59
+ }
60
+
61
+ function skillBlockPattern(name) {
62
+ const escaped = String(name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ return new RegExp(`\\n? <skill>\\n <name>${escaped}<\\/name>[\\s\\S]*? <\\/skill>`, "g");
64
+ }
65
+
66
+ function replaceAvailableSkillsSection(systemPrompt, skills) {
67
+ const nextSection = formatSkillsForPrompt(skills);
68
+ const replacement = nextSection ? `\n${nextSection}\n` : "\n";
69
+ if (systemPrompt.includes("<available_skills>")) {
70
+ return systemPrompt.replace(/\n?The following skills provide[\s\S]*?<\/available_skills>\n?/m, replacement);
71
+ }
72
+ return systemPrompt;
73
+ }
74
+
75
+ export default function webuiRpcHelper(pi) {
76
+ let enabledTools = new Set();
77
+ let disabledSkills = new Set();
78
+
79
+ function allToolNames() {
80
+ return pi.getAllTools().map((tool) => tool.name);
81
+ }
82
+
83
+ function persistToolsState() {
84
+ pi.appendEntry(TOOLS_CONFIG_TYPE, { enabledTools: [...enabledTools] });
85
+ }
86
+
87
+ function applyTools() {
88
+ const existing = new Set(allToolNames());
89
+ pi.setActiveTools([...enabledTools].filter((name) => existing.has(name)));
90
+ }
91
+
92
+ function restoreToolsFromBranch(ctx) {
93
+ const saved = lastBranchConfig(ctx, TOOLS_CONFIG_TYPE)?.enabledTools;
94
+ if (Array.isArray(saved)) {
95
+ const existing = new Set(allToolNames());
96
+ enabledTools = new Set(normalizeNameList(saved).filter((name) => existing.has(name)));
97
+ applyTools();
98
+ return;
99
+ }
100
+ enabledTools = new Set(pi.getActiveTools());
101
+ }
102
+
103
+ function toolState() {
104
+ const active = new Set(pi.getActiveTools());
105
+ enabledTools = new Set([...active]);
106
+ return {
107
+ tools: pi.getAllTools().map((tool) => ({
108
+ name: tool.name,
109
+ description: tool.description || "",
110
+ enabled: active.has(tool.name),
111
+ sourceInfo: safeSourceInfo(tool.sourceInfo),
112
+ })),
113
+ };
114
+ }
115
+
116
+ function setToolState(payload) {
117
+ const existing = new Set(allToolNames());
118
+ if (Array.isArray(payload.enabledTools)) {
119
+ enabledTools = new Set(normalizeNameList(payload.enabledTools).filter((name) => existing.has(name)));
120
+ } else if (Array.isArray(payload.disabledTools)) {
121
+ const disabled = new Set(normalizeNameList(payload.disabledTools));
122
+ enabledTools = new Set([...existing].filter((name) => !disabled.has(name)));
123
+ } else {
124
+ throw new Error("Tool update requires enabledTools or disabledTools");
125
+ }
126
+ applyTools();
127
+ persistToolsState();
128
+ return toolState();
129
+ }
130
+
131
+ function persistSkillsState() {
132
+ pi.appendEntry(SKILLS_CONFIG_TYPE, { disabledSkills: [...disabledSkills] });
133
+ }
134
+
135
+ function restoreSkillsFromBranch(ctx) {
136
+ const saved = lastBranchConfig(ctx, SKILLS_CONFIG_TYPE)?.disabledSkills;
137
+ disabledSkills = new Set(normalizeNameList(saved));
138
+ }
139
+
140
+ function skillsFromContext(ctx) {
141
+ const options = ctx.getSystemPromptOptions?.();
142
+ const skills = Array.isArray(options?.skills) ? options.skills : [];
143
+ return skills.map((skill) => ({
144
+ name: skill.name,
145
+ description: skill.description || "",
146
+ enabled: !disabledSkills.has(skill.name),
147
+ disableModelInvocation: skill.disableModelInvocation === true,
148
+ filePath: skill.filePath,
149
+ sourceInfo: safeSourceInfo(skill.sourceInfo),
150
+ }));
151
+ }
152
+
153
+ function skillState(ctx) {
154
+ const known = new Set(skillsFromContext(ctx).map((skill) => skill.name));
155
+ disabledSkills = new Set([...disabledSkills].filter((name) => known.has(name)));
156
+ return { skills: skillsFromContext(ctx) };
157
+ }
158
+
159
+ function setSkillState(ctx, payload) {
160
+ const allNames = new Set(skillsFromContext(ctx).map((skill) => skill.name));
161
+ if (Array.isArray(payload.enabledSkills)) {
162
+ const enabled = new Set(normalizeNameList(payload.enabledSkills));
163
+ disabledSkills = new Set([...allNames].filter((name) => !enabled.has(name)));
164
+ } else if (Array.isArray(payload.disabledSkills)) {
165
+ disabledSkills = new Set(normalizeNameList(payload.disabledSkills).filter((name) => allNames.has(name)));
166
+ } else {
167
+ throw new Error("Skill update requires enabledSkills or disabledSkills");
168
+ }
169
+ persistSkillsState();
170
+ return skillState(ctx);
171
+ }
172
+
173
+ function executeAction(action, payload, ctx) {
174
+ switch (action) {
175
+ case "tools-state":
176
+ return toolState();
177
+ case "tools-set":
178
+ return setToolState(payload);
179
+ case "skills-state":
180
+ return skillState(ctx);
181
+ case "skills-set":
182
+ return setSkillState(ctx, payload);
183
+ default:
184
+ throw new Error(`Unknown ${HELPER_COMMAND} action: ${action}`);
185
+ }
186
+ }
187
+
188
+ pi.registerCommand(HELPER_COMMAND, {
189
+ description: "Internal Web UI helper for browser-native tools and skills configuration",
190
+ handler: async (args, ctx) => {
191
+ let requestId = "";
192
+ try {
193
+ const request = parseHelperArgs(args);
194
+ requestId = request.requestId;
195
+ const data = executeAction(request.action, request.payload, ctx);
196
+ ctx.ui.notify(responseMessage({ requestId, ok: true, data }), "info");
197
+ } catch (error) {
198
+ ctx.ui.notify(responseMessage({ requestId, ok: false, error: error instanceof Error ? error.message : String(error) }), "error");
199
+ }
200
+ },
201
+ });
202
+
203
+ pi.on("session_start", async (_event, ctx) => {
204
+ restoreToolsFromBranch(ctx);
205
+ restoreSkillsFromBranch(ctx);
206
+ });
207
+
208
+ pi.on("session_tree", async (_event, ctx) => {
209
+ restoreToolsFromBranch(ctx);
210
+ restoreSkillsFromBranch(ctx);
211
+ });
212
+
213
+ pi.on("input", async (event, ctx) => {
214
+ const match = String(event.text || "").trim().match(/^\/skill:([^\s]+)/i);
215
+ if (!match) return { action: "continue" };
216
+ const skillName = match[1];
217
+ if (!disabledSkills.has(skillName)) return { action: "continue" };
218
+ ctx.ui.notify(`Skill /skill:${skillName} is disabled in the Web UI /skills selector.`, "warning");
219
+ return { action: "handled" };
220
+ });
221
+
222
+ pi.on("before_agent_start", async (event) => {
223
+ if (disabledSkills.size === 0) return;
224
+ const allSkills = Array.isArray(event.systemPromptOptions?.skills) ? event.systemPromptOptions.skills : [];
225
+ if (allSkills.length === 0) return;
226
+ const filteredSkills = allSkills.filter((skill) => !disabledSkills.has(skill.name));
227
+ let nextPrompt = replaceAvailableSkillsSection(event.systemPrompt, filteredSkills);
228
+ for (const name of disabledSkills) nextPrompt = nextPrompt.replace(skillBlockPattern(name), "");
229
+ return { systemPrompt: nextPrompt };
230
+ });
231
+ }