@hyperdreamer/pi-webui 1.11.2 → 1.11.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.
- package/README.md +3 -1
- package/dist/client/assets/{CodeViewer-BRcVinSx.js → CodeViewer-RC12gm-a.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-ChxwNz6T.js → UnifiedDiffViewer-BKkTUzo8.js} +1 -1
- package/dist/client/assets/{index-DG-QGbW9.js → index-6Nw3vRPS.js} +540 -477
- package/dist/client/index.html +2 -1
- package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsData.js +2 -0
- package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelElement.js +420 -0
- package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelLayout.js +68 -0
- package/dist/pi-webui-plugins/workspace-learned-skills/package.json +9 -0
- package/dist/pi-webui-plugins/workspace-learned-skills/pi-webui-plugin.js +48 -0
- package/dist/server/app.js +39 -3
- package/dist/server/app.js.map +1 -1
- package/dist/server/learnedSkills/learnedSkillCatalog.js +66 -0
- package/dist/server/learnedSkills/learnedSkillCatalog.js.map +1 -0
- package/dist/server/learnedSkills/learnedSkillProvider.js +2 -0
- package/dist/server/learnedSkills/learnedSkillProvider.js.map +1 -0
- package/dist/server/learnedSkills/learnedSkillsRoutes.js +25 -0
- package/dist/server/learnedSkills/learnedSkillsRoutes.js.map +1 -0
- package/dist/server/learnedSkills/piHermesLearnedSkillProvider.js +123 -0
- package/dist/server/learnedSkills/piHermesLearnedSkillProvider.js.map +1 -0
- package/dist/server/learnedSkills/skillDocumentParser.js +42 -0
- package/dist/server/learnedSkills/skillDocumentParser.js.map +1 -0
- package/dist/server/memory/piHermesMemoryProvider.js +9 -7
- package/dist/server/memory/piHermesMemoryProvider.js.map +1 -1
- package/dist/server/piHermes/projectIdentity.js +103 -0
- package/dist/server/piHermes/projectIdentity.js.map +1 -0
- package/dist/server/projects/projectService.js +28 -2
- package/dist/server/projects/projectService.js.map +1 -1
- package/dist/server/realtime/sessionEventHub.js +16 -0
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/realtime/slowConsumerGuard.js +75 -0
- package/dist/server/realtime/slowConsumerGuard.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +14 -1
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +8 -0
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +16 -1
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +4 -3
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/storage/projectStore.js +170 -26
- package/dist/server/storage/projectStore.js.map +1 -1
- package/dist/server/webSocketBridge.js +12 -1
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +19 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +4 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/projectAncestry.js +43 -0
- package/dist/shared/projectAncestry.js.map +1 -0
- package/docs/plugins.md +24 -1
- package/package.json +10 -8
package/dist/client/index.html
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
--pi-terminal-text: #e6edf3;
|
|
22
22
|
--pi-border: #30363d;
|
|
23
23
|
--pi-border-muted: #21262d;
|
|
24
|
+
--pi-hierarchy-border: #5c6b80;
|
|
24
25
|
--pi-text: #e6edf3;
|
|
25
26
|
--pi-text-secondary: #c9d1d9;
|
|
26
27
|
--pi-text-bright: #f0f6fc;
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
html, body { margin: 0; height: 100%; overflow: hidden; background: var(--pi-bg); color: var(--pi-text); }
|
|
54
55
|
body { min-height: 100dvh; }
|
|
55
56
|
</style>
|
|
56
|
-
<script type="module" crossorigin src="./assets/index-
|
|
57
|
+
<script type="module" crossorigin src="./assets/index-6Nw3vRPS.js"></script>
|
|
57
58
|
<link rel="modulepreload" crossorigin href="./assets/vendor-editor-core-vUi74DnD.js">
|
|
58
59
|
<link rel="modulepreload" crossorigin href="./assets/vendor-editor-languages-DoaXUodB.js">
|
|
59
60
|
<link rel="modulepreload" crossorigin href="./assets/vendor-terminal-D8k4UKM2.js">
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// Generated from pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelElement.ts. Do not edit directly.
|
|
2
|
+
import { MAX_LIST_WIDTH, MIN_LIST_WIDTH, clampLearnedSkillsListWidth, readLearnedSkillsListWidth, writeLearnedSkillsListWidth, } from "./learnedSkillsPanelLayout.js";
|
|
3
|
+
export const learnedSkillsPanelTagName = "pi-webui-learned-skills-panel";
|
|
4
|
+
export function learnedSkillsBadge(state) {
|
|
5
|
+
if (state.kind !== "data")
|
|
6
|
+
return undefined;
|
|
7
|
+
const total = state.globalSkills.length + state.projectSkills.length;
|
|
8
|
+
return total > 0 ? total : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function isLearnedSkillsPanelVisible(state) {
|
|
11
|
+
return state.kind !== "unavailable";
|
|
12
|
+
}
|
|
13
|
+
export function defineLearnedSkillsPanelElement() {
|
|
14
|
+
if (typeof customElements !== "undefined" && !customElements.get(learnedSkillsPanelTagName)) {
|
|
15
|
+
customElements.define(learnedSkillsPanelTagName, PiWebUiLearnedSkillsPanel);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Keep the plugin importable in the Node-based contribution tests, where the
|
|
19
|
+
// browser element globals are intentionally absent.
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
21
|
+
class NoopElement {
|
|
22
|
+
}
|
|
23
|
+
function noopElementConstructor() {
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
25
|
+
return NoopElement;
|
|
26
|
+
}
|
|
27
|
+
const BaseElement = typeof HTMLElement === "undefined"
|
|
28
|
+
? noopElementConstructor()
|
|
29
|
+
: HTMLElement;
|
|
30
|
+
class PiWebUiLearnedSkillsPanel extends BaseElement {
|
|
31
|
+
state = { kind: "loading" };
|
|
32
|
+
retry;
|
|
33
|
+
selectedSkillId;
|
|
34
|
+
showMobileDetail = false;
|
|
35
|
+
pointerInteraction;
|
|
36
|
+
resizeObserver;
|
|
37
|
+
connected = false;
|
|
38
|
+
preferredListWidth;
|
|
39
|
+
listWidth;
|
|
40
|
+
root;
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
this.root = this.attachShadow({ mode: "open" });
|
|
44
|
+
this.preferredListWidth = readLearnedSkillsListWidth();
|
|
45
|
+
this.listWidth = this.preferredListWidth;
|
|
46
|
+
}
|
|
47
|
+
set context(_value) {
|
|
48
|
+
// Activity Rail supplies context for the host boundary. The panel reads
|
|
49
|
+
// only the already-shaped snapshot and retry callback below.
|
|
50
|
+
}
|
|
51
|
+
set learnedSkillsState(value) {
|
|
52
|
+
if (this.state === value)
|
|
53
|
+
return;
|
|
54
|
+
this.state = value;
|
|
55
|
+
this.reconcileSelection();
|
|
56
|
+
this.render();
|
|
57
|
+
}
|
|
58
|
+
set onRetry(value) {
|
|
59
|
+
this.retry = value;
|
|
60
|
+
}
|
|
61
|
+
connectedCallback() {
|
|
62
|
+
if (this.connected)
|
|
63
|
+
return;
|
|
64
|
+
this.connected = true;
|
|
65
|
+
if (typeof window !== "undefined") {
|
|
66
|
+
window.addEventListener("resize", this.handleWindowResize);
|
|
67
|
+
window.addEventListener("pointermove", this.handlePointerMove);
|
|
68
|
+
window.addEventListener("pointerup", this.handlePointerUp);
|
|
69
|
+
window.addEventListener("pointercancel", this.handlePointerCancel);
|
|
70
|
+
}
|
|
71
|
+
this.render();
|
|
72
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
73
|
+
this.resizeObserver = new ResizeObserver(this.handleObservedResize);
|
|
74
|
+
this.resizeObserver.observe(this);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
disconnectedCallback() {
|
|
78
|
+
this.finishPointerInteraction(false);
|
|
79
|
+
this.resizeObserver?.disconnect();
|
|
80
|
+
this.resizeObserver = undefined;
|
|
81
|
+
if (this.connected && typeof window !== "undefined") {
|
|
82
|
+
window.removeEventListener("resize", this.handleWindowResize);
|
|
83
|
+
window.removeEventListener("pointermove", this.handlePointerMove);
|
|
84
|
+
window.removeEventListener("pointerup", this.handlePointerUp);
|
|
85
|
+
window.removeEventListener("pointercancel", this.handlePointerCancel);
|
|
86
|
+
}
|
|
87
|
+
this.connected = false;
|
|
88
|
+
}
|
|
89
|
+
handleObservedResize = () => {
|
|
90
|
+
if (this.connected)
|
|
91
|
+
this.handleWindowResize();
|
|
92
|
+
};
|
|
93
|
+
handleWindowResize = () => {
|
|
94
|
+
if (this.pointerInteraction === undefined)
|
|
95
|
+
this.applyPreferredListWidth();
|
|
96
|
+
else
|
|
97
|
+
this.updateSeparator();
|
|
98
|
+
};
|
|
99
|
+
handlePointerDown = (event) => {
|
|
100
|
+
if (event.button !== 0)
|
|
101
|
+
return;
|
|
102
|
+
const divider = event.currentTarget;
|
|
103
|
+
if (!(divider instanceof HTMLElement))
|
|
104
|
+
return;
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
this.finishPointerInteraction(false);
|
|
107
|
+
this.pointerInteraction = {
|
|
108
|
+
pointerId: event.pointerId,
|
|
109
|
+
startX: event.clientX,
|
|
110
|
+
startWidth: this.listWidth,
|
|
111
|
+
divider,
|
|
112
|
+
};
|
|
113
|
+
divider.classList.add("dragging");
|
|
114
|
+
callPointerMethod(divider, "setPointerCapture", event.pointerId);
|
|
115
|
+
};
|
|
116
|
+
handlePointerMove = (event) => {
|
|
117
|
+
const interaction = this.pointerInteraction;
|
|
118
|
+
if (interaction?.pointerId !== event.pointerId)
|
|
119
|
+
return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
this.updatePreferredListWidth(interaction.startWidth + event.clientX - interaction.startX);
|
|
122
|
+
};
|
|
123
|
+
handlePointerUp = (event) => {
|
|
124
|
+
if (this.pointerInteraction?.pointerId !== event.pointerId)
|
|
125
|
+
return;
|
|
126
|
+
this.finishPointerInteraction(true);
|
|
127
|
+
};
|
|
128
|
+
handlePointerCancel = (event) => {
|
|
129
|
+
if (this.pointerInteraction?.pointerId !== event.pointerId)
|
|
130
|
+
return;
|
|
131
|
+
this.finishPointerInteraction(false);
|
|
132
|
+
};
|
|
133
|
+
render() {
|
|
134
|
+
this.finishPointerInteraction(false);
|
|
135
|
+
this.applyPreferredListWidth();
|
|
136
|
+
this.root.innerHTML = `${panelStyles()}${renderPanelState(this.state, this.selectedSkillId, this.showMobileDetail)}`;
|
|
137
|
+
this.attachEventListeners();
|
|
138
|
+
this.updateSeparator();
|
|
139
|
+
}
|
|
140
|
+
attachEventListeners() {
|
|
141
|
+
for (const button of this.root.querySelectorAll("button[data-retry]")) {
|
|
142
|
+
button.addEventListener("click", () => { this.retry?.(); });
|
|
143
|
+
}
|
|
144
|
+
for (const button of this.root.querySelectorAll("button[data-skill-key]")) {
|
|
145
|
+
button.addEventListener("click", () => {
|
|
146
|
+
const key = button.dataset["skillKey"];
|
|
147
|
+
if (key !== undefined)
|
|
148
|
+
this.selectSkill(key);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
this.root.querySelector("button[data-back]")?.addEventListener("click", () => {
|
|
152
|
+
this.showMobileDetail = false;
|
|
153
|
+
this.render();
|
|
154
|
+
});
|
|
155
|
+
const divider = this.root.querySelector("[role=separator]");
|
|
156
|
+
divider?.addEventListener("pointerdown", this.handlePointerDown);
|
|
157
|
+
divider?.addEventListener("keydown", this.handleDividerKeyDown);
|
|
158
|
+
}
|
|
159
|
+
handleDividerKeyDown = (event) => {
|
|
160
|
+
const step = event.shiftKey ? 72 : 24;
|
|
161
|
+
let nextWidth;
|
|
162
|
+
if (event.key === "ArrowLeft")
|
|
163
|
+
nextWidth = this.listWidth - step;
|
|
164
|
+
else if (event.key === "ArrowRight")
|
|
165
|
+
nextWidth = this.listWidth + step;
|
|
166
|
+
else if (event.key === "Home")
|
|
167
|
+
nextWidth = MIN_LIST_WIDTH;
|
|
168
|
+
else if (event.key === "End")
|
|
169
|
+
nextWidth = this.runtimeMaximumWidth();
|
|
170
|
+
if (nextWidth === undefined)
|
|
171
|
+
return;
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
this.updatePreferredListWidth(nextWidth, true);
|
|
174
|
+
};
|
|
175
|
+
selectSkill(key) {
|
|
176
|
+
if (findSelectedSkill(this.state, key) === undefined)
|
|
177
|
+
return;
|
|
178
|
+
this.selectedSkillId = key;
|
|
179
|
+
this.showMobileDetail = true;
|
|
180
|
+
this.render();
|
|
181
|
+
}
|
|
182
|
+
reconcileSelection() {
|
|
183
|
+
if (this.selectedSkillId !== undefined && findSelectedSkill(this.state, this.selectedSkillId) !== undefined)
|
|
184
|
+
return;
|
|
185
|
+
this.selectedSkillId = undefined;
|
|
186
|
+
this.showMobileDetail = false;
|
|
187
|
+
}
|
|
188
|
+
applyPreferredListWidth() {
|
|
189
|
+
this.applyEffectiveListWidth(this.preferredListWidth);
|
|
190
|
+
}
|
|
191
|
+
applyEffectiveListWidth(width) {
|
|
192
|
+
const containerWidth = this.isNarrowViewport() ? undefined : this.knownContainerWidth();
|
|
193
|
+
this.listWidth = clampLearnedSkillsListWidth(width, containerWidth);
|
|
194
|
+
this.style.setProperty("--learned-skills-list-width", `${String(this.listWidth)}px`);
|
|
195
|
+
this.updateSeparator();
|
|
196
|
+
}
|
|
197
|
+
updatePreferredListWidth(width, persist = false) {
|
|
198
|
+
this.applyEffectiveListWidth(width);
|
|
199
|
+
this.preferredListWidth = this.listWidth;
|
|
200
|
+
if (persist)
|
|
201
|
+
writeLearnedSkillsListWidth(this.preferredListWidth);
|
|
202
|
+
}
|
|
203
|
+
updateSeparator() {
|
|
204
|
+
const divider = this.root.querySelector("[role=separator]");
|
|
205
|
+
if (divider === null)
|
|
206
|
+
return;
|
|
207
|
+
divider.setAttribute("aria-valuenow", String(this.listWidth));
|
|
208
|
+
divider.setAttribute("aria-valuemax", String(this.runtimeMaximumWidth()));
|
|
209
|
+
}
|
|
210
|
+
runtimeMaximumWidth() {
|
|
211
|
+
if (this.isNarrowViewport())
|
|
212
|
+
return MAX_LIST_WIDTH;
|
|
213
|
+
return clampLearnedSkillsListWidth(MAX_LIST_WIDTH, this.knownContainerWidth());
|
|
214
|
+
}
|
|
215
|
+
knownContainerWidth() {
|
|
216
|
+
try {
|
|
217
|
+
const width = this.getBoundingClientRect().width;
|
|
218
|
+
if (Number.isFinite(width) && width > 0)
|
|
219
|
+
return width;
|
|
220
|
+
const clientWidth = this.clientWidth;
|
|
221
|
+
return clientWidth > 0 ? clientWidth : undefined;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
isNarrowViewport() {
|
|
228
|
+
if (typeof window === "undefined")
|
|
229
|
+
return false;
|
|
230
|
+
try {
|
|
231
|
+
return window.matchMedia("(max-width: 760px)").matches;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return window.innerWidth <= 760;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
finishPointerInteraction(persist) {
|
|
238
|
+
const interaction = this.pointerInteraction;
|
|
239
|
+
if (interaction === undefined)
|
|
240
|
+
return;
|
|
241
|
+
this.pointerInteraction = undefined;
|
|
242
|
+
interaction.divider.classList.remove("dragging");
|
|
243
|
+
callPointerMethod(interaction.divider, "releasePointerCapture", interaction.pointerId);
|
|
244
|
+
if (persist)
|
|
245
|
+
writeLearnedSkillsListWidth(this.preferredListWidth);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function renderPanelState(state, selectedSkillId, showMobileDetail) {
|
|
249
|
+
if (state.kind === "loading")
|
|
250
|
+
return `<section class="state-screen" role="status">Loading learned skills...</section>`;
|
|
251
|
+
if (state.kind === "unavailable")
|
|
252
|
+
return `<section class="state-screen">Learned skills are unavailable.</section>`;
|
|
253
|
+
if (state.kind === "error") {
|
|
254
|
+
return `<section class="state-screen error" role="alert">
|
|
255
|
+
<p>${escapeHtml(state.message)}</p>
|
|
256
|
+
<button type="button" class="secondary" data-retry>Retry</button>
|
|
257
|
+
</section>`;
|
|
258
|
+
}
|
|
259
|
+
const groups = [
|
|
260
|
+
renderSkillGroup("project", "PROJECT", state.projectSkills, state.projectUnavailableMessage, selectedSkillId),
|
|
261
|
+
renderSkillGroup("global", "GLOBAL", state.globalSkills, undefined, selectedSkillId),
|
|
262
|
+
].filter((group) => group !== "").join("");
|
|
263
|
+
const empty = groups === "" ? `<p class="empty-list">No learned skills found.</p>` : "";
|
|
264
|
+
const warning = state.refreshError === undefined
|
|
265
|
+
? ""
|
|
266
|
+
: `<div class="refresh-warning" role="status">${escapeHtml(state.refreshError)}</div>`;
|
|
267
|
+
const retry = state.refreshError === undefined ? "" : `<button type="button" class="secondary refresh-retry" data-retry>Retry</button>`;
|
|
268
|
+
return `<section class="learned-skills-panel${showMobileDetail ? " show-mobile-detail" : ""}">
|
|
269
|
+
<section class="skill-list" aria-label="Learned skills list">
|
|
270
|
+
${warning}
|
|
271
|
+
${groups}
|
|
272
|
+
${empty}
|
|
273
|
+
${retry}
|
|
274
|
+
</section>
|
|
275
|
+
<div
|
|
276
|
+
class="skill-divider"
|
|
277
|
+
role="separator"
|
|
278
|
+
aria-label="Resize learned skills list"
|
|
279
|
+
aria-orientation="vertical"
|
|
280
|
+
aria-valuemin="${String(MIN_LIST_WIDTH)}"
|
|
281
|
+
aria-valuemax="${String(MAX_LIST_WIDTH)}"
|
|
282
|
+
aria-valuenow="280"
|
|
283
|
+
tabindex="0"
|
|
284
|
+
></div>
|
|
285
|
+
<section class="skill-detail" aria-label="Learned skill details">
|
|
286
|
+
${renderSkillDetail(state, selectedSkillId)}
|
|
287
|
+
</section>
|
|
288
|
+
</section>`;
|
|
289
|
+
}
|
|
290
|
+
function renderSkillGroup(scope, title, skills, unavailableMessage, selectedSkillId) {
|
|
291
|
+
if (skills.length === 0 && unavailableMessage === undefined)
|
|
292
|
+
return "";
|
|
293
|
+
const warning = unavailableMessage === undefined
|
|
294
|
+
? ""
|
|
295
|
+
: `<p class="group-warning" role="status">${escapeHtml(unavailableMessage)}</p>`;
|
|
296
|
+
const rows = skills.map((skill) => renderSkillRow(scope, skill, selectedSkillId)).join("");
|
|
297
|
+
return `<section class="skill-group" data-skill-group="${scope}">
|
|
298
|
+
<header class="skill-group-header">
|
|
299
|
+
<h2>${title}</h2>
|
|
300
|
+
<span class="skill-count">${String(skills.length)}</span>
|
|
301
|
+
</header>
|
|
302
|
+
${warning}
|
|
303
|
+
${rows}
|
|
304
|
+
</section>`;
|
|
305
|
+
}
|
|
306
|
+
function renderSkillRow(scope, skill, selectedSkillId) {
|
|
307
|
+
const key = skillKey(scope, skill.id);
|
|
308
|
+
const selected = key === selectedSkillId;
|
|
309
|
+
return `<button type="button" class="skill-row${selected ? " selected" : ""}" data-skill-key="${escapeAttribute(key)}" aria-pressed="${String(selected)}">
|
|
310
|
+
<span class="skill-row-name">${escapeHtml(skill.name)}</span>
|
|
311
|
+
<span class="skill-row-description">${escapeHtml(skill.description)}</span>
|
|
312
|
+
</button>`;
|
|
313
|
+
}
|
|
314
|
+
function renderSkillDetail(state, selectedSkillId) {
|
|
315
|
+
const selected = selectedSkillId === undefined ? undefined : findSelectedSkill(state, selectedSkillId);
|
|
316
|
+
if (selected === undefined)
|
|
317
|
+
return `<div class="detail-empty">Select a skill</div>`;
|
|
318
|
+
const { scope, skill } = selected;
|
|
319
|
+
const metadata = [
|
|
320
|
+
`<div><dt>Path</dt><dd><code>${escapeHtml(skill.filePath)}</code></dd></div>`,
|
|
321
|
+
...(skill.version === undefined ? [] : [`<div><dt>Version</dt><dd>${escapeHtml(skill.version)}</dd></div>`]),
|
|
322
|
+
...(skill.created === undefined ? [] : [`<div><dt>Created</dt><dd>${escapeHtml(skill.created)}</dd></div>`]),
|
|
323
|
+
...(skill.updated === undefined ? [] : [`<div><dt>Updated</dt><dd>${escapeHtml(skill.updated)}</dd></div>`]),
|
|
324
|
+
].join("");
|
|
325
|
+
return `<div class="detail-content">
|
|
326
|
+
<button type="button" class="mobile-back" data-back aria-label="Back to learned skills" title="Back to learned skills">
|
|
327
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
328
|
+
<path d="m15 18-6-6 6-6"></path>
|
|
329
|
+
</svg>
|
|
330
|
+
</button>
|
|
331
|
+
<div class="skill-scope">${scope.toUpperCase()}</div>
|
|
332
|
+
<h2>${escapeHtml(skill.name)}</h2>
|
|
333
|
+
<p class="skill-description">${escapeHtml(skill.description)}</p>
|
|
334
|
+
<dl class="skill-metadata">${metadata}</dl>
|
|
335
|
+
</div>`;
|
|
336
|
+
}
|
|
337
|
+
function findSelectedSkill(state, key) {
|
|
338
|
+
if (state.kind !== "data")
|
|
339
|
+
return undefined;
|
|
340
|
+
const project = state.projectSkills.find((skill) => skillKey("project", skill.id) === key);
|
|
341
|
+
if (project !== undefined)
|
|
342
|
+
return { scope: "project", skill: project };
|
|
343
|
+
const global = state.globalSkills.find((skill) => skillKey("global", skill.id) === key);
|
|
344
|
+
return global === undefined ? undefined : { scope: "global", skill: global };
|
|
345
|
+
}
|
|
346
|
+
function skillKey(scope, id) {
|
|
347
|
+
return `${scope}:${id}`;
|
|
348
|
+
}
|
|
349
|
+
function panelStyles() {
|
|
350
|
+
return `<style>
|
|
351
|
+
:host { display: block; height: 100%; min-height: 0; color: var(--pi-text); font: 14px system-ui, sans-serif; }
|
|
352
|
+
* { box-sizing: border-box; }
|
|
353
|
+
.learned-skills-panel { display: grid; grid-template-columns: var(--learned-skills-list-width) 8px minmax(320px, 1fr); height: 100%; min-height: 0; overflow: hidden; }
|
|
354
|
+
.skill-list, .skill-detail { min-width: 0; min-height: 0; overflow: auto; }
|
|
355
|
+
.skill-list { padding: 2px 12px 12px 0; }
|
|
356
|
+
.skill-detail { padding: 2px 0 12px 16px; border-left: 0; }
|
|
357
|
+
.skill-group { margin-bottom: 16px; }
|
|
358
|
+
.skill-group-header { display: flex; align-items: center; gap: 8px; min-height: 28px; padding: 0 4px 6px; border-bottom: 1px solid var(--pi-border); }
|
|
359
|
+
.skill-group-header h2 { flex: 1 1 auto; min-width: 0; margin: 0; color: var(--pi-muted); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; }
|
|
360
|
+
.skill-count { flex: 0 0 auto; color: var(--pi-muted); font-size: 12px; }
|
|
361
|
+
.skill-row { display: block; width: 100%; min-width: 0; margin: 4px 0 0; padding: 9px 10px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--pi-text); cursor: pointer; text-align: left; font: inherit; }
|
|
362
|
+
.skill-row:hover { background: var(--pi-surface-hover); }
|
|
363
|
+
.skill-row:focus-visible, .mobile-back:focus-visible, .skill-divider:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; }
|
|
364
|
+
.skill-row.selected { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
|
|
365
|
+
.skill-row-name { display: block; overflow: hidden; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
|
366
|
+
.skill-row-description { display: block; margin-top: 3px; overflow: hidden; color: var(--pi-muted); font-size: 12px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
|
367
|
+
.skill-divider { position: relative; min-width: 8px; border: 0; background: transparent; cursor: col-resize; touch-action: none; }
|
|
368
|
+
.skill-divider::before { position: absolute; top: 0; bottom: 0; left: 3px; width: 2px; background: var(--pi-border); content: ""; }
|
|
369
|
+
.skill-divider:hover::before, .skill-divider:focus-visible::before, .skill-divider.dragging::before { background: var(--pi-accent); }
|
|
370
|
+
.detail-content { max-width: 760px; padding: 2px 12px 12px 0; }
|
|
371
|
+
.detail-content h2 { margin: 0 0 12px; overflow-wrap: anywhere; font-size: 22px; line-height: 1.25; }
|
|
372
|
+
.skill-scope { margin-bottom: 6px; color: var(--pi-accent); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; }
|
|
373
|
+
.skill-description { margin: 0 0 20px; max-width: 70ch; line-height: 1.55; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
374
|
+
.skill-metadata { display: grid; gap: 8px; margin: 0; color: var(--pi-muted); }
|
|
375
|
+
.skill-metadata > div { display: grid; grid-template-columns: minmax(70px, auto) minmax(0, 1fr); gap: 12px; }
|
|
376
|
+
.skill-metadata dt { font-weight: 600; }
|
|
377
|
+
.skill-metadata dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
|
|
378
|
+
.skill-metadata code { color: var(--pi-text-secondary); font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
379
|
+
.detail-empty, .empty-list, .state-screen { color: var(--pi-muted); }
|
|
380
|
+
.detail-empty { display: grid; min-height: 180px; place-items: center; padding: 24px; text-align: center; }
|
|
381
|
+
.empty-list { padding: 20px 4px; }
|
|
382
|
+
.state-screen { display: grid; min-height: 160px; place-items: center; gap: 12px; padding: 24px; text-align: center; }
|
|
383
|
+
.state-screen.error { color: var(--pi-danger); }
|
|
384
|
+
.state-screen p { margin: 0; overflow-wrap: anywhere; }
|
|
385
|
+
.refresh-warning, .group-warning { color: var(--pi-warning, #b45309); line-height: 1.4; overflow-wrap: anywhere; }
|
|
386
|
+
.refresh-warning { margin: 0 0 8px; padding: 8px 10px; border-left: 2px solid var(--pi-warning, #b45309); }
|
|
387
|
+
.group-warning { margin: 8px 4px; }
|
|
388
|
+
button.secondary { border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); cursor: pointer; padding: 7px 10px; font: inherit; }
|
|
389
|
+
button.secondary:hover { background: var(--pi-surface-hover); }
|
|
390
|
+
.refresh-retry { margin: 0 0 12px 4px; }
|
|
391
|
+
.mobile-back { display: none; width: 32px; height: 32px; margin: 0 0 12px; padding: 5px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-text); cursor: pointer; }
|
|
392
|
+
.mobile-back svg { display: block; width: 20px; height: 20px; }
|
|
393
|
+
|
|
394
|
+
@media (max-width: 760px) {
|
|
395
|
+
.learned-skills-panel { display: block; }
|
|
396
|
+
.skill-list, .skill-detail { height: 100%; padding: 0; }
|
|
397
|
+
.skill-detail { display: none; }
|
|
398
|
+
.learned-skills-panel.show-mobile-detail .skill-list { display: none; }
|
|
399
|
+
.learned-skills-panel.show-mobile-detail .skill-detail { display: block; }
|
|
400
|
+
.skill-divider { display: none; }
|
|
401
|
+
.mobile-back { display: block; }
|
|
402
|
+
}
|
|
403
|
+
</style>`;
|
|
404
|
+
}
|
|
405
|
+
function escapeHtml(value) {
|
|
406
|
+
return String(value)
|
|
407
|
+
.replaceAll("&", "&")
|
|
408
|
+
.replaceAll("<", "<")
|
|
409
|
+
.replaceAll(">", ">")
|
|
410
|
+
.replaceAll('"', """)
|
|
411
|
+
.replaceAll("'", "'");
|
|
412
|
+
}
|
|
413
|
+
function escapeAttribute(value) {
|
|
414
|
+
return escapeHtml(value);
|
|
415
|
+
}
|
|
416
|
+
function callPointerMethod(element, methodName, pointerId) {
|
|
417
|
+
const method = Reflect.get(element, methodName);
|
|
418
|
+
if (typeof method === "function")
|
|
419
|
+
method.call(element, pointerId);
|
|
420
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Generated from pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelLayout.ts. Do not edit directly.
|
|
2
|
+
export const LEARNED_SKILLS_LAYOUT_STORAGE_KEY = "pi-webui:workspace-learned-skills:layout:v1";
|
|
3
|
+
export const DEFAULT_LIST_WIDTH = 280;
|
|
4
|
+
export const MIN_LIST_WIDTH = 190;
|
|
5
|
+
export const MAX_LIST_WIDTH = 440;
|
|
6
|
+
export const MIN_DETAIL_WIDTH = 320;
|
|
7
|
+
export const DIVIDER_WIDTH = 8;
|
|
8
|
+
export function readLearnedSkillsListWidth(storage) {
|
|
9
|
+
const source = storage ?? browserStorage();
|
|
10
|
+
if (source === undefined)
|
|
11
|
+
return DEFAULT_LIST_WIDTH;
|
|
12
|
+
try {
|
|
13
|
+
const raw = source.getItem(LEARNED_SKILLS_LAYOUT_STORAGE_KEY);
|
|
14
|
+
if (raw === null)
|
|
15
|
+
return DEFAULT_LIST_WIDTH;
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (!isLayoutEnvelope(parsed) || !Number.isFinite(parsed.listWidth))
|
|
18
|
+
return DEFAULT_LIST_WIDTH;
|
|
19
|
+
return clampLearnedSkillsListWidth(parsed.listWidth);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return DEFAULT_LIST_WIDTH;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function writeLearnedSkillsListWidth(width, storage) {
|
|
26
|
+
const source = storage ?? browserStorage();
|
|
27
|
+
if (source === undefined)
|
|
28
|
+
return;
|
|
29
|
+
const envelope = {
|
|
30
|
+
version: 1,
|
|
31
|
+
listWidth: clampLearnedSkillsListWidth(width),
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
source.setItem(LEARNED_SKILLS_LAYOUT_STORAGE_KEY, JSON.stringify(envelope));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Browser privacy modes and full quotas should not break the panel.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function clampLearnedSkillsListWidth(width, containerWidth) {
|
|
41
|
+
const safeWidth = Number.isFinite(width) ? width : DEFAULT_LIST_WIDTH;
|
|
42
|
+
let maximum = MAX_LIST_WIDTH;
|
|
43
|
+
if (containerWidth !== undefined && Number.isFinite(containerWidth)) {
|
|
44
|
+
maximum = Math.min(MAX_LIST_WIDTH, Math.floor(containerWidth - MIN_DETAIL_WIDTH - DIVIDER_WIDTH));
|
|
45
|
+
}
|
|
46
|
+
// A desktop container is expected to be wide enough for the minimum detail
|
|
47
|
+
// pane. Keep the minimum as the final guard for transient narrow geometry.
|
|
48
|
+
maximum = Math.max(MIN_LIST_WIDTH, maximum);
|
|
49
|
+
return Math.round(Math.min(maximum, Math.max(MIN_LIST_WIDTH, safeWidth)));
|
|
50
|
+
}
|
|
51
|
+
function browserStorage() {
|
|
52
|
+
try {
|
|
53
|
+
if (typeof globalThis.localStorage === "undefined")
|
|
54
|
+
return undefined;
|
|
55
|
+
return globalThis.localStorage;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isLayoutEnvelope(value) {
|
|
62
|
+
if (!isRecord(value))
|
|
63
|
+
return false;
|
|
64
|
+
return value["version"] === 1 && typeof value["listWidth"] === "number";
|
|
65
|
+
}
|
|
66
|
+
function isRecord(value) {
|
|
67
|
+
return typeof value === "object" && value !== null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated from pi-webui-plugins/workspace-learned-skills/pi-webui-plugin.ts. Do not edit directly.
|
|
2
|
+
import { defineLearnedSkillsPanelElement, isLearnedSkillsPanelVisible, learnedSkillsBadge } from "./learnedSkillsPanelElement.js";
|
|
3
|
+
// Keep the core-owned state dependency at the public Activity Rail boundary.
|
|
4
|
+
function bundledLearnedSkillsContext(context) {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
6
|
+
return context;
|
|
7
|
+
}
|
|
8
|
+
const plugin = {
|
|
9
|
+
apiVersion: 1,
|
|
10
|
+
name: "Workspace Learned Skills",
|
|
11
|
+
activate: ({ html, svg }) => {
|
|
12
|
+
defineLearnedSkillsPanelElement();
|
|
13
|
+
return {
|
|
14
|
+
contributions: {
|
|
15
|
+
activityRailItems: [
|
|
16
|
+
{
|
|
17
|
+
id: "workspace.learned-skills",
|
|
18
|
+
title: "Learned Skills",
|
|
19
|
+
icon: svg `
|
|
20
|
+
<svg data-icon="lightbulb" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<path d="M9 18h6"></path>
|
|
22
|
+
<path d="M10 22h4"></path>
|
|
23
|
+
<path d="M15.09 14c.18-.69.66-1.22 1.19-1.75A6 6 0 1 0 7.72 12.25c.52.52 1 1.05 1.18 1.75"></path>
|
|
24
|
+
<path d="M9 14h6"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
`,
|
|
27
|
+
order: 51,
|
|
28
|
+
visible: (context) => {
|
|
29
|
+
const learned = bundledLearnedSkillsContext(context);
|
|
30
|
+
return context.workspaceScope !== undefined
|
|
31
|
+
&& isLearnedSkillsPanelVisible(learned.state.learnedSkills);
|
|
32
|
+
},
|
|
33
|
+
badge: (context) => learnedSkillsBadge(bundledLearnedSkillsContext(context).state.learnedSkills),
|
|
34
|
+
render: (context) => {
|
|
35
|
+
const learned = bundledLearnedSkillsContext(context);
|
|
36
|
+
return html `<pi-webui-learned-skills-panel
|
|
37
|
+
.context=${context}
|
|
38
|
+
.learnedSkillsState=${learned.state.learnedSkills}
|
|
39
|
+
.onRetry=${learned.onRefreshLearnedSkills}
|
|
40
|
+
></pi-webui-learned-skills-panel>`;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
export default plugin;
|
package/dist/server/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import fastifyCompress from "@fastify/compress";
|
|
|
6
6
|
import fastifyStatic from "@fastify/static";
|
|
7
7
|
import fastifyWebsocket from "@fastify/websocket";
|
|
8
8
|
import { ProjectStore } from "./storage/projectStore.js";
|
|
9
|
-
import { ProjectService } from "./projects/projectService.js";
|
|
9
|
+
import { ProjectNotFoundError, ProjectService } from "./projects/projectService.js";
|
|
10
10
|
import { WorkspaceService } from "./workspaces/workspaceService.js";
|
|
11
11
|
import { isAbsoluteishFileSuggestionQuery, listFileSuggestions, listPathSuggestions } from "./workspaces/fileSuggestions.js";
|
|
12
12
|
import { pathAccessForCwd } from "./workspaces/effectivePathAccess.js";
|
|
@@ -32,6 +32,7 @@ import { getPiWebUiRuntime, getPiWebUiStatus, getPiWebUiVersionStatus } from "./
|
|
|
32
32
|
import { ActiveAgentProfileAccessError, requireActiveAgentProfile, SessionDaemonActiveAgentProfileProvider, } from "./activeAgentProfileProvider.js";
|
|
33
33
|
import { MachineService } from "./machines/machineService.js";
|
|
34
34
|
import { registerMemoryRoutes } from "./memory/memoryRoutes.js";
|
|
35
|
+
import { registerLearnedSkillsRoutes } from "./learnedSkills/learnedSkillsRoutes.js";
|
|
35
36
|
import { registerMachineRoutes } from "./machines/machineRoutes.js";
|
|
36
37
|
import { registerMachineProxyRoutes } from "./machines/machineProxyRoutes.js";
|
|
37
38
|
import { proxyMachinePluginAsset, registerMachinePluginProxyRoutes } from "./machines/machinePluginProxyRoutes.js";
|
|
@@ -51,7 +52,31 @@ function registerLocalProjectRoutes(app, projects, workspaces, prefix, options =
|
|
|
51
52
|
return { closed: true };
|
|
52
53
|
}
|
|
53
54
|
catch (error) {
|
|
54
|
-
return reply
|
|
55
|
+
return sendProjectRouteError(reply, error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
app.post(`${prefix}/projects/:projectId/pin`, async (request, reply) => {
|
|
59
|
+
try {
|
|
60
|
+
return await projects.pin(request.params.projectId);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return sendProjectRouteError(reply, error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
app.post(`${prefix}/projects/:projectId/unpin`, async (request, reply) => {
|
|
67
|
+
try {
|
|
68
|
+
return await projects.unpin(request.params.projectId);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return sendProjectRouteError(reply, error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
app.post(`${prefix}/projects/:projectId/close-tree`, async (request, reply) => {
|
|
75
|
+
try {
|
|
76
|
+
return await projects.closeTree(request.params.projectId);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return sendProjectRouteError(reply, error);
|
|
55
80
|
}
|
|
56
81
|
});
|
|
57
82
|
app.get(`${prefix}/project-directories`, async (request, reply) => {
|
|
@@ -68,10 +93,19 @@ function registerLocalProjectRoutes(app, projects, workspaces, prefix, options =
|
|
|
68
93
|
return await listWorkspacesWithEffectiveConfig(project, workspaces, options.config);
|
|
69
94
|
}
|
|
70
95
|
catch (error) {
|
|
71
|
-
return reply
|
|
96
|
+
return sendProjectRouteError(reply, error);
|
|
72
97
|
}
|
|
73
98
|
});
|
|
74
99
|
}
|
|
100
|
+
function sendProjectRouteError(reply, error) {
|
|
101
|
+
// Only unknown project ids answer 404. Genuine store or workspace failures
|
|
102
|
+
// (git, filesystem) answer 500 so clients can distinguish them. The wider
|
|
103
|
+
// resolveWorkspaceContext consumers (git, workspace explorer, terminal, and
|
|
104
|
+
// workspace deletion routes) deliberately keep their own catch-all mapping
|
|
105
|
+
// instead of this instanceof split, so the asymmetry is not an oversight.
|
|
106
|
+
const status = error instanceof ProjectNotFoundError ? 404 : 500;
|
|
107
|
+
return reply.code(status).send({ error: error instanceof Error ? error.message : String(error) });
|
|
108
|
+
}
|
|
75
109
|
async function listWorkspacesWithEffectiveConfig(project, workspaces, config) {
|
|
76
110
|
const [workspaceList, effectiveConfig] = await Promise.all([
|
|
77
111
|
workspaces.list(project),
|
|
@@ -192,6 +226,8 @@ export async function buildApp(deps = {}) {
|
|
|
192
226
|
registerLocalMachineConfigRoutes(app, invalidatingConfigService);
|
|
193
227
|
registerMemoryRoutes(app, agentProfileProvider, "/api");
|
|
194
228
|
registerMemoryRoutes(app, agentProfileProvider, "/api/machines/local");
|
|
229
|
+
registerLearnedSkillsRoutes(app, agentProfileProvider, "/api");
|
|
230
|
+
registerLearnedSkillsRoutes(app, agentProfileProvider, "/api/machines/local");
|
|
195
231
|
registerMachineRoutes(app, machines);
|
|
196
232
|
registerMachinePluginProxyRoutes(app, machines);
|
|
197
233
|
registerLocalProjectRoutes(app, projects, workspaces, "/api", { config: configService });
|