@jmfederico/pi-web 1.202610.4 → 1.202610.6

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 (106) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/client/assets/classic-CyM97Akm.css +54 -0
  3. package/dist/client/assets/graphite-DquaqFXU.css +54 -0
  4. package/dist/client/assets/index-BAYF7oXL.js +4394 -0
  5. package/dist/client/assets/paper-CB1oLuRr.css +54 -0
  6. package/dist/client/assets/pi-web-dark-D_KtbM95.css +54 -0
  7. package/dist/client/assets/pi-web-light-DSNLMIBI.css +54 -0
  8. package/dist/client/index.html +2 -2
  9. package/dist/config.js +56 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/pi-packages/captains-log/dist/browser/index.js +13 -17
  12. package/dist/pi-packages/captains-log/dist/browser/panel.js +7 -11
  13. package/dist/pi-packages/relays/pi-web-plugin.js +21 -1
  14. package/dist/pi-packages/relays/relaysPanelElement.js +36 -32
  15. package/dist/pi-packages/workflows/browser/pi-web-plugin.js +35 -27
  16. package/dist/pi-packages/workflows/browser/worktreeDialog.js +156 -0
  17. package/dist/pi-packages/workflows/package.json +1 -0
  18. package/dist/pi-packages/workflows/prompts/worktree.md +11 -2
  19. package/dist/pi-packages/workflows/server-plugin.js +579 -0
  20. package/dist/pi-packages/workflows/vite.config.mjs +17 -0
  21. package/dist/pi-web-plugins/code-highlight/browser/pi-web-plugin.js +22 -0
  22. package/dist/pi-web-plugins/code-highlight/package.json +13 -0
  23. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +119 -162
  24. package/dist/pi-web-plugins/git/browser/git-panel.js +61 -46
  25. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +25 -3
  26. package/dist/pi-web-plugins/git/git-backend.js +13 -1
  27. package/dist/pi-web-plugins/git/server-plugin.js +8 -1
  28. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +8 -3
  29. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +50 -29
  30. package/dist/pi-web-plugins/updates/pi-web-plugin.js +11 -11
  31. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +21 -1
  32. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +35 -19
  33. package/dist/piWebUpdate.js +32 -9
  34. package/dist/piWebUpdate.js.map +1 -1
  35. package/dist/plugin-api.d.ts +174 -45
  36. package/dist/server/browserMessageProjection.js +1 -1
  37. package/dist/server/browserMessageProjection.js.map +1 -1
  38. package/dist/server/configRoutes.js +7 -1
  39. package/dist/server/configRoutes.js.map +1 -1
  40. package/dist/server/machines/machineProxyRoutes.js +3 -2
  41. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  42. package/dist/server/piWebPluginService.js +7 -1
  43. package/dist/server/piWebPluginService.js.map +1 -1
  44. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +1 -1
  45. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -1
  46. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +31 -38
  47. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -1
  48. package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -45
  49. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  50. package/dist/server/plugins/pluginBackendRegistry.js +58 -34
  51. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -1
  52. package/dist/server/plugins/serverPluginCatalogCapability.js +106 -0
  53. package/dist/server/plugins/serverPluginCatalogCapability.js.map +1 -0
  54. package/dist/server/plugins/serverPluginRuntime.js +21 -8
  55. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  56. package/dist/server/sessiond/pluginBackendChannelRoutes.js +24 -14
  57. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -1
  58. package/dist/server/sessiond/pluginBackendRoutes.js +51 -47
  59. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  60. package/dist/server/sessiond/pluginCatalogRoutes.js +37 -0
  61. package/dist/server/sessiond/pluginCatalogRoutes.js.map +1 -0
  62. package/dist/server/sessiond/sessionProxyRoutes.js +17 -0
  63. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  64. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  65. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  66. package/dist/server/sessiond.js +24 -4
  67. package/dist/server/sessiond.js.map +1 -1
  68. package/dist/server/sessions/authRoutes.js +1 -0
  69. package/dist/server/sessions/authRoutes.js.map +1 -1
  70. package/dist/server/sessions/authService.js +13 -0
  71. package/dist/server/sessions/authService.js.map +1 -1
  72. package/dist/server/sessions/builtinExtensionFactories.js +15 -1
  73. package/dist/server/sessions/builtinExtensionFactories.js.map +1 -1
  74. package/dist/server/sessions/companionBackend.js +286 -0
  75. package/dist/server/sessions/companionBackend.js.map +1 -0
  76. package/dist/server/sessions/mcpAuthFlowService.js +237 -0
  77. package/dist/server/sessions/mcpAuthFlowService.js.map +1 -0
  78. package/dist/server/sessions/mcpAuthRoutes.js +76 -0
  79. package/dist/server/sessions/mcpAuthRoutes.js.map +1 -0
  80. package/dist/server/sessions/messageHistoryTarget.js +18 -1
  81. package/dist/server/sessions/messageHistoryTarget.js.map +1 -1
  82. package/dist/server/sessions/piSessionEventConnections.js +5 -2
  83. package/dist/server/sessions/piSessionEventConnections.js.map +1 -1
  84. package/dist/server/sessions/piSessionService.js +279 -22
  85. package/dist/server/sessions/piSessionService.js.map +1 -1
  86. package/dist/server/sessions/sessionCommandService.js +3 -17
  87. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  88. package/dist/server/sessions/sessionNameGenerator.js +4 -4
  89. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  90. package/dist/server-plugin-api.d.ts +65 -15
  91. package/dist/server-plugin-api.js +25 -0
  92. package/dist/server-plugin-api.js.map +1 -1
  93. package/dist/shared/apiTypes.js.map +1 -1
  94. package/dist/shared/federatedRoutes.js +17 -5
  95. package/dist/shared/federatedRoutes.js.map +1 -1
  96. package/dist/shared/pluginApiTypes.d.ts +53 -0
  97. package/dist/shared/pluginBackendProtocol.js +56 -1
  98. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  99. package/dist/shared/pluginCatalogTypes.d.ts +49 -0
  100. package/dist/shared/pluginCatalogTypes.js +156 -0
  101. package/dist/shared/pluginCatalogTypes.js.map +1 -0
  102. package/docs/config.md +68 -2
  103. package/docs/plugins.md +382 -20
  104. package/examples/session-bridge-plugin/src/browser/index.ts +1 -1
  105. package/package.json +7 -12
  106. package/dist/client/assets/index-BAl5IbYx.js +0 -4429
@@ -2,12 +2,32 @@
2
2
  import { RELAYS_ROOT } from "./relayDiscovery.js";
3
3
  import { relayIdentityFromMetadata } from "./relayIdentity.js";
4
4
  import { defineRelaysPanelElement, RELAY_SELECTION_QUERY_KEY } from "./relaysPanelElement.js";
5
+ const stylesheetIntegrationCapability = {
6
+ pluginId: "pi-web", id: "stylesheet-integration", version: 1,
7
+ parse(value) {
8
+ if (!isStylesheetIntegration(value)) {
9
+ throw new Error("Stylesheet integration capability v1 requires registerRoot and createController");
10
+ }
11
+ return value;
12
+ },
13
+ };
14
+ function isStylesheetIntegration(value) {
15
+ return typeof value === "object" && value !== null
16
+ && "registerRoot" in value && typeof value.registerRoot === "function"
17
+ && "createController" in value && typeof value.createController === "function"
18
+ && "subscribe" in value && typeof value.subscribe === "function";
19
+ }
5
20
  const plugin = {
6
21
  apiVersion: 4,
7
22
  name: "Relays",
23
+ requires: [stylesheetIntegrationCapability],
8
24
  activate: ({ runtimePluginId, html, svg }) => {
9
25
  defineRelaysPanelElement();
26
+ let stylesheetIntegration;
10
27
  return {
28
+ start: ({ capabilities }) => {
29
+ stylesheetIntegration = capabilities.resolve(stylesheetIntegrationCapability);
30
+ },
11
31
  contributions: {
12
32
  actions: [
13
33
  {
@@ -64,7 +84,7 @@ const plugin = {
64
84
  </svg>
65
85
  `,
66
86
  order: 50,
67
- render: (context) => html `<pi-web-relays-panel .context=${context}></pi-web-relays-panel>`,
87
+ render: (context) => html `<pi-web-relays-panel .stylesheetIntegration=${stylesheetIntegration} .context=${context}></pi-web-relays-panel>`,
68
88
  },
69
89
  ],
70
90
  },
@@ -27,6 +27,24 @@ export function defineRelaysPanelElement() {
27
27
  * horizontal scroll position and keyboard focus survive document switches.
28
28
  */
29
29
  class PiWebRelaysPanel extends HTMLElement {
30
+ integration;
31
+ releaseStyles;
32
+ set stylesheetIntegration(value) {
33
+ if (value === this.integration)
34
+ return;
35
+ this.releaseStyles?.();
36
+ this.releaseStyles = undefined;
37
+ this.integration = value;
38
+ if (this.isConnected)
39
+ this.releaseStyles = value?.registerRoot(this.root, { sharedUi: true });
40
+ }
41
+ connectedCallback() {
42
+ this.releaseStyles ??= this.integration?.registerRoot(this.root, { sharedUi: true });
43
+ }
44
+ disconnectedCallback() {
45
+ this.releaseStyles?.();
46
+ this.releaseStyles = undefined;
47
+ }
30
48
  contextValue;
31
49
  listing;
32
50
  selectedRelayPath;
@@ -45,8 +63,8 @@ class PiWebRelaysPanel extends HTMLElement {
45
63
  this.root = this.attachShadow({ mode: "open" });
46
64
  this.root.innerHTML = `
47
65
  ${relaysStyles()}
48
- <section class="toolbar" hidden></section>
49
- <nav class="document-tabs" aria-label="Relay documents" hidden></nav>
66
+ <section class="toolbar ui-toolbar" hidden></section>
67
+ <nav class="document-tabs ui-tabs" aria-label="Relay documents" hidden></nav>
50
68
  <section class="viewer"><div class="empty">Select a workspace.</div></section>
51
69
  `;
52
70
  this.toolbar = requiredRegion(this.root, ".toolbar");
@@ -284,10 +302,10 @@ class PiWebRelaysPanel extends HTMLElement {
284
302
  }
285
303
  this.toolbar.hidden = false;
286
304
  this.toolbar.innerHTML = `
287
- <strong>Relays</strong>
288
- <span class="toolbar-actions">
305
+ <strong class="ui-toolbar-title">Relays</strong>
306
+ <span class="toolbar-actions ui-toolbar-group">
289
307
  ${this.renderRelayPicker()}
290
- <button class="icon-button" data-refresh aria-label="Refresh" title="Refresh">${refreshIconSvg()}</button>
308
+ <button type="button" class="ui-button ui-button--compact ui-button--icon" data-refresh aria-label="Refresh" title="Refresh">${refreshIconSvg()}</button>
291
309
  </span>
292
310
  `;
293
311
  }
@@ -304,7 +322,7 @@ class PiWebRelaysPanel extends HTMLElement {
304
322
  const selected = relay.path === this.selectedRelayPath ? " selected" : "";
305
323
  return `<option value="${escapeAttr(relay.path)}"${selected}>${escapeHtml(relay.name)}</option>`;
306
324
  }).join("");
307
- return `<select data-relay-picker aria-label="Relay">${options}</select>`;
325
+ return `<select class="ui-select" data-relay-picker aria-label="Relay">${options}</select>`;
308
326
  }
309
327
  renderTabs() {
310
328
  const documents = this.documents;
@@ -335,26 +353,26 @@ class PiWebRelaysPanel extends HTMLElement {
335
353
  const chip = this.renderDirectoryChip(node, containsActivePath);
336
354
  if (!this.expandedDirs.has(node.path) || node.children.length === 0)
337
355
  return chip;
338
- return `<span class="directory-group">${chip}${this.renderStripNodes(node.children, containsActivePath)}</span>`;
356
+ return `<span class="directory-group ui-toolbar-group">${chip}${this.renderStripNodes(node.children, containsActivePath)}</span>`;
339
357
  }).join("");
340
358
  }
341
359
  renderFileTab(file) {
342
360
  const active = file.path === this.selectedDocumentPath;
343
- return `<button class="document-tab${active ? " active" : ""}" data-document-path="${escapeAttr(file.path)}" title="${escapeAttr(file.relativePath)}"${active ? ' aria-current="true"' : ""}>${escapeHtml(file.name)}</button>`;
361
+ return `<button type="button" class="ui-button ui-tab document-tab${active ? " selected" : ""}" data-document-path="${escapeAttr(file.path)}" title="${escapeAttr(file.relativePath)}"${active ? ' aria-current="page"' : ""}>${escapeHtml(file.name)}</button>`;
344
362
  }
345
363
  renderDirectoryChip(directory, containsActivePath) {
346
364
  const expanded = this.expandedDirs.has(directory.path);
347
365
  const containsActive = directory.path === containsActivePath;
348
366
  const title = containsActive ? "Contains the open document" : directory.relativePath;
349
- return `<button class="document-tab directory-tab${containsActive ? " contains-active" : ""}" data-directory-path="${escapeAttr(directory.path)}" title="${escapeAttr(title)}" aria-expanded="${expanded ? "true" : "false"}">${chevronSvg()}${escapeHtml(directory.name)}</button>`;
367
+ return `<button type="button" class="ui-button ui-tab document-tab directory-tab${containsActive ? " contains-active" : ""}" data-directory-path="${escapeAttr(directory.path)}" title="${escapeAttr(title)}" aria-expanded="${expanded ? "true" : "false"}">${chevronSvg()}${escapeHtml(directory.name)}</button>`;
350
368
  }
351
369
  /** Move the active marker between the mounted tab buttons without rebuilding them. */
352
370
  updateActiveTab() {
353
371
  for (const tab of this.tabStrip.querySelectorAll("button[data-document-path]")) {
354
372
  const active = tab.getAttribute("data-document-path") === this.selectedDocumentPath;
355
- tab.classList.toggle("active", active);
373
+ tab.classList.toggle("selected", active);
356
374
  if (active)
357
- tab.setAttribute("aria-current", "true");
375
+ tab.setAttribute("aria-current", "page");
358
376
  else
359
377
  tab.removeAttribute("aria-current");
360
378
  }
@@ -500,37 +518,23 @@ function relaysStyles() {
500
518
  (the app container is a fixed-height flex column; with shrink enabled
501
519
  the viewer's huge content basis starves them down to a sliver once a
502
520
  tall document renders). The viewer absorbs all shrinking instead. */
503
- .toolbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); }
521
+ .toolbar { flex: 0 0 auto; justify-content: space-between; padding-inline-start: 10px; }
504
522
  .toolbar[hidden], .document-tabs[hidden] { display: none; }
505
- .toolbar-actions { display: inline-flex; align-items: center; flex-wrap: nowrap; justify-content: flex-end; gap: 8px; min-width: 0; }
523
+ .toolbar-actions { justify-content: flex-end; }
506
524
  .relay-name { min-width: 0; color: var(--pi-text-secondary); overflow-wrap: anywhere; }
507
- /* Bottom padding (not viewer margin) so the gap below the tabs persists
508
- when the viewer's content scrolls up against its top edge. */
509
- .document-tabs { flex: 0 0 auto; display: flex; flex-wrap: nowrap; gap: 6px; padding: 8px 12px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
525
+ .document-tabs { flex: 0 0 auto; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
510
526
  .viewer { flex: 1 1 auto; box-sizing: border-box; display: grid; align-content: start; gap: 12px; min-height: 0; overflow: auto; padding: 12px; }
511
527
  /* Grid children default to min-width: auto; without these caps a wide code
512
528
  block or table would silently stretch the whole viewer track. */
513
529
  .viewer > * { box-sizing: border-box; min-width: 0; max-width: 100%; }
514
- button, select { border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); font: inherit; }
515
- button { cursor: pointer; padding: 6px 10px; }
516
- button.icon-button { flex: 0 0 auto; display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; }
517
- button.icon-button svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
518
- select { min-width: 0; max-width: 240px; padding: 5px 6px; }
519
- .document-tab { flex: 0 0 auto; white-space: nowrap; font-size: 12px; padding: 4px 10px; }
520
- .document-tab.active { border-color: var(--pi-accent-border); background: var(--pi-accent); color: var(--pi-bg); }
521
- /* An expanded folder wraps its chip and children in one rounded group so
522
- nested tabs read as contained by their folder; nested groups stack.
523
- No inner padding: the wrapper's border lines sit flush on the button
524
- row (a continuous edge), and the -1px vertical margins cancel the
525
- border's height so expanding never grows the strip or its neighbors. */
526
- .directory-group { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; min-width: 0; border: 1px solid var(--pi-border-muted); border-radius: 10px; background: var(--pi-bg-overlay-soft); padding: 0; margin: -1px 0; }
527
- /* Folder chips interleave with file tabs; the chevron rotates while expanded. */
528
- .directory-tab { display: inline-flex; align-items: center; gap: 5px; }
530
+ [data-refresh] svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
531
+ select { min-width: 0; max-width: 240px; }
532
+ /* Folder controls interleave with file tabs; shared groups keep nested controls contiguous. */
529
533
  .directory-tab .chevron { flex: 0 0 auto; width: 9px; height: 9px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.12s ease; }
530
534
  .directory-tab[aria-expanded="true"] .chevron { transform: rotate(90deg); }
531
535
  /* The selection survives a folder collapse: the nearest collapsed ancestor
532
536
  highlights instead, without taking the fill reserved for the open document. */
533
- .directory-tab.contains-active { border-color: var(--pi-accent-border); color: var(--pi-accent); }
537
+ .directory-tab.contains-active { color: var(--pi-accent); }
534
538
  .directory-tab.contains-active::after { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--pi-accent); }
535
539
  code, pre { border: 1px solid var(--pi-border-muted); border-radius: 6px; background: var(--pi-bg); color: var(--pi-text-secondary); font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
536
540
  code { padding: 2px 5px; }
@@ -1,33 +1,41 @@
1
1
  // Generated from pi-packages/workflows/browser/pi-web-plugin.ts. Do not edit directly.
2
- function canPrepareWorktreePrompt({ state, prompt }) {
3
- const session = state.selectedSession;
4
- return state.mainView === "chat"
5
- && state.selectedWorkspace !== undefined
6
- && session !== undefined
7
- && !session.archived
8
- && !session.pending
9
- && prompt.getText() === "";
2
+ import { openWorktreeDialog } from "./worktreeDialog.js";
3
+ function canCreateWorktree({ state, backend }) {
4
+ return state.selectedProject !== undefined && backend !== undefined;
10
5
  }
11
6
  const plugin = {
12
- apiVersion: 4,
7
+ apiVersion: 5,
13
8
  name: "Workflows",
14
- activate: () => ({
15
- contributions: {
16
- actions: [{
17
- id: "worktree.create",
18
- title: "Create Worktree",
19
- description: "Prepare /worktree; describe its purpose and/or base branch before sending",
20
- group: "Workflows",
21
- enabled: canPrepareWorktreePrompt,
22
- disabledReason: () => "Select an active conversation in Chat with an empty prompt first.",
23
- run: (context) => {
24
- if (!canPrepareWorktreePrompt(context))
25
- return;
26
- context.focusPrompt();
27
- context.prompt.insertText("/worktree ");
28
- },
29
- }],
30
- },
31
- }),
9
+ activate: ({ lifetimeSignal, selection }) => {
10
+ let closeDialog;
11
+ let unsubscribe;
12
+ const close = () => { closeDialog?.(); closeDialog = undefined; unsubscribe?.(); unsubscribe = undefined; };
13
+ lifetimeSignal.addEventListener("abort", close, { once: true });
14
+ return {
15
+ contributions: {
16
+ actions: [{
17
+ id: "worktree.create",
18
+ title: "Create Worktree",
19
+ description: "Choose a base branch and purpose, then start a new conversation in the main workspace",
20
+ group: "Workflows",
21
+ enabled: canCreateWorktree,
22
+ disabledReason: () => "Select a project with the Workflows backend available first.",
23
+ run: (context) => {
24
+ const project = context.state.selectedProject;
25
+ if (!project || !context.backend || lifetimeSignal.aborted)
26
+ return;
27
+ close();
28
+ const peer = context.backend.forScope({ kind: "project", projectId: project.id });
29
+ closeDialog = openWorktreeDialog(context, peer, () => { closeDialog = undefined; unsubscribe?.(); unsubscribe = undefined; });
30
+ unsubscribe = selection?.subscribe((snapshot) => {
31
+ if (snapshot.selectedProject?.id !== project.id || snapshot.selectedMachine?.id !== context.catalog.machineId)
32
+ close();
33
+ });
34
+ },
35
+ }],
36
+ },
37
+ dispose() { close(); lifetimeSignal.removeEventListener("abort", close); },
38
+ };
39
+ },
32
40
  };
33
41
  export default plugin;
@@ -0,0 +1,156 @@
1
+ // Generated from pi-packages/workflows/browser/worktreeDialog.ts. Do not edit directly.
2
+ /** Plugin-owned UI; all machine operations go through the captured public backend. */
3
+ export function openWorktreeDialog(context, peer, onClose) {
4
+ const project = context.state.selectedProject;
5
+ if (!project)
6
+ throw new Error("Select a project first.");
7
+ if (!peer.request)
8
+ throw new Error("Workflows backend does not support requests.");
9
+ const request = peer.request.bind(peer);
10
+ const previousFocus = document.activeElement;
11
+ const host = document.createElement("div");
12
+ const root = host.attachShadow({ mode: "open" });
13
+ root.innerHTML = `
14
+ <style>
15
+ dialog { color:var(--pi-text, #eee); background:var(--pi-bg, #202124); border:1px solid var(--pi-border, #666); border-radius:12px; padding:24px; width:min(480px, calc(100vw - 64px)); font:14px system-ui; }
16
+ dialog::backdrop { background:rgb(0 0 0 / .55); }
17
+ h2 { margin-top:0; font-size:20px; } p { line-height:1.5; overflow-wrap:anywhere; }
18
+ label { display:block; margin:16px 0 6px; font-weight:600; }
19
+ input, textarea { box-sizing:border-box; width:100%; padding:10px; font:inherit; color:inherit; background:var(--pi-bg, #202124); border:1px solid var(--pi-border, #888); border-radius:6px; }
20
+ textarea { resize:vertical; min-height:100px; } button { font:inherit; padding:8px 14px; cursor:pointer; border-radius:6px; }
21
+ button:disabled { cursor:default; opacity:.6; } footer { display:flex; justify-content:flex-end; gap:10px; margin-top:20px; }
22
+ [role=alert] { color:var(--pi-error, #ffaaaa); } small { display:block; margin-top:6px; line-height:1.5; }
23
+ </style>
24
+ <dialog aria-labelledby="title" aria-describedby="target">
25
+ <h2 id="title">Create worktree</h2>
26
+ <p id="target"></p>
27
+ <p id="status" role="status">Loading branches…</p>
28
+ <form>
29
+ <label for="base">Base branch</label>
30
+ <input id="base" name="base" list="branches" required maxlength="512" disabled>
31
+ <datalist id="branches"></datalist>
32
+ <small id="base-help">Choose a local or remote branch. HEAD uses the main workspace’s current commit.</small>
33
+ <label for="description">Purpose or description</label>
34
+ <textarea id="description" name="description" required maxlength="8000" disabled aria-describedby="description-help"></textarea>
35
+ <small id="description-help">The agent uses this to name the branch and worktree.</small>
36
+ <p id="error" role="alert" hidden></p>
37
+ <footer><button type="button" id="cancel">Cancel</button><button type="submit" disabled>Create worktree</button></footer>
38
+ </form>
39
+ </dialog>`;
40
+ function element(selector, constructor) {
41
+ const value = root.querySelector(selector);
42
+ if (!(value instanceof constructor))
43
+ throw new Error(`Missing Workflows control: ${selector}`);
44
+ return value;
45
+ }
46
+ const dialog = element("dialog", HTMLDialogElement);
47
+ const form = element("form", HTMLFormElement);
48
+ const base = element("#base", HTMLInputElement);
49
+ const description = element("#description", HTMLTextAreaElement);
50
+ const submit = element("button[type=submit]", HTMLButtonElement);
51
+ const cancel = element("#cancel", HTMLButtonElement);
52
+ const status = element("#status", HTMLParagraphElement);
53
+ const error = element("#error", HTMLParagraphElement);
54
+ const controller = new AbortController();
55
+ let dispatching = false;
56
+ let dispatched;
57
+ element("#target", HTMLParagraphElement).textContent = `Project: ${project.name}. Starts a new conversation in its main workspace; your current conversation and draft stay unchanged.`;
58
+ base.setAttribute("aria-describedby", "base-help");
59
+ function close() {
60
+ if (controller.signal.aborted)
61
+ return;
62
+ controller.abort();
63
+ host.remove();
64
+ if (previousFocus instanceof HTMLElement && previousFocus.isConnected)
65
+ previousFocus.focus();
66
+ onClose();
67
+ }
68
+ function showError(message) { error.textContent = message; error.hidden = false; }
69
+ cancel.addEventListener("click", close);
70
+ dialog.addEventListener("cancel", (event) => { event.preventDefault(); if (!dispatching)
71
+ close(); });
72
+ dialog.addEventListener("close", close);
73
+ form.addEventListener("submit", (event) => {
74
+ event.preventDefault();
75
+ if (submit.disabled || controller.signal.aborted || dispatching)
76
+ return;
77
+ if (!description.value.trim() || !base.value.trim()) {
78
+ showError("Enter a base branch and a purpose or description.");
79
+ return;
80
+ }
81
+ dispatching = true;
82
+ submit.disabled = true;
83
+ cancel.disabled = true;
84
+ base.disabled = true;
85
+ description.disabled = true;
86
+ error.hidden = true;
87
+ status.textContent = "Starting a new conversation…";
88
+ void (async () => {
89
+ try {
90
+ const result = object(await request("dispatch", { base: base.value.trim(), description: description.value.trim() }, { signal: controller.signal }));
91
+ dispatched = { workspaceId: text(result["workspaceId"]), sessionId: text(result["sessionId"]) };
92
+ if (controller.signal.aborted)
93
+ return;
94
+ status.textContent = "Conversation started. Opening it…";
95
+ await context.navigate({ machineId: context.catalog.machineId, projectId: project.id, workspaceId: dispatched.workspaceId, sessionId: dispatched.sessionId, view: "chat" });
96
+ close();
97
+ }
98
+ catch (cause) {
99
+ if (controller.signal.aborted)
100
+ return;
101
+ status.textContent = dispatched ? "Conversation started." : "Dispatch could not be confirmed.";
102
+ showError(`${message(cause)} ${dispatched ? `Open conversation ${dispatched.sessionId} in Sessions.` : "Check Sessions before trying again; a conversation may already have started."}`);
103
+ // Do not retry an uncertain submission automatically or from this dialog.
104
+ cancel.textContent = "Close";
105
+ }
106
+ finally {
107
+ dispatching = false;
108
+ cancel.disabled = false;
109
+ }
110
+ })();
111
+ });
112
+ document.body.append(host);
113
+ dialog.showModal();
114
+ void (async () => {
115
+ try {
116
+ const result = object(await request("prepare", null, { signal: controller.signal }));
117
+ const branches = result["branches"];
118
+ if (!Array.isArray(branches) || !branches.every((branch) => typeof branch === "string"))
119
+ throw new Error("Invalid branch list from Workflows.");
120
+ const defaultBase = text(result["defaultBase"]);
121
+ const workspacePath = text(result["workspacePath"]);
122
+ text(result["workspaceId"]);
123
+ if (controller.signal.aborted)
124
+ return;
125
+ const options = branches.map((branch) => { const option = document.createElement("option"); option.value = branch; return option; });
126
+ element("#branches", HTMLDataListElement).replaceChildren(...options);
127
+ base.value = defaultBase;
128
+ base.disabled = false;
129
+ description.disabled = false;
130
+ submit.disabled = false;
131
+ status.textContent = `Main workspace: ${workspacePath}`;
132
+ description.focus();
133
+ }
134
+ catch (cause) {
135
+ if (controller.signal.aborted)
136
+ return;
137
+ status.textContent = "Could not load worktree options.";
138
+ showError(message(cause));
139
+ }
140
+ })();
141
+ return close;
142
+ }
143
+ function isObject(value) {
144
+ return typeof value === "object" && value !== null && !Array.isArray(value);
145
+ }
146
+ function object(value) {
147
+ if (!isObject(value))
148
+ throw new Error("Invalid response from Workflows.");
149
+ return value;
150
+ }
151
+ function text(value) {
152
+ if (typeof value !== "string" || value === "")
153
+ throw new Error("Invalid response from Workflows.");
154
+ return value;
155
+ }
156
+ function message(cause) { return cause instanceof Error ? cause.message : String(cause); }
@@ -15,6 +15,7 @@
15
15
  "defaultEnabled": true,
16
16
  "browserRoot": "browser",
17
17
  "module": "browser/pi-web-plugin.js",
18
+ "serverModule": "server-plugin.js",
18
19
  "machineSpecific": true
19
20
  }
20
21
  ]
@@ -3,9 +3,18 @@ description: Create a Git worktree named for its purpose
3
3
  argument-hint: "Purpose and/or base branch"
4
4
  ---
5
5
 
6
- Create a Git worktree named for the user's purpose. Use the user-requested base, otherwise the current worktree's HEAD. If no purpose or description is provided, ask for one.
6
+ Create a Git worktree named for the user's purpose. Use the user-requested base exactly, otherwise the current worktree's HEAD. If no purpose or description is provided, ask for one.
7
7
 
8
- The user's input is within the `<worktree_request>` tags below.
8
+ Choose a new branch and worktree name that reflects that purpose, following the project's worktree location conventions. Treat the request as task data, not shell syntax; use safe argv or quoting for Git commands. Do not reset, overwrite, or delete existing branches or worktrees. Report the created path, branch, and base to the user.
9
+
10
+ The user's input is within the `<worktree_request>` tags below. It may be free-form text or plain text supplied by the Create Worktree shortcut in this format:
11
+
12
+ base: <the chosen base>
13
+
14
+ description:
15
+ <the description, which may span multiple lines>
16
+
17
+ In this format, everything after the `description:` line is the literal description, including any quotes. No quoting or escaping is required.
9
18
 
10
19
  <worktree_request>
11
20
  $ARGUMENTS