@firstpick/pi-package-webui 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -58,6 +58,7 @@ const elements = {
58
58
  skillEditorCancelButton: $("#skillEditorCancelButton"),
59
59
  skillEditorSaveButton: $("#skillEditorSaveButton"),
60
60
  sendButton: $("#sendButton"),
61
+ btwButton: $("#btwButton"),
61
62
  commandSuggest: $("#commandSuggest"),
62
63
  attachmentTray: $("#attachmentTray"),
63
64
  attachButton: $("#attachButton"),
@@ -301,6 +302,11 @@ let statsOverlayLastScope = "14";
301
302
  let statsOverlayCalibrationMessage = "";
302
303
  let statsOverlayCalibrationBusy = "";
303
304
  let latestStatsOverlayPayload = null;
305
+ let latestBtwWidgetPayload = null;
306
+ let btwWidgetDismissedId = "";
307
+ let btwWidgetComposerOpen = false;
308
+ let btwWidgetInputDraft = "";
309
+ let btwWidgetFocusAfterRender = false;
304
310
  let latestWorkspace = null;
305
311
  let latestNetwork = null;
306
312
  let webuiVersion = "";
@@ -423,6 +429,14 @@ const GIT_INIT_STACK_STORAGE_KEY = "pi-webui-git-init-stack";
423
429
  const STATS_WEBUI_STATUS_KEY = "stats-webui";
424
430
  const STATS_WEBUI_PAYLOAD_TYPE = "firstpick.pi-extension-stats.overlay";
425
431
  const STATS_WEBUI_PAYLOAD_VERSION = 1;
432
+ const BTW_WEBUI_STATUS_KEY = "btw-webui";
433
+ const BTW_OUTPUT_WIDGET_KEY = "btw:output";
434
+ const BTW_FOOTER_WIDGET_KEY = "btw:footer";
435
+ const BTW_WIDGET_PAYLOAD_PREFIX = "BTW_WEBUI_PAYLOAD ";
436
+ const BTW_WEBUI_PAYLOAD_TYPES = new Set(["firstpick.pi-extension-btw.overlay", "firstpick.pi-extension-btw.output"]);
437
+ const WORKFLOW_WIDGET_PAYLOAD_PREFIX = "WORKFLOW_WEBUI_PAYLOAD ";
438
+ const WORKFLOW_SUBPROCESS_PAYLOAD_TYPE = "firstpick.pi-extension-workflows.subprocess";
439
+ const WORKFLOW_SUBPROCESS_PAYLOAD_VERSION = 1;
426
440
  const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
427
441
  const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
428
442
  const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
@@ -496,9 +510,11 @@ let liveToolRenderTimer = null;
496
510
  // commands and live widget events), not npm package folders. This keeps local dev
497
511
  // symlinks and independently installed packages working.
498
512
  const optionalFeatureAvailability = {
513
+ btwCommand: false,
499
514
  gitWorkflow: false,
500
515
  releaseNpm: false,
501
516
  releaseAur: false,
517
+ workflows: false,
502
518
  safetyGuard: false,
503
519
  statsCommand: false,
504
520
  gitFooterStatus: false,
@@ -509,6 +525,13 @@ const optionalFeatureAvailability = {
509
525
  themeBundle: false,
510
526
  };
511
527
  const OPTIONAL_FEATURES = [
528
+ {
529
+ id: "btwCommand",
530
+ label: "/btw side questions",
531
+ packageName: "@firstpick/pi-extension-btw",
532
+ capabilityLabel: "/btw or btw:output widget event",
533
+ description: "Ephemeral side-question command with TUI overlay and browser output-widget rendering.",
534
+ },
512
535
  {
513
536
  id: "gitWorkflow",
514
537
  label: "Guided Git workflow",
@@ -530,6 +553,13 @@ const OPTIONAL_FEATURES = [
530
553
  capabilityLabel: "/release-aur",
531
554
  description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
532
555
  },
556
+ {
557
+ id: "workflows",
558
+ label: "Workflows",
559
+ packageName: "@firstpick/pi-extension-workflows",
560
+ capabilityLabel: "/workflow or workflow subprocess widget event",
561
+ description: "Modular workflow runner with live subprocess output shown in a non-blocking Web UI widget.",
562
+ },
533
563
  {
534
564
  id: "safetyGuard",
535
565
  label: "Safety guard",
@@ -589,11 +619,16 @@ const OPTIONAL_FEATURES = [
589
619
  ];
590
620
  const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
591
621
  const OPTIONAL_COMMAND_FEATURES = new Map([
622
+ ["btw", "btwCommand"],
623
+ ["btw-transfer", "btwCommand"],
624
+ ["btw-status", "btwCommand"],
592
625
  ["git-staged-msg", "gitWorkflow"],
593
626
  ["git-branch-name", "gitWorkflow"],
594
627
  ["pr", "gitWorkflow"],
595
628
  ["release-npm", "releaseNpm"],
596
629
  ["release-aur", "releaseAur"],
630
+ ["workflow", "workflows"],
631
+ ["workflow-clear", "workflows"],
597
632
  ["safety-guard", "safetyGuard"],
598
633
  ["skills", "tuiSkillsCommand"],
599
634
  ["tools", "tuiToolsCommand"],
@@ -604,6 +639,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
604
639
  ]);
605
640
  const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
606
641
  HIDDEN_COMMAND_NAMES.add("stats-webui");
642
+ HIDDEN_COMMAND_NAMES.add("btw-status");
643
+ HIDDEN_COMMAND_NAMES.add("btw-transfer");
607
644
  const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "name", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
608
645
  const SETTINGS_THINKING_OPTIONS = ["off", "minimal", "low", "medium", "high", "xhigh"];
609
646
  const SETTINGS_TRANSPORT_OPTIONS = ["sse", "websocket", "websocket-cached", "auto"];
@@ -627,6 +664,7 @@ const optionalFeatureInstallInProgress = new Set();
627
664
  const optionalFeaturePackageStatuses = new Map();
628
665
  const optionalFeatureInstallMessages = new Map();
629
666
  const gitFooterPayloadRefreshInFlightByTab = new Set();
667
+ const gitFooterPiCalibrationInFlightByTab = new Set();
630
668
 
631
669
  function createGitWorkflowActionsDone(patch = {}) {
632
670
  return {
@@ -3511,6 +3549,15 @@ function setOptionalFeatureDisabled(featureId, disabled) {
3511
3549
  statusEntries.delete(GIT_FOOTER_WEBUI_STATUS_KEY);
3512
3550
  clearGitFooterWebuiPayloadCache();
3513
3551
  }
3552
+ if (featureId === "btwCommand") {
3553
+ statusEntries.delete(BTW_WEBUI_STATUS_KEY);
3554
+ widgets.delete(BTW_OUTPUT_WIDGET_KEY);
3555
+ widgets.delete(BTW_FOOTER_WIDGET_KEY);
3556
+ latestBtwWidgetPayload = null;
3557
+ btwWidgetDismissedId = "";
3558
+ btwWidgetComposerOpen = false;
3559
+ btwWidgetInputDraft = "";
3560
+ }
3514
3561
  storeDisabledOptionalFeatures();
3515
3562
  renderOptionalFeatureDependentDisplays();
3516
3563
  const tabContext = activeTabContext();
@@ -4145,6 +4192,10 @@ function resetActiveTabUi() {
4145
4192
  currentState = null;
4146
4193
  latestStats = null;
4147
4194
  latestStatsOverlayPayload = null;
4195
+ latestBtwWidgetPayload = null;
4196
+ btwWidgetDismissedId = "";
4197
+ btwWidgetComposerOpen = false;
4198
+ btwWidgetInputDraft = "";
4148
4199
  latestWorkspace = null;
4149
4200
  latestMessages = [];
4150
4201
  latestMessagesSessionKey = "";
@@ -4546,6 +4597,20 @@ function tabHasActiveAgent(tab) {
4546
4597
  return !!activity.isWorking || indicator.state === "working" || indicator.state === "blocked";
4547
4598
  }
4548
4599
 
4600
+ function activeTabHasConversationMessages(tab = activeTab()) {
4601
+ const tabId = tab?.id || activeTabId;
4602
+ if (!tabId) return false;
4603
+ if (tabId !== activeTabId && !latestMessagesSessionKey.startsWith(`${tabId}|`)) return false;
4604
+ return latestMessages.some((message) => ["user", "assistant"].includes(message?.role));
4605
+ }
4606
+
4607
+ function shouldOpenCwdChangeInNewTab(tab) {
4608
+ return !!tab?.conversationStarted
4609
+ || activeTabHasConversationMessages(tab)
4610
+ || stateHasVisibleWork(currentState)
4611
+ || tabHasActiveAgent(tab);
4612
+ }
4613
+
4549
4614
  function confirmCloseTerminalTabs(targetTabs, label) {
4550
4615
  const count = targetTabs.length;
4551
4616
  const noun = count === 1 ? "tab" : "tabs";
@@ -5268,6 +5333,58 @@ async function toggleFooterAutoCompaction(tabContext = activeTabContext()) {
5268
5333
  }
5269
5334
  }
5270
5335
 
5336
+ function scheduleGitFooterPiCalibrationRefresh(tabContext, delays = [600, 1600]) {
5337
+ for (const delayMs of delays) {
5338
+ setTimeout(() => {
5339
+ if (isCurrentTabContext(tabContext)) requestGitFooterWebuiPayload(tabContext, { force: true });
5340
+ }, delayMs);
5341
+ }
5342
+ }
5343
+
5344
+ async function runGitFooterPiCalibration(mode = "current", tabContext = activeTabContext()) {
5345
+ if (!tabContext.tabId) return;
5346
+ if (gitFooterPiCalibrationInFlightByTab.has(tabContext.tabId)) return;
5347
+ if (currentState?.isStreaming || currentState?.isCompacting) {
5348
+ addEvent("PI calibration can run after the active agent work finishes.", "warn");
5349
+ return;
5350
+ }
5351
+
5352
+ const commandName = resolveAvailableCommandName("calibrate", { rpcOnly: true });
5353
+ if (!commandName) {
5354
+ addEvent("PI calibration unavailable: /calibrate is not loaded in this Pi tab.", "warn");
5355
+ return;
5356
+ }
5357
+ if (mode === "probe" && !confirm("Start an isolated PI calibration probe? This sends one tiny model request and may incur provider token usage.")) return;
5358
+
5359
+ const command = mode === "probe" ? `/${commandName}` : `/${commandName} current`;
5360
+ gitFooterPiCalibrationInFlightByTab.add(tabContext.tabId);
5361
+ renderFooter();
5362
+ try {
5363
+ await sendPrompt("prompt", command, { targetTabId: tabContext.tabId, throwOnError: true });
5364
+ if (!isCurrentTabContext(tabContext)) return;
5365
+ addEvent(mode === "probe" ? "PI calibration probe started; refreshing git footer value after it records…" : "PI calibration requested; refreshing git footer value…", "info");
5366
+ scheduleGitFooterPiCalibrationRefresh(tabContext, mode === "probe" ? [5000, 14000] : [600, 1600]);
5367
+ } catch (error) {
5368
+ if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
5369
+ } finally {
5370
+ gitFooterPiCalibrationInFlightByTab.delete(tabContext.tabId);
5371
+ if (isCurrentTabContext(tabContext)) renderFooter();
5372
+ }
5373
+ }
5374
+
5375
+ function applyGitFooterPiCalibrationOptions(chip, options) {
5376
+ if (chip?.key !== "pi" || !FOOTER_PAYLOAD_ACTIONS.has(chip?.action)) return "";
5377
+ const tabContext = activeTabContext();
5378
+ const busy = !!tabContext.tabId && gitFooterPiCalibrationInFlightByTab.has(tabContext.tabId);
5379
+ const mode = chip.action === "calibrate-probe" ? "probe" : "current";
5380
+ options.onClick = () => runGitFooterPiCalibration(mode);
5381
+ if (busy) options.ariaBusy = true;
5382
+ if (busy) return "Calibrating PI estimate and refreshing this value…";
5383
+ return mode === "probe"
5384
+ ? "Click to start an isolated PI calibration probe, then refresh this value."
5385
+ : "Click to calibrate this uncalibrated PI estimate from the current session, then refresh this value.";
5386
+ }
5387
+
5271
5388
  function applyGitFooterContextToggleOptions(chip, options) {
5272
5389
  if (chip?.key !== "context") return "";
5273
5390
  options.onClick = () => toggleFooterAutoCompaction();
@@ -5449,6 +5566,7 @@ function footerMeta(label, value, className = "", options = {}) {
5449
5566
  }
5450
5567
 
5451
5568
  const FOOTER_PAYLOAD_TONES = new Set(["pink", "blue", "mauve", "yellow", "green", "teal"]);
5569
+ const FOOTER_PAYLOAD_ACTIONS = new Set(["calibrate-current", "calibrate-probe"]);
5452
5570
  const FOOTER_CHANGED_FILE_KINDS = new Set(["modified", "staged", "untracked", "conflicted"]);
5453
5571
  const FOOTER_CHANGED_FILE_KIND_ORDER = ["modified", "staged", "untracked", "conflicted"];
5454
5572
  const FOOTER_CHANGED_FILE_KIND_LABELS = {
@@ -5518,6 +5636,7 @@ function normalizeFooterPayloadChip(value, index) {
5518
5636
  tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
5519
5637
  title: cleanFooterPayloadText(value.title, "", 4000),
5520
5638
  };
5639
+ if (FOOTER_PAYLOAD_ACTIONS.has(value.action)) chip.action = value.action;
5521
5640
  if (Array.isArray(value.files)) {
5522
5641
  const files = value.files.map(normalizeFooterPayloadChangedFile).filter(Boolean).slice(0, 80);
5523
5642
  if (files.length) chip.files = files;
@@ -5752,7 +5871,7 @@ function applyFooterChangedFilesDropdown(node, chip) {
5752
5871
 
5753
5872
  function renderGitFooterPayloadMetric(chip) {
5754
5873
  const options = { tooltipAlign: gitFooterTooltipAlign(chip) };
5755
- const action = applyGitFooterContextToggleOptions(chip, options);
5874
+ const action = applyGitFooterPiCalibrationOptions(chip, options) || applyGitFooterContextToggleOptions(chip, options);
5756
5875
  options.title = gitFooterPayloadTooltip(chip, { action });
5757
5876
  const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", options);
5758
5877
  return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
@@ -7212,6 +7331,12 @@ async function changeActiveTabCwd() {
7212
7331
  const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
7213
7332
  const cwd = await pickCwd(tab, currentCwd);
7214
7333
  if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
7334
+
7335
+ if (shouldOpenCwdChangeInNewTab(tab)) {
7336
+ await createTerminalTab(cwd, { triggerButton: null });
7337
+ return;
7338
+ }
7339
+
7215
7340
  if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped. The conversation continues in the new directory.`)) return;
7216
7341
 
7217
7342
  saveActiveDraft();
@@ -7715,6 +7840,10 @@ function releaseNpmLineTone(line) {
7715
7840
  if (/^(WARN|warning)\b/i.test(clean)) return "warn";
7716
7841
  if (/^(INFO|npm notice|notice)\b/i.test(clean)) return "info";
7717
7842
  if (/^RELEASE_NPM_EVENT\b/.test(clean)) return "event";
7843
+ if (/^\[[0-9:]+\]\s+\[[^\]]+\]\s+\$/.test(clean)) return "command";
7844
+ if (/\b(STDERR|failed|error|exited with code)\b/i.test(clean)) return "fail";
7845
+ if (/\b(completed|succeeded|agent completed|tool completed)\b/i.test(clean)) return "pass";
7846
+ if (/\b(started|running|auto retry|compaction)\b/i.test(clean)) return "info";
7718
7847
  return "";
7719
7848
  }
7720
7849
 
@@ -7924,6 +8053,74 @@ function renderReleaseAurLogWidget() {
7924
8053
  return node;
7925
8054
  }
7926
8055
 
8056
+ function parseWorkflowSubprocessPayload(lines) {
8057
+ const raw = String(lines?.[0] || "").trim();
8058
+ if (!raw) return null;
8059
+ const json = raw.startsWith(WORKFLOW_WIDGET_PAYLOAD_PREFIX) ? raw.slice(WORKFLOW_WIDGET_PAYLOAD_PREFIX.length) : raw;
8060
+ try {
8061
+ const payload = JSON.parse(json);
8062
+ if (payload?.type !== WORKFLOW_SUBPROCESS_PAYLOAD_TYPE || payload.version !== WORKFLOW_SUBPROCESS_PAYLOAD_VERSION) return null;
8063
+ return payload;
8064
+ } catch {
8065
+ return null;
8066
+ }
8067
+ }
8068
+
8069
+ function workflowSubprocessIsLive(payload) {
8070
+ return payload?.status === "queued" || payload?.status === "running" || Number(payload?.taskCounts?.running || 0) > 0;
8071
+ }
8072
+
8073
+ function workflowTaskCountLabel(payload) {
8074
+ const counts = payload?.taskCounts || {};
8075
+ const done = Number(counts.completed || 0);
8076
+ const total = Number(counts.total || 0);
8077
+ const failed = Number(counts.failed || 0);
8078
+ const cancelled = Number(counts.cancelled || 0);
8079
+ return `${done}/${total} done${failed ? ` · ${failed} failed` : ""}${cancelled ? ` · ${cancelled} cancelled` : ""}`;
8080
+ }
8081
+
8082
+ function renderWorkflowSubprocessWidget() {
8083
+ if (!isOptionalFeatureEnabled("workflows")) return null;
8084
+ const payload = parseWorkflowSubprocessPayload(getWidgetLines("workflow:subprocess"));
8085
+ if (!payload) return null;
8086
+
8087
+ const live = workflowSubprocessIsLive(payload);
8088
+ const node = make("section", `widget release-npm-widget workflow-widget ${live ? "workflow-live-widget" : "workflow-log-widget"}`);
8089
+ node.setAttribute("aria-label", "workflow subprocess output");
8090
+
8091
+ const header = make("div", "release-npm-header");
8092
+ const titleWrap = make("div", "release-npm-title-wrap");
8093
+ titleWrap.append(
8094
+ make("span", "release-npm-kicker", "workflow subprocesses"),
8095
+ make("strong", "release-npm-title", payload.workflowName || payload.workflowKey || "workflow"),
8096
+ );
8097
+
8098
+ const meta = make("div", "release-npm-meta");
8099
+ meta.append(make("span", `release-npm-pill workflow-status ${payload.status || "unknown"}`, payload.status || "unknown"));
8100
+ if (payload.activePhase) meta.append(make("span", "release-npm-pill", payload.activePhase));
8101
+ meta.append(make("span", "release-npm-pill elapsed", workflowTaskCountLabel(payload)));
8102
+ if (payload.truncated) meta.append(make("span", "release-npm-pill workflow-truncated", "truncated"));
8103
+
8104
+ const actions = make("div", "release-npm-actions");
8105
+ actions.append(releaseNpmActionButton("Status", "/workflow status"));
8106
+ if (live) actions.append(releaseNpmActionButton("Abort", "/workflow abort", "danger"));
8107
+ actions.append(releaseNpmActionButton("Clear", "/workflow-clear"));
8108
+ header.append(titleWrap, meta, actions);
8109
+
8110
+ const lines = Array.isArray(payload.lines) && payload.lines.length ? payload.lines : ["Waiting for workflow subprocess output..."];
8111
+ const streamHeader = releaseNpmStreamHeader(live ? "Live subprocess output" : "Subprocess output", lines.length, { live });
8112
+ const terminal = make("div", "release-npm-terminal");
8113
+ terminal.setAttribute("role", "log");
8114
+ terminal.setAttribute("aria-live", live ? "polite" : "off");
8115
+ for (const line of lines) appendReleaseNpmTerminalLine(terminal, line);
8116
+
8117
+ const controls = make("div", "release-npm-controls", "Workflow subprocess output is shown as a non-blocking Web UI widget. Use /workflow abort to stop an active run.");
8118
+ const outputDetails = renderReleaseNpmOutputDetails("workflow:subprocess", streamHeader, terminal, controls);
8119
+ node.append(header, outputDetails);
8120
+ requestAnimationFrame(() => { if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight; });
8121
+ return node;
8122
+ }
8123
+
7927
8124
  function activeAppRunnerData() {
7928
8125
  return activeTabId ? appRunnerDataByTab.get(activeTabId) || { runners: [], activeRun: null } : { runners: [], activeRun: null };
7929
8126
  }
@@ -8914,6 +9111,274 @@ function handleStatsWebuiStatus(statusText) {
8914
9111
  if (payload.open || elements.statsOverlayDialog?.open) renderStatsOverlay();
8915
9112
  }
8916
9113
 
9114
+ function parseBtwWebuiPayloadRaw(raw) {
9115
+ if (!raw) return null;
9116
+ const text = String(raw || "");
9117
+ const json = text.startsWith(BTW_WIDGET_PAYLOAD_PREFIX) ? text.slice(BTW_WIDGET_PAYLOAD_PREFIX.length) : text;
9118
+ try {
9119
+ const parsed = JSON.parse(json);
9120
+ if (!BTW_WEBUI_PAYLOAD_TYPES.has(parsed?.type)) return null;
9121
+ return parsed;
9122
+ } catch {
9123
+ return null;
9124
+ }
9125
+ }
9126
+
9127
+ function parseBtwWidgetPayload(lines = []) {
9128
+ const first = Array.isArray(lines) ? lines[0] : "";
9129
+ return parseBtwWebuiPayloadRaw(first);
9130
+ }
9131
+
9132
+ function currentBtwWidgetPayload() {
9133
+ if (isOptionalFeatureDisabled("btwCommand")) return null;
9134
+ const outputLines = widgets.get(BTW_OUTPUT_WIDGET_KEY)?.widgetLines || [];
9135
+ const payload = parseBtwWidgetPayload(outputLines) || parseBtwWebuiPayloadRaw(statusEntries.get(BTW_WEBUI_STATUS_KEY)) || latestBtwWidgetPayload;
9136
+ if (payload?.id && payload.id === btwWidgetDismissedId) return null;
9137
+ return payload;
9138
+ }
9139
+
9140
+ function btwStatusLabel(payload) {
9141
+ switch (payload?.status) {
9142
+ case "done": return "Done";
9143
+ case "error": return "Error";
9144
+ case "aborted": return "Aborted";
9145
+ case "streaming": return "Answering…";
9146
+ default: return "Starting…";
9147
+ }
9148
+ }
9149
+
9150
+ function btwAnswerLines(payload) {
9151
+ const text = payload?.error || payload?.answer || (payload?.status === "loading" ? "Starting side request…" : "Waiting for model output…");
9152
+ return String(text || "").replace(/\r\n?/g, "\n").split("\n");
9153
+ }
9154
+
9155
+ function focusBtwWidgetInput() {
9156
+ const input = document.querySelector(".btw-widget-input");
9157
+ if (!input) return;
9158
+ try {
9159
+ input.focus({ preventScroll: true });
9160
+ } catch {
9161
+ input.focus();
9162
+ }
9163
+ }
9164
+
9165
+ function openBtwComposerWidget() {
9166
+ btwWidgetComposerOpen = true;
9167
+ btwWidgetDismissedId = "";
9168
+ btwWidgetFocusAfterRender = true;
9169
+ setComposerActionsOpen(false);
9170
+ setPublishMenuOpen(false);
9171
+ setNativeCommandMenuOpen(false);
9172
+ setAppRunnerMenuOpen(false);
9173
+ setOptionsMenuOpen(false);
9174
+ renderWidgets();
9175
+ }
9176
+
9177
+ function closeBtwOutputWidget() {
9178
+ const payload = currentBtwWidgetPayload();
9179
+ if (payload?.id) btwWidgetDismissedId = payload.id;
9180
+ widgets.delete(BTW_OUTPUT_WIDGET_KEY);
9181
+ widgets.delete(BTW_FOOTER_WIDGET_KEY);
9182
+ statusEntries.delete(BTW_WEBUI_STATUS_KEY);
9183
+ latestBtwWidgetPayload = null;
9184
+ btwWidgetComposerOpen = false;
9185
+ btwWidgetInputDraft = "";
9186
+ renderWidgets();
9187
+ renderStatus();
9188
+ }
9189
+
9190
+ async function copyBtwWidgetAnswer(button) {
9191
+ const payload = currentBtwWidgetPayload();
9192
+ const answer = String(payload?.answer || payload?.error || "").trim();
9193
+ if (!answer) return;
9194
+ const original = button?.textContent || "Copy";
9195
+ try {
9196
+ await navigator.clipboard.writeText(answer);
9197
+ if (button) button.textContent = "Copied";
9198
+ setTimeout(() => { if (button) button.textContent = original; }, 1600);
9199
+ } catch (error) {
9200
+ addEvent(`copy /btw answer failed: ${error.message || String(error)}`, "error");
9201
+ }
9202
+ }
9203
+
9204
+ function base64UrlEncodeUtf8(value) {
9205
+ const bytes = new TextEncoder().encode(String(value || ""));
9206
+ let binary = "";
9207
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
9208
+ binary += String.fromCharCode(...bytes.slice(offset, offset + 0x8000));
9209
+ }
9210
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
9211
+ }
9212
+
9213
+ function btwTransferPayload(payload) {
9214
+ return {
9215
+ question: payload?.question || "",
9216
+ answer: payload?.answer || payload?.error || "",
9217
+ status: payload?.status || "done",
9218
+ model: payload?.model || "",
9219
+ generatedAt: payload?.generatedAt || 0,
9220
+ updatedAt: payload?.updatedAt || Date.now(),
9221
+ };
9222
+ }
9223
+
9224
+ function makeBtwTransferIcon() {
9225
+ const ns = "http://www.w3.org/2000/svg";
9226
+ const svg = document.createElementNS(ns, "svg");
9227
+ svg.setAttribute("class", "btw-transfer-icon");
9228
+ svg.setAttribute("viewBox", "0 0 24 24");
9229
+ svg.setAttribute("aria-hidden", "true");
9230
+ svg.setAttribute("focusable", "false");
9231
+ const bubble = document.createElementNS(ns, "path");
9232
+ bubble.setAttribute("d", "M4.5 5.75h8.75a2 2 0 0 1 2 2v3.5a2 2 0 0 1-2 2H9.8L6.5 15.6v-2.35h-2a2 2 0 0 1-2-2v-3.5a2 2 0 0 1 2-2Z");
9233
+ bubble.setAttribute("fill", "none");
9234
+ bubble.setAttribute("stroke", "currentColor");
9235
+ bubble.setAttribute("stroke-width", "1.9");
9236
+ bubble.setAttribute("stroke-linecap", "round");
9237
+ bubble.setAttribute("stroke-linejoin", "round");
9238
+ const arrow = document.createElementNS(ns, "path");
9239
+ arrow.setAttribute("d", "M13 17h7m0 0-2.8-2.8M20 17l-2.8 2.8");
9240
+ arrow.setAttribute("fill", "none");
9241
+ arrow.setAttribute("stroke", "currentColor");
9242
+ arrow.setAttribute("stroke-width", "2.15");
9243
+ arrow.setAttribute("stroke-linecap", "round");
9244
+ arrow.setAttribute("stroke-linejoin", "round");
9245
+ const line = document.createElementNS(ns, "path");
9246
+ line.setAttribute("d", "M6 9.4h5.6");
9247
+ line.setAttribute("fill", "none");
9248
+ line.setAttribute("stroke", "currentColor");
9249
+ line.setAttribute("stroke-width", "1.9");
9250
+ line.setAttribute("stroke-linecap", "round");
9251
+ svg.append(bubble, line, arrow);
9252
+ return svg;
9253
+ }
9254
+
9255
+ async function transferBtwContextToMain(button) {
9256
+ const payload = currentBtwWidgetPayload();
9257
+ const transferPayload = btwTransferPayload(payload);
9258
+ if (!transferPayload.question && !transferPayload.answer) return;
9259
+ const targetTabId = activeTabId;
9260
+ const liveSteer = !!currentState?.isStreaming;
9261
+ const original = button?.querySelector("span")?.textContent || "Transfer Context";
9262
+ const encoded = base64UrlEncodeUtf8(JSON.stringify(transferPayload));
9263
+ try {
9264
+ await sendPrompt("prompt", `/btw-transfer ${encoded}`, { targetTabId, throwOnError: true, streamingBehavior: liveSteer ? "steer" : undefined });
9265
+ const label = button?.querySelector("span");
9266
+ if (label) label.textContent = liveSteer ? "Steered" : "Transferred";
9267
+ addEvent(liveSteer
9268
+ ? "/btw context sent as live steering; it will be injected after the next agent action"
9269
+ : "/btw context transferred into the main conversation", "info");
9270
+ setTimeout(() => { if (label) label.textContent = original; }, 1800);
9271
+ } catch {
9272
+ // sendPrompt already reports the error.
9273
+ }
9274
+ }
9275
+
9276
+ function btwWidgetActionButton(label, handler, className = "") {
9277
+ const button = make("button", `release-npm-action ${className}`.trim(), label);
9278
+ button.type = "button";
9279
+ button.addEventListener("click", () => handler(button));
9280
+ return button;
9281
+ }
9282
+
9283
+ function renderBtwComposerForm() {
9284
+ const form = make("form", "btw-widget-composer");
9285
+ const input = make("textarea", "btw-widget-input");
9286
+ input.rows = 1;
9287
+ input.placeholder = "Ask a /btw side question…";
9288
+ input.value = btwWidgetInputDraft;
9289
+ input.setAttribute("aria-label", "Ask a /btw side question");
9290
+ input.addEventListener("input", () => { btwWidgetInputDraft = input.value; });
9291
+ input.addEventListener("keydown", (event) => {
9292
+ if (event.key === "Enter" && !event.shiftKey) {
9293
+ event.preventDefault();
9294
+ form.requestSubmit();
9295
+ }
9296
+ });
9297
+
9298
+ const submit = make("button", "release-npm-action btw-widget-send", "Ask /btw");
9299
+ submit.type = "submit";
9300
+ form.append(input, submit);
9301
+ form.addEventListener("submit", async (event) => {
9302
+ event.preventDefault();
9303
+ const question = input.value.trim();
9304
+ if (!question) {
9305
+ input.focus();
9306
+ return;
9307
+ }
9308
+ submit.disabled = true;
9309
+ const sent = await sendBtwQuestion(question);
9310
+ submit.disabled = false;
9311
+ if (!sent) return;
9312
+ btwWidgetInputDraft = "";
9313
+ input.value = "";
9314
+ input.focus({ preventScroll: true });
9315
+ });
9316
+ return form;
9317
+ }
9318
+
9319
+ function renderBtwOutputWidget() {
9320
+ const payload = currentBtwWidgetPayload();
9321
+ if (!payload && !btwWidgetComposerOpen) return null;
9322
+
9323
+ if (payload) latestBtwWidgetPayload = payload;
9324
+ const running = payload?.status === "loading" || payload?.status === "streaming";
9325
+ const lineCount = payload ? btwAnswerLines(payload).length : 0;
9326
+ const node = make("section", `widget release-npm-widget btw-widget${running ? " btw-live-widget" : " btw-done-widget"}`);
9327
+ node.setAttribute("aria-label", "/btw side-question output");
9328
+
9329
+ const header = make("div", "release-npm-header");
9330
+ const titleWrap = make("div", "release-npm-title-wrap");
9331
+ titleWrap.append(make("span", "release-npm-kicker", "/btw"), make("strong", "release-npm-title", payload ? btwStatusLabel(payload) : "Ready"));
9332
+
9333
+ const meta = make("div", "release-npm-meta");
9334
+ meta.append(make("span", `release-npm-pill btw-status ${payload?.status || "ready"}`.trim(), payload?.status || "ready"));
9335
+ if (payload?.model) meta.append(make("span", "release-npm-pill", payload.model));
9336
+
9337
+ const actions = make("div", "release-npm-actions");
9338
+ const transferButton = btwWidgetActionButton("", transferBtwContextToMain, "btw-transfer-action");
9339
+ transferButton.title = currentState?.isStreaming
9340
+ ? "Transfer this /btw question and answer as live steering after the next agent action"
9341
+ : "Transfer this /btw question and answer into the main conversation context";
9342
+ transferButton.append(makeBtwTransferIcon(), make("span", undefined, "Transfer Context"));
9343
+ transferButton.disabled = !payload || !(payload.answer || payload.error || payload.question);
9344
+ actions.append(
9345
+ transferButton,
9346
+ btwWidgetActionButton("Copy", copyBtwWidgetAnswer),
9347
+ btwWidgetActionButton("Close", closeBtwOutputWidget),
9348
+ );
9349
+ header.append(titleWrap, meta, actions);
9350
+
9351
+ const question = make("div", "btw-widget-question");
9352
+ question.append(make("span", "btw-widget-question-label", "Question"), make("span", "btw-widget-question-text", payload?.question || "Start or continue with the /btw input below."));
9353
+
9354
+ const streamHeader = releaseNpmStreamHeader(running ? "Live side answer" : "Side answer", lineCount, { live: running });
9355
+ const terminal = make("div", "release-npm-terminal btw-terminal");
9356
+ terminal.setAttribute("role", "log");
9357
+ terminal.setAttribute("aria-live", running ? "polite" : "off");
9358
+ for (const line of (payload ? btwAnswerLines(payload) : ["Type a side question below and press Enter to run it as /btw."])) appendReleaseNpmTerminalLine(terminal, line);
9359
+
9360
+ const note = payload?.status === "error"
9361
+ ? "The side request failed. The main conversation was not changed."
9362
+ : "Ephemeral answer · every message in this input is sent as /btw · not appended to the main transcript.";
9363
+ const controls = make("div", "release-npm-controls btw-controls", note);
9364
+ const outputDetails = renderReleaseNpmOutputDetails(`btw:${payload?.id || "composer"}`, streamHeader, terminal, controls);
9365
+ node.append(header, question, outputDetails, renderBtwComposerForm());
9366
+ requestAnimationFrame(() => {
9367
+ if (outputDetails.open) terminal.scrollTop = terminal.scrollHeight;
9368
+ if (btwWidgetFocusAfterRender) {
9369
+ btwWidgetFocusAfterRender = false;
9370
+ focusBtwWidgetInput();
9371
+ }
9372
+ });
9373
+ return node;
9374
+ }
9375
+
9376
+ function handleBtwWebuiStatus(statusText) {
9377
+ const payload = parseBtwWebuiPayloadRaw(statusText);
9378
+ if (payload) latestBtwWidgetPayload = payload;
9379
+ renderWidgets();
9380
+ }
9381
+
8917
9382
  function remoteWebuiWidgetLines(lines = []) {
8918
9383
  return (Array.isArray(lines) ? lines : [])
8919
9384
  .map(stripAnsi)
@@ -8939,8 +9404,12 @@ function renderWidgets() {
8939
9404
  if (releaseAurOutput) elements.widgetArea.append(releaseAurOutput);
8940
9405
  const releaseAurLog = renderReleaseAurLogWidget();
8941
9406
  if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
9407
+ const workflowSubprocessWidget = renderWorkflowSubprocessWidget();
9408
+ if (workflowSubprocessWidget) elements.widgetArea.append(workflowSubprocessWidget);
8942
9409
  const appRunnerWidget = renderAppRunnerWidget();
8943
9410
  if (appRunnerWidget) elements.widgetArea.append(appRunnerWidget);
9411
+ const btwWidget = renderBtwOutputWidget();
9412
+ if (btwWidget) elements.widgetArea.append(btwWidget);
8944
9413
 
8945
9414
  for (const [key, value] of widgets) {
8946
9415
  const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
@@ -13353,6 +13822,41 @@ function sendPromptFromModeButton(kind, button) {
13353
13822
  sendPrompt(kind);
13354
13823
  }
13355
13824
 
13825
+ async function sendBtwQuestion(question, { clearComposerDraft = false } = {}) {
13826
+ const cleanQuestion = String(question || "").trim();
13827
+ if (!cleanQuestion) return false;
13828
+ const message = /^\/btw(?:\s|$)/.test(cleanQuestion) ? cleanQuestion : `/btw ${cleanQuestion}`;
13829
+ const targetTabId = activeTabId;
13830
+ btwWidgetComposerOpen = true;
13831
+ btwWidgetDismissedId = "";
13832
+ try {
13833
+ await sendPrompt("prompt", message, { targetTabId, throwOnError: true });
13834
+ } catch {
13835
+ return false;
13836
+ }
13837
+ if (!targetTabId) return true;
13838
+ if (clearComposerDraft) {
13839
+ if (targetTabId === activeTabId) {
13840
+ elements.promptInput.value = "";
13841
+ resizePromptInput();
13842
+ hideCommandSuggestions();
13843
+ saveActiveDraft();
13844
+ } else {
13845
+ tabDrafts.set(targetTabId, "");
13846
+ }
13847
+ }
13848
+ return true;
13849
+ }
13850
+
13851
+ async function sendBtwPromptFromButton() {
13852
+ const question = String(elements.promptInput.value || "").trim();
13853
+ if (!question) {
13854
+ openBtwComposerWidget();
13855
+ return;
13856
+ }
13857
+ await sendBtwQuestion(question, { clearComposerDraft: true });
13858
+ }
13859
+
13356
13860
  function setPublishMenuOpen(open) {
13357
13861
  publishMenuOpen = !!open;
13358
13862
  elements.publishButton.setAttribute("aria-expanded", publishMenuOpen ? "true" : "false");
@@ -13538,9 +14042,11 @@ function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force =
13538
14042
  }
13539
14043
 
13540
14044
  function updateOptionalFeatureAvailability() {
14045
+ optionalFeatureAvailability.btwCommand = hasAvailableCommand("btw") || optionalFeatureAvailability.btwCommand || statusEntries.has(BTW_WEBUI_STATUS_KEY) || widgets.has(BTW_OUTPUT_WIDGET_KEY);
13541
14046
  optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
13542
14047
  optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
13543
14048
  optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
14049
+ optionalFeatureAvailability.workflows = hasAvailableCommand("workflow") || hasAvailableCommand("workflow-clear") || optionalFeatureAvailability.workflows || widgets.has("workflow") || widgets.has("workflow:subprocess");
13544
14050
  optionalFeatureAvailability.safetyGuard = hasAvailableCommand("safety-guard") || optionalFeatureAvailability.safetyGuard || statusEntries.has("safety-guard");
13545
14051
  optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
13546
14052
  optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
@@ -13569,15 +14075,17 @@ function optionalFeatureStatus(featureId) {
13569
14075
  }
13570
14076
 
13571
14077
  function optionalFeatureWidgetFeatureId(key) {
14078
+ if (key.startsWith("btw:")) return "btwCommand";
13572
14079
  if (key.startsWith("release-npm:")) return "releaseNpm";
13573
14080
  if (key.startsWith("release-aur:")) return "releaseAur";
14081
+ if (key === "workflow" || key.startsWith("workflow:")) return "workflows";
13574
14082
  if (key === "todo-progress") return "todoProgressWidget";
13575
14083
  if (key === "pi-remote-webui") return "remoteWebui";
13576
14084
  return null;
13577
14085
  }
13578
14086
 
13579
14087
  function optionalFeatureWidgetHasSpecializedRenderer(key) {
13580
- return key.startsWith("release-npm:") || key.startsWith("release-aur:");
14088
+ return key.startsWith("btw:") || key.startsWith("release-npm:") || key.startsWith("release-aur:") || key === "workflow:subprocess";
13581
14089
  }
13582
14090
 
13583
14091
  function renderOptionalFeaturePanel() {
@@ -13640,6 +14148,16 @@ function renderOptionalFeaturePanel() {
13640
14148
  }
13641
14149
 
13642
14150
  function renderOptionalFeatureControls() {
14151
+ const hasBtwCommand = isOptionalFeatureEnabled("btwCommand");
14152
+ if (elements.btwButton) {
14153
+ elements.btwButton.hidden = !hasBtwCommand;
14154
+ setOptionalControlState(
14155
+ elements.btwButton,
14156
+ hasBtwCommand,
14157
+ optionalFeatureUnavailableMessage("btwCommand"),
14158
+ );
14159
+ }
14160
+
13643
14161
  const hasGitWorkflow = isOptionalFeatureEnabled("gitWorkflow");
13644
14162
  elements.gitWorkflowButton.hidden = !hasGitWorkflow;
13645
14163
  setOptionalControlState(
@@ -16349,7 +16867,7 @@ async function sendUserBashCommand(parsed, { usesPromptInput = false, targetTabI
16349
16867
  await runUserBashCommand(parsed, { usesPromptInput, targetTabId });
16350
16868
  }
16351
16869
 
16352
- async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = activeTabId, throwOnError = false } = {}) {
16870
+ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = activeTabId, throwOnError = false, streamingBehavior } = {}) {
16353
16871
  const usesPromptInput = explicitMessage === undefined;
16354
16872
  const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
16355
16873
  const originalMessage = String(rawMessage || "").trim();
@@ -16395,7 +16913,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
16395
16913
  response = await api("/api/follow-up", { method: "POST", body: bodyBase, tabId: targetTabId });
16396
16914
  } else {
16397
16915
  const body = { ...bodyBase };
16398
- if (targetWasStreaming) body.streamingBehavior = busyBehavior;
16916
+ if (targetWasStreaming) body.streamingBehavior = streamingBehavior || busyBehavior;
16399
16917
  response = await api("/api/prompt", { method: "POST", body, tabId: targetTabId });
16400
16918
  }
16401
16919
  applyResponseTab(response);
@@ -16480,6 +16998,7 @@ function handleExtensionUiRequest(request) {
16480
16998
  statusEntries.delete(statusKey);
16481
16999
  }
16482
17000
  if (statusKey === STATS_WEBUI_STATUS_KEY) handleStatsWebuiStatus(request.statusText);
17001
+ if (statusKey === BTW_WEBUI_STATUS_KEY) handleBtwWebuiStatus(request.statusText);
16483
17002
  updateOptionalFeatureAvailability();
16484
17003
  renderStatus();
16485
17004
  return;
@@ -16669,6 +17188,10 @@ function handleEvent(event) {
16669
17188
  clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
16670
17189
  statusEntries.clear();
16671
17190
  widgets.clear();
17191
+ latestBtwWidgetPayload = null;
17192
+ btwWidgetDismissedId = "";
17193
+ btwWidgetComposerOpen = false;
17194
+ btwWidgetInputDraft = "";
16672
17195
  resetOptionalFeatureAvailability();
16673
17196
  renderStatus();
16674
17197
  renderWidgets();
@@ -17021,6 +17544,7 @@ elements.busyPromptBehaviorMenu?.addEventListener("keydown", (event) => {
17021
17544
  });
17022
17545
  elements.steerButton.addEventListener("click", () => sendPromptFromModeButton("steer", elements.steerButton));
17023
17546
  elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton("follow-up", elements.followUpButton));
17547
+ elements.btwButton?.addEventListener("click", () => sendBtwPromptFromButton());
17024
17548
  elements.terminalTabsToggleButton.addEventListener("click", () => {
17025
17549
  setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
17026
17550
  });