@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.
Files changed (52) hide show
  1. package/README.md +3 -1
  2. package/dist/client/assets/{CodeViewer-BRcVinSx.js → CodeViewer-RC12gm-a.js} +1 -1
  3. package/dist/client/assets/{UnifiedDiffViewer-ChxwNz6T.js → UnifiedDiffViewer-BKkTUzo8.js} +1 -1
  4. package/dist/client/assets/{index-DG-QGbW9.js → index-6Nw3vRPS.js} +540 -477
  5. package/dist/client/index.html +2 -1
  6. package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsData.js +2 -0
  7. package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelElement.js +420 -0
  8. package/dist/pi-webui-plugins/workspace-learned-skills/learnedSkillsPanelLayout.js +68 -0
  9. package/dist/pi-webui-plugins/workspace-learned-skills/package.json +9 -0
  10. package/dist/pi-webui-plugins/workspace-learned-skills/pi-webui-plugin.js +48 -0
  11. package/dist/server/app.js +39 -3
  12. package/dist/server/app.js.map +1 -1
  13. package/dist/server/learnedSkills/learnedSkillCatalog.js +66 -0
  14. package/dist/server/learnedSkills/learnedSkillCatalog.js.map +1 -0
  15. package/dist/server/learnedSkills/learnedSkillProvider.js +2 -0
  16. package/dist/server/learnedSkills/learnedSkillProvider.js.map +1 -0
  17. package/dist/server/learnedSkills/learnedSkillsRoutes.js +25 -0
  18. package/dist/server/learnedSkills/learnedSkillsRoutes.js.map +1 -0
  19. package/dist/server/learnedSkills/piHermesLearnedSkillProvider.js +123 -0
  20. package/dist/server/learnedSkills/piHermesLearnedSkillProvider.js.map +1 -0
  21. package/dist/server/learnedSkills/skillDocumentParser.js +42 -0
  22. package/dist/server/learnedSkills/skillDocumentParser.js.map +1 -0
  23. package/dist/server/memory/piHermesMemoryProvider.js +9 -7
  24. package/dist/server/memory/piHermesMemoryProvider.js.map +1 -1
  25. package/dist/server/piHermes/projectIdentity.js +103 -0
  26. package/dist/server/piHermes/projectIdentity.js.map +1 -0
  27. package/dist/server/projects/projectService.js +28 -2
  28. package/dist/server/projects/projectService.js.map +1 -1
  29. package/dist/server/realtime/sessionEventHub.js +16 -0
  30. package/dist/server/realtime/sessionEventHub.js.map +1 -1
  31. package/dist/server/realtime/slowConsumerGuard.js +75 -0
  32. package/dist/server/realtime/slowConsumerGuard.js.map +1 -0
  33. package/dist/server/sessiond/sessionProxyRoutes.js +14 -1
  34. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  35. package/dist/server/sessions/authService.js +8 -0
  36. package/dist/server/sessions/authService.js.map +1 -1
  37. package/dist/server/sessions/oauthLoginFlowService.js +16 -1
  38. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  39. package/dist/server/sessions/piSessionService.js +4 -3
  40. package/dist/server/sessions/piSessionService.js.map +1 -1
  41. package/dist/server/storage/projectStore.js +170 -26
  42. package/dist/server/storage/projectStore.js.map +1 -1
  43. package/dist/server/webSocketBridge.js +12 -1
  44. package/dist/server/webSocketBridge.js.map +1 -1
  45. package/dist/shared/apiTypes.d.ts +19 -0
  46. package/dist/shared/apiTypes.js.map +1 -1
  47. package/dist/shared/federatedRoutes.js +4 -0
  48. package/dist/shared/federatedRoutes.js.map +1 -1
  49. package/dist/shared/projectAncestry.js +43 -0
  50. package/dist/shared/projectAncestry.js.map +1 -0
  51. package/docs/plugins.md +24 -1
  52. package/package.json +10 -8
@@ -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-DG-QGbW9.js"></script>
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,2 @@
1
+ // Generated from pi-webui-plugins/workspace-learned-skills/learnedSkillsData.ts. Do not edit directly.
2
+ export {};
@@ -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("&", "&amp;")
408
+ .replaceAll("<", "&lt;")
409
+ .replaceAll(">", "&gt;")
410
+ .replaceAll('"', "&quot;")
411
+ .replaceAll("'", "&#39;");
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,9 @@
1
+ {
2
+ "name": "@pi-webui/workspace-learned-skills-plugin",
3
+ "private": true,
4
+ "piWebUi": {
5
+ "plugins": [
6
+ { "id": "workspace-learned-skills", "module": "pi-webui-plugin.js" }
7
+ ]
8
+ }
9
+ }
@@ -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;
@@ -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.code(404).send({ error: error instanceof Error ? error.message : String(error) });
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.code(404).send({ error: error instanceof Error ? error.message : String(error) });
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 });