@firstpick/pi-package-webui 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/public/app.js CHANGED
@@ -43,6 +43,10 @@ const elements = {
43
43
  publishMenu: $("#publishMenu"),
44
44
  releaseNpmButton: $("#releaseNpmButton"),
45
45
  releaseAurButton: $("#releaseAurButton"),
46
+ nativeCommandMenuButton: $("#nativeCommandMenuButton"),
47
+ nativeCommandMenu: $("#nativeCommandMenu"),
48
+ nativeSkillsButton: $("#nativeSkillsButton"),
49
+ nativeToolsButton: $("#nativeToolsButton"),
46
50
  gitWorkflowPanel: $("#gitWorkflowPanel"),
47
51
  gitWorkflowTitle: $("#gitWorkflowTitle"),
48
52
  gitWorkflowHint: $("#gitWorkflowHint"),
@@ -143,6 +147,7 @@ let pathFastPicksReady = false;
143
147
  let pathFastPicksLoadPromise = null;
144
148
  let mobileTabsExpanded = false;
145
149
  let openTerminalTabGroupKey = null;
150
+ let nativeCommandMenuOpen = false;
146
151
  let availableCommands = [];
147
152
  let rawAvailableCommands = [];
148
153
  let commandSuggestions = [];
@@ -361,6 +366,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
361
366
  ["git-staged-msg", "gitWorkflow"],
362
367
  ["release-npm", "releaseNpm"],
363
368
  ["release-aur", "releaseAur"],
369
+ ["skills", "tuiSkillsCommand"],
370
+ ["tools", "tuiToolsCommand"],
364
371
  ["stats", "statsCommand"],
365
372
  ["git-footer-refresh", "gitFooterStatus"],
366
373
  ["todo-progress-status", "todoProgressWidget"],
@@ -6943,6 +6950,13 @@ function setPublishMenuOpen(open) {
6943
6950
  elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
6944
6951
  }
6945
6952
 
6953
+ function setNativeCommandMenuOpen(open) {
6954
+ nativeCommandMenuOpen = !!open;
6955
+ elements.nativeCommandMenuButton.setAttribute("aria-expanded", nativeCommandMenuOpen ? "true" : "false");
6956
+ elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
6957
+ elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
6958
+ }
6959
+
6946
6960
  function optionalFeatureIdForCommand(name) {
6947
6961
  if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
6948
6962
  if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
@@ -7096,6 +7110,18 @@ function renderOptionalFeatureControls() {
7096
7110
  );
7097
7111
  if (!hasPublishWorkflow && publishMenuOpen) setPublishMenuOpen(false);
7098
7112
 
7113
+ const hasNativeCommandMenu = isOptionalFeatureEnabled("tuiSkillsCommand") || isOptionalFeatureEnabled("tuiToolsCommand");
7114
+ elements.nativeSkillsButton.hidden = !isOptionalFeatureEnabled("tuiSkillsCommand");
7115
+ elements.nativeToolsButton.hidden = !isOptionalFeatureEnabled("tuiToolsCommand");
7116
+ const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
7117
+ if (nativeCommandMenuContainer) nativeCommandMenuContainer.hidden = !hasNativeCommandMenu;
7118
+ setOptionalControlState(
7119
+ elements.nativeCommandMenuButton,
7120
+ hasNativeCommandMenu,
7121
+ "Slash command menu unavailable: enable/install TUI Skills command and/or TUI Tools command in Optional features.",
7122
+ );
7123
+ if (!hasNativeCommandMenu && nativeCommandMenuOpen) setNativeCommandMenuOpen(false);
7124
+
7099
7125
  renderOptionalFeaturePanel();
7100
7126
  }
7101
7127
 
@@ -7159,6 +7185,23 @@ function runPublishWorkflow(command) {
7159
7185
  sendPrompt("prompt", command);
7160
7186
  }
7161
7187
 
7188
+ async function runNativeCommandMenu(command) {
7189
+ setComposerActionsOpen(false);
7190
+ setPublishMenuOpen(false);
7191
+ setNativeCommandMenuOpen(false);
7192
+ const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
7193
+ const featureId = optionalFeatureIdForCommand(commandName);
7194
+ if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
7195
+ const tabContext = activeTabContext();
7196
+ addEvent(commandUnavailableMessage(commandName), "warn");
7197
+ refreshCommands(tabContext).catch((error) => {
7198
+ if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
7199
+ });
7200
+ return;
7201
+ }
7202
+ await handleNativeSlashSelectorCommand(command);
7203
+ }
7204
+
7162
7205
  function slashCommandName(message) {
7163
7206
  const match = String(message || "").trim().match(/^\/([^\s]+)$/);
7164
7207
  return match ? match[1].toLowerCase() : "";
@@ -7212,7 +7255,8 @@ function renderNativeLoading(label = "Loading…") {
7212
7255
  function nativeSelectorMatches(item, query) {
7213
7256
  if (!query) return true;
7214
7257
  const needle = query.toLowerCase();
7215
- return [item.label, item.description, item.meta, item.badge]
7258
+ const tags = Array.isArray(item.tags) ? item.tags.map((tag) => tag?.label) : [];
7259
+ return [item.label, item.description, item.meta, item.badge, ...tags]
7216
7260
  .filter(Boolean)
7217
7261
  .some((value) => String(value).toLowerCase().includes(needle));
7218
7262
  }
@@ -7245,6 +7289,10 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
7245
7289
  }
7246
7290
  title.append(badge);
7247
7291
  }
7292
+ for (const tag of Array.isArray(item.tags) ? item.tags : []) {
7293
+ if (!tag?.label) continue;
7294
+ title.append(make("span", `native-selector-badge${tag.className ? ` ${tag.className}` : ""}`, tag.label));
7295
+ }
7248
7296
  const detail = make("span", "native-selector-detail", item.description || "");
7249
7297
  const meta = make("span", "native-selector-meta", item.meta || "");
7250
7298
  button.append(title);
@@ -7583,6 +7631,12 @@ function nativeResourceSourceLabel(resource) {
7583
7631
  return [info.source, info.scope, info.origin].filter(Boolean).join(" · ") || resource?.location || "loaded resource";
7584
7632
  }
7585
7633
 
7634
+ function nativeToolOriginTag(resource) {
7635
+ return resource?.sourceInfo?.source === "builtin"
7636
+ ? { label: "Pi Native", className: "native-selector-badge-pi-native" }
7637
+ : { label: "External", className: "native-selector-badge-external" };
7638
+ }
7639
+
7586
7640
  function nativeResourceCounts(resources) {
7587
7641
  const disabled = resources.filter((resource) => resource.enabled === false).length;
7588
7642
  return { total: resources.length, disabled, enabled: resources.length - disabled };
@@ -7594,19 +7648,23 @@ function nativeResourceFilterMatches(resource, filter) {
7594
7648
  return true;
7595
7649
  }
7596
7650
 
7597
- function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle } = {}) {
7651
+ function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle, getResourceTag } = {}) {
7598
7652
  const filteredResources = resources.filter((resource) => nativeResourceFilterMatches(resource, filter));
7599
7653
  const counts = nativeResourceCounts(resources);
7600
- const items = filteredResources.map((resource) => ({
7601
- id: resource.name,
7602
- label: resource.name,
7603
- description: resource.description || "No description provided.",
7604
- meta: nativeResourceSourceLabel(resource),
7605
- badge: resource.enabled === false ? "disabled" : "enabled",
7606
- badgeClass: resource.enabled === false ? "disabled native-selector-badge-disabled" : "enabled native-selector-badge-enabled",
7607
- disabled: Boolean(savingName),
7608
- resource,
7609
- }));
7654
+ const items = filteredResources.map((resource) => {
7655
+ const resourceTag = getResourceTag?.(resource);
7656
+ return {
7657
+ id: resource.name,
7658
+ label: resource.name,
7659
+ description: resource.description || "No description provided.",
7660
+ meta: nativeResourceSourceLabel(resource),
7661
+ badge: resource.enabled === false ? "disabled" : "enabled",
7662
+ badgeClass: resource.enabled === false ? "disabled native-selector-badge-disabled" : "enabled native-selector-badge-enabled",
7663
+ tags: resourceTag ? [resourceTag] : [],
7664
+ disabled: Boolean(savingName),
7665
+ resource,
7666
+ };
7667
+ });
7610
7668
  const filterLabel = filter === "enabled" ? "enabled" : filter === "disabled" ? "disabled" : "all";
7611
7669
  renderNativeSelectorItems(items, {
7612
7670
  emptyText: `No ${filterLabel} entries match this filter.`,
@@ -7631,7 +7689,7 @@ function renderNativeResourceFilterActions(filter, setFilter, render) {
7631
7689
  }
7632
7690
 
7633
7691
  async function openNativeToolsSelector() {
7634
- openNativeCommandDialog({ title: "/tools", message: "Enable or disable tools for the active Pi tab. Changes apply to the next model turn and persist on this session branch.", searchPlaceholder: "Filter tools…" });
7692
+ openNativeCommandDialog({ title: "Tools Setup", message: "Enable or disable tools for the active Pi tab. Changes apply to the next model turn and persist on this session branch.", searchPlaceholder: "Filter tools…" });
7635
7693
  renderNativeLoading("Loading tools…");
7636
7694
  let tools = [];
7637
7695
  let savingName = "";
@@ -7640,6 +7698,7 @@ async function openNativeToolsSelector() {
7640
7698
  renderNativeResourceToggles(tools, {
7641
7699
  savingName,
7642
7700
  filter,
7701
+ getResourceTag: nativeToolOriginTag,
7643
7702
  onToggle: async (tool) => {
7644
7703
  if (!tool || savingName) return;
7645
7704
  const enabledTools = new Set(tools.filter((item) => item.enabled !== false).map((item) => item.name));
@@ -7674,7 +7733,7 @@ async function openNativeToolsSelector() {
7674
7733
  }
7675
7734
 
7676
7735
  async function openNativeSkillsSelector() {
7677
- openNativeCommandDialog({ title: "/skills", message: "Enable or disable skills for automatic model invocation in the active Pi tab. Disabled skills are removed from the system prompt and their /skill:name commands are blocked by Web UI.", searchPlaceholder: "Filter skills…" });
7736
+ openNativeCommandDialog({ title: "Skills Setup", message: "Enable or disable skills for automatic model invocation in the active Pi tab. Disabled skills are removed from the system prompt and their /skill:name commands are blocked by Web UI.", searchPlaceholder: "Filter skills…" });
7678
7737
  renderNativeLoading("Loading skills…");
7679
7738
  let skills = [];
7680
7739
  let savingName = "";
@@ -7730,6 +7789,15 @@ function openNativeAuthInfo(mode) {
7730
7789
  async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
7731
7790
  const name = slashCommandName(message);
7732
7791
  if (!NATIVE_SELECTOR_COMMANDS.has(name)) return false;
7792
+ const featureId = optionalFeatureIdForCommand(name);
7793
+ if (featureId && !isOptionalFeatureEnabled(featureId)) {
7794
+ const tabContext = activeTabContext();
7795
+ addEvent(commandUnavailableMessage(name), "warn");
7796
+ refreshCommands(tabContext).catch((error) => {
7797
+ if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
7798
+ });
7799
+ return true;
7800
+ }
7733
7801
  setComposerActionsOpen(false);
7734
7802
  hideCommandSuggestions();
7735
7803
  if (usesPromptInput) {
@@ -9213,7 +9281,7 @@ function showNextDialog() {
9213
9281
  if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
9214
9282
  if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
9215
9283
  if (isReleaseDialog && /^(?:Yes|All eligible packages\b|Publish selected packages \([1-9]\d*\))/.test(optionLabel)) button.classList.add("primary", "release-publish-action");
9216
- if (isReleaseDialog && /^Publish selected packages \(select at least one\)$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
9284
+ if (isReleaseDialog && /^Publish selected packages$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
9217
9285
  if (isReleaseDialog && /^\[x\]/.test(optionLabel)) button.classList.add("release-target-option", "release-target-selected");
9218
9286
  if (isReleaseDialog && /^\[ \]/.test(optionLabel)) button.classList.add("release-target-option");
9219
9287
  if (isReleaseDialog && /^(?:No|Cancel)$/i.test(optionLabel)) button.classList.add("release-cancel-action");
@@ -9515,18 +9583,46 @@ elements.gitWorkflowButton.addEventListener("click", () => {
9515
9583
  });
9516
9584
  const publishMenuContainer = elements.publishButton.parentElement;
9517
9585
  elements.publishButton.addEventListener("click", () => {
9586
+ setNativeCommandMenuOpen(false);
9587
+ setPublishMenuOpen(true);
9588
+ });
9589
+ publishMenuContainer?.addEventListener("pointerenter", () => {
9590
+ setNativeCommandMenuOpen(false);
9518
9591
  setPublishMenuOpen(true);
9519
9592
  });
9520
- publishMenuContainer?.addEventListener("pointerenter", () => setPublishMenuOpen(true));
9521
9593
  publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
9522
- publishMenuContainer?.addEventListener("focusin", () => setPublishMenuOpen(true));
9594
+ publishMenuContainer?.addEventListener("focusin", () => {
9595
+ setNativeCommandMenuOpen(false);
9596
+ setPublishMenuOpen(true);
9597
+ });
9523
9598
  publishMenuContainer?.addEventListener("focusout", () => {
9524
9599
  setTimeout(() => {
9525
9600
  if (!publishMenuContainer?.contains(document.activeElement)) setPublishMenuOpen(false);
9526
9601
  }, 0);
9527
9602
  });
9603
+ const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
9604
+ elements.nativeCommandMenuButton.addEventListener("click", () => {
9605
+ setPublishMenuOpen(false);
9606
+ setNativeCommandMenuOpen(true);
9607
+ });
9608
+ nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
9609
+ setPublishMenuOpen(false);
9610
+ setNativeCommandMenuOpen(true);
9611
+ });
9612
+ nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
9613
+ nativeCommandMenuContainer?.addEventListener("focusin", () => {
9614
+ setPublishMenuOpen(false);
9615
+ setNativeCommandMenuOpen(true);
9616
+ });
9617
+ nativeCommandMenuContainer?.addEventListener("focusout", () => {
9618
+ setTimeout(() => {
9619
+ if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
9620
+ }, 0);
9621
+ });
9528
9622
  elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
9529
9623
  elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
9624
+ elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
9625
+ elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
9530
9626
  elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
9531
9627
  elements.nativeCommandDialog.addEventListener("close", () => {
9532
9628
  elements.nativeCommandSearch.oninput = null;
@@ -9727,6 +9823,9 @@ document.addEventListener("pointerdown", (event) => {
9727
9823
  if (publishMenuOpen && !event.target?.closest?.(".composer-publish-menu")) {
9728
9824
  setPublishMenuOpen(false);
9729
9825
  }
9826
+ if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
9827
+ setNativeCommandMenuOpen(false);
9828
+ }
9730
9829
  if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
9731
9830
  setMobileTabsExpanded(false);
9732
9831
  }
@@ -9813,6 +9912,10 @@ window.addEventListener("keydown", (event) => {
9813
9912
  setPublishMenuOpen(false);
9814
9913
  return;
9815
9914
  }
9915
+ if (nativeCommandMenuOpen) {
9916
+ setNativeCommandMenuOpen(false);
9917
+ return;
9918
+ }
9816
9919
  if (document.body.classList.contains("composer-actions-open")) {
9817
9920
  setComposerActionsOpen(false);
9818
9921
  return;
package/public/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="manifest" href="/manifest.webmanifest" />
13
13
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
14
14
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
15
- <link rel="stylesheet" href="/styles.css?v=20" />
15
+ <link rel="stylesheet" href="/styles.css?v=23" />
16
16
  </head>
17
17
  <body>
18
18
  <button id="sidePanelExpandButton" class="side-panel-expand-button" type="button" aria-controls="sidePanel" aria-expanded="false" aria-label="Expand side panel" title="Expand side panel">
@@ -133,6 +133,27 @@
133
133
  </button>
134
134
  </div>
135
135
  </div>
136
+ <div class="composer-publish-menu composer-native-command-menu">
137
+ <button
138
+ id="nativeCommandMenuButton"
139
+ class="composer-icon-button composer-publish-button composer-native-command-button"
140
+ type="button"
141
+ title="Open skills and tools commands"
142
+ aria-label="Open /skills and /tools commands"
143
+ aria-haspopup="menu"
144
+ aria-expanded="false"
145
+ aria-controls="nativeCommandMenu"
146
+ data-tooltip="Skills/tools setup: open skill or tool setup."
147
+ ><svg class="composer-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16v14H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m7 10 2.5 2L7 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 15h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>
148
+ <div id="nativeCommandMenu" class="composer-publish-menu-panel composer-native-command-menu-panel" role="menu" aria-label="Skills and tools setup">
149
+ <button id="nativeSkillsButton" class="composer-publish-menu-item composer-native-command-menu-item" type="button" role="menuitem" data-command="/skills">
150
+ <span>Skills Setup</span>
151
+ </button>
152
+ <button id="nativeToolsButton" class="composer-publish-menu-item composer-native-command-menu-item" type="button" role="menuitem" data-command="/tools">
153
+ <span>Tools Setup</span>
154
+ </button>
155
+ </div>
156
+ </div>
136
157
  </div>
137
158
  <div class="spacer"></div>
138
159
  <button
@@ -364,6 +385,6 @@
364
385
  </form>
365
386
  </dialog>
366
387
 
367
- <script type="module" src="/app.js?v=20"></script>
388
+ <script type="module" src="/app.js?v=23"></script>
368
389
  </body>
369
390
  </html>
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "pi-webui-pwa-v20";
1
+ const CACHE_NAME = "pi-webui-pwa-v23";
2
2
  const APP_SHELL = [
3
3
  "/",
4
4
  "/index.html",
package/public/styles.css CHANGED
@@ -3092,6 +3092,22 @@ summary { cursor: pointer; color: var(--warning); }
3092
3092
  .composer-publish-button.menu-open {
3093
3093
  background: linear-gradient(120deg, var(--ctp-peach), var(--ctp-yellow), var(--ctp-mauve));
3094
3094
  }
3095
+ .composer-native-command-button {
3096
+ color: var(--ctp-mauve);
3097
+ border-color: rgba(203, 166, 247, 0.40);
3098
+ background:
3099
+ linear-gradient(120deg, rgba(203, 166, 247, 0.15), rgba(137, 180, 250, 0.10)),
3100
+ linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.88), rgba(var(--ctp-crust-rgb), 0.88));
3101
+ }
3102
+ .composer-native-command-button:hover,
3103
+ .composer-native-command-button.menu-open {
3104
+ color: #11111b;
3105
+ background: linear-gradient(120deg, var(--ctp-mauve), var(--ctp-blue), var(--ctp-teal));
3106
+ border-color: transparent;
3107
+ }
3108
+ .composer-native-command-menu.open .composer-native-command-button {
3109
+ border-color: rgba(203, 166, 247, 0.62);
3110
+ }
3095
3111
  .composer-publish-menu-panel {
3096
3112
  position: absolute;
3097
3113
  z-index: 100;
@@ -3141,6 +3157,20 @@ summary { cursor: pointer; color: var(--warning); }
3141
3157
  background: linear-gradient(120deg, var(--ctp-peach), var(--ctp-yellow));
3142
3158
  box-shadow: 0 0 1rem rgba(250, 179, 135, 0.20);
3143
3159
  }
3160
+ .composer-native-command-menu-item {
3161
+ color: var(--ctp-mauve);
3162
+ border-color: rgba(203, 166, 247, 0.32);
3163
+ background:
3164
+ linear-gradient(120deg, rgba(203, 166, 247, 0.12), rgba(137, 180, 250, 0.08)),
3165
+ var(--ctp-crust);
3166
+ }
3167
+ .composer-native-command-menu-item:hover,
3168
+ .composer-native-command-menu-item:focus-visible {
3169
+ color: #11111b;
3170
+ border-color: transparent;
3171
+ background: linear-gradient(120deg, var(--ctp-mauve), var(--ctp-blue));
3172
+ box-shadow: 0 0 1rem rgba(203, 166, 247, 0.20);
3173
+ }
3144
3174
  .composer button[data-tooltip] {
3145
3175
  position: relative;
3146
3176
  }
@@ -3213,6 +3243,15 @@ summary { cursor: pointer; color: var(--warning); }
3213
3243
  .composer-input-row button[data-tooltip].tooltip-open::before {
3214
3244
  transform: translate(-1.2rem, 0) rotate(45deg);
3215
3245
  }
3246
+ .composer-publish-menu:hover > .composer-publish-button[data-tooltip]::before,
3247
+ .composer-publish-menu:hover > .composer-publish-button[data-tooltip]::after,
3248
+ .composer-publish-menu:focus-within > .composer-publish-button[data-tooltip]::before,
3249
+ .composer-publish-menu:focus-within > .composer-publish-button[data-tooltip]::after,
3250
+ .composer-publish-menu.open > .composer-publish-button[data-tooltip]::before,
3251
+ .composer-publish-menu.open > .composer-publish-button[data-tooltip]::after {
3252
+ display: none !important;
3253
+ opacity: 0 !important;
3254
+ }
3216
3255
 
3217
3256
  .details {
3218
3257
  display: grid;
@@ -3489,6 +3528,18 @@ summary { cursor: pointer; color: var(--warning); }
3489
3528
  color: #ff9f43 !important;
3490
3529
  background: rgba(255, 159, 67, 0.10);
3491
3530
  }
3531
+ .native-selector-badge.native-selector-badge-pi-native,
3532
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-pi-native {
3533
+ border-color: rgba(137, 180, 250, 0.48);
3534
+ color: var(--ctp-blue);
3535
+ background: rgba(137, 180, 250, 0.10);
3536
+ }
3537
+ .native-selector-badge.native-selector-badge-external,
3538
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-external {
3539
+ border-color: rgba(203, 166, 247, 0.46);
3540
+ color: var(--ctp-mauve);
3541
+ background: rgba(203, 166, 247, 0.10);
3542
+ }
3492
3543
  .native-selector-detail,
3493
3544
  .native-selector-meta,
3494
3545
  .native-settings-hint {
@@ -98,6 +98,10 @@ assert.doesNotMatch(html, /class="side-panel-controls"[\s\S]*id="abortButton"/,
98
98
  assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
99
99
  assert.match(html, /id="releaseNpmButton"[^>]*data-command="\/release-npm"[\s\S]*?<span>NPM Release<\/span>/, "Publish menu should include the npm release workflow by label");
100
100
  assert.match(html, /id="releaseAurButton"[^>]*data-command="\/release-aur"[\s\S]*?<span>AUR Release<\/span>/, "Publish menu should include the AUR release workflow by label");
101
+ assert.match(html, /id="nativeCommandMenuButton"[\s\S]*?aria-controls="nativeCommandMenu"/, "composer should expose a /skills and /tools command menu button");
102
+ assert.ok(html.indexOf('id="publishButton"') < html.indexOf('id="nativeCommandMenuButton"'), "skills/tools command menu should render immediately after the Publish workflow button");
103
+ assert.match(html, /id="nativeSkillsButton"[^>]*data-command="\/skills"[\s\S]*?<span>Skills Setup<\/span>/, "skills/tools command menu should include Skills Setup");
104
+ assert.match(html, /id="nativeToolsButton"[^>]*data-command="\/tools"[\s\S]*?<span>Tools Setup<\/span>/, "skills/tools command menu should include Tools Setup");
101
105
  assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
102
106
  assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
103
107
  assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
@@ -180,9 +184,12 @@ assert.match(css, /\.action-feedback-controls:not\(:hover\):not\(:focus-within\)
180
184
  assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
181
185
  assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
182
186
  assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
187
+ assert.match(css, /\.composer-publish-menu:hover > \.composer-publish-button\[data-tooltip\]::before,[\s\S]*?\.composer-publish-menu\.open > \.composer-publish-button\[data-tooltip\]::after \{[\s\S]*?display:\s*none !important;[\s\S]*?opacity:\s*0 !important;/, "dropdown button tooltips should hide while publish or setup menus are open");
183
188
  assert.match(css, /\.composer-publish-menu-panel \{[\s\S]*?display:\s*none;[\s\S]*?flex-direction:\s*column/, "Publish workflow menu should hide when closed and expand like grouped tabs");
184
189
  assert.match(css, /\.composer-publish-menu:hover \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu:focus-within \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu\.open \.composer-publish-menu-panel \{\n\s+display:\s*flex;/, "Publish workflow menu should open on hover, focus, or explicit open state");
185
- assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish workflow button should fit beside Git workflow in mobile actions");
190
+ assert.match(css, /\.composer-native-command-button \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu should have a distinct slash-command button style");
191
+ assert.match(css, /\.composer-native-command-menu-item \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu items should be styled separately from publish actions");
192
+ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish and command menu buttons should fit beside Git workflow in mobile actions");
186
193
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
187
194
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
188
195
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
@@ -220,6 +227,8 @@ assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialo
220
227
  assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
221
228
  assert.match(css, /\.native-command-dialog \{[\s\S]*?width:\s*min\(56rem/, "native slash selector dialog should have roomy desktop layout");
222
229
  assert.match(css, /\.native-selector-item \{[\s\S]*?--tree-depth/, "native slash selector choices should support tree indentation");
230
+ assert.match(css, /\.native-selector-badge\.native-selector-badge-pi-native[\s\S]*?color:\s*var\(--ctp-blue\)/, "Tools Setup should distinguish Pi native tools with a Pi Native tag");
231
+ assert.match(css, /\.native-selector-badge\.native-selector-badge-external[\s\S]*?color:\s*var\(--ctp-mauve\)/, "Tools Setup should distinguish external tools with an External tag");
223
232
  assert.match(css, /\.native-settings-grid,[\s\S]*?\.native-tree-options \{[\s\S]*?grid-template-columns:/, "native settings and tree selector options should use responsive grids");
224
233
  assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
225
234
  assert.match(css, /\.extension-dialog\.release-dialog[\s\S]*?width:\s*min\(64rem/, "release confirmation dialogs should have more horizontal room");
@@ -339,6 +348,9 @@ assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable tog
339
348
  assert.match(app, /function renderOptionalFeatureDependentDisplays\(\)[\s\S]*renderOptionalFeatureControls\(\);[\s\S]*renderThemeSelect\(\);[\s\S]*renderWidgets\(\);[\s\S]*renderStatus\(\);[\s\S]*renderCommands\(\);[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\);[\s\S]*if \(streamRawText\) renderStreamingAssistantText\(\);/, "optional feature toggles should immediately refresh visible controls, commands, transcript, and live stream displays");
340
349
  assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*renderOptionalFeatureDependentDisplays\(\);[\s\S]*const tabContext = activeTabContext\(\);[\s\S]*refreshCommands\(tabContext\)/, "optional feature enable/disable should re-render the GUI and then refresh command capabilities");
341
350
  assert.match(app, /function setOptionalControlState\(button, available, unavailableTitle\)[\s\S]*setAttribute\("aria-label", nextAriaLabel\)[\s\S]*setAttribute\("data-tooltip", nextTooltip\)/, "optional feature button disabled state should update accessible labels and visible tooltips");
351
+ assert.match(app, /\["skills", "tuiSkillsCommand"\][\s\S]*\["tools", "tuiToolsCommand"\]/, "optional feature toggles should gate /skills and /tools command surfaces");
352
+ assert.match(app, /function setNativeCommandMenuOpen\(open\)/, "frontend should track the skills/tools command menu open state separately from Publish");
353
+ assert.match(app, /nativeSkillsButton\.hidden = !isOptionalFeatureEnabled\("tuiSkillsCommand"\)[\s\S]*nativeToolsButton\.hidden = !isOptionalFeatureEnabled\("tuiToolsCommand"\)/, "skills/tools menu items should be hidden by their optional feature toggles");
342
354
  assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
343
355
  assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
344
356
  assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
@@ -486,10 +498,18 @@ assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-bu
486
498
  assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
487
499
  assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
488
500
  assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*?sendPrompt\("prompt", command\)/, "Publish workflows should send slash commands directly without replacing the draft");
489
- assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerenter", \(\) => setPublishMenuOpen\(true\)\)/, "Publish menu should expand on hover");
501
+ assert.match(app, /async function runNativeCommandMenu\(command\)[\s\S]*?await handleNativeSlashSelectorCommand\(command\)/, "skills/tools command menu should open native selector dialogs directly");
502
+ assert.match(app, /function nativeToolOriginTag\(resource\)[\s\S]*?sourceInfo\?\.source === "builtin"[\s\S]*?label: "Pi Native"[\s\S]*?label: "External"/, "Tools Setup should classify built-in Pi tools separately from external tools");
503
+ assert.match(app, /renderNativeResourceToggles\(tools, \{[\s\S]*?getResourceTag: nativeToolOriginTag/, "Tools Setup should render Pi Native\/External tags");
504
+ assert.match(app, /const tags = Array\.isArray\(item\.tags\)[\s\S]*?item\.badge, \.\.\.tags/, "native selector filtering should include extra resource tags");
505
+ assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerenter", \(\) => \{[\s\S]*?setPublishMenuOpen\(true\);[\s\S]*?\}\)/, "Publish menu should expand on hover");
490
506
  assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerleave", \(\) => setPublishMenuOpen\(false\)\)/, "Publish menu should collapse after hover leaves");
507
+ assert.match(app, /nativeCommandMenuContainer\?\.addEventListener\("pointerenter", \(\) => \{[\s\S]*?setNativeCommandMenuOpen\(true\);[\s\S]*?\}\)/, "skills/tools command menu should expand on hover");
508
+ assert.match(app, /nativeCommandMenuContainer\?\.addEventListener\("pointerleave", \(\) => setNativeCommandMenuOpen\(false\)\)/, "skills/tools command menu should collapse after hover leaves");
491
509
  assert.match(app, /releaseNpmButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-npm"\)\)/, "Publish menu should launch /release-npm");
492
510
  assert.match(app, /releaseAurButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-aur"\)\)/, "Publish menu should launch /release-aur");
511
+ assert.match(app, /nativeSkillsButton\.addEventListener\("click", \(\) => runNativeCommandMenu\("\/skills"\)\)/, "skills/tools command menu should launch /skills");
512
+ assert.match(app, /nativeToolsButton\.addEventListener\("click", \(\) => runNativeCommandMenu\("\/tools"\)\)/, "skills/tools command menu should launch /tools");
493
513
  assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
494
514
  assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
495
515
  assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
@@ -600,7 +620,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
600
620
  assert.ok(manifest.icons?.some((icon) => icon.src === "/apple-touch-icon.png" && icon.sizes === "180x180"), "PWA manifest should include a conventional 180px apple touch icon");
601
621
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
602
622
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
603
- assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v20"/, "PWA service worker should define an app-shell cache");
623
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v23"/, "PWA service worker should define an app-shell cache");
604
624
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
605
625
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
606
626
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");