@wrongstack/webui 0.3.4 → 0.3.7

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/dist/index.js CHANGED
@@ -108,6 +108,11 @@ function playCompletionChime() {
108
108
  tone(659.25, 0, 0.18);
109
109
  tone(880, 0.12, 0.24);
110
110
  }
111
+ function playPermissionChime() {
112
+ tone(523.25, 0, 0.15);
113
+ tone(659.25, 0.1, 0.15);
114
+ tone(783.99, 0.2, 0.25);
115
+ }
111
116
 
112
117
  // src/lib/favicon.ts
113
118
  var BASE_BG = "#4f46e5";
@@ -119,6 +124,8 @@ function buildSvg(status) {
119
124
  return '<circle cx="50" cy="14" r="14" fill="#ef4444" stroke="#fff" stroke-width="3" />';
120
125
  if (status === "running")
121
126
  return '<circle cx="50" cy="14" r="14" fill="#f59e0b" stroke="#fff" stroke-width="3" />';
127
+ if (status === "attention")
128
+ return '<circle cx="50" cy="14" r="14" fill="#eab308" stroke="#fff" stroke-width="3"><animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite"/></circle>';
122
129
  return "";
123
130
  })();
124
131
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
@@ -153,7 +160,7 @@ function installFaviconVisibilityReset() {
153
160
  if (visibilityHookInstalled || typeof document === "undefined") return;
154
161
  visibilityHookInstalled = true;
155
162
  document.addEventListener("visibilitychange", () => {
156
- if (!document.hidden && (currentStatus === "ready" || currentStatus === "error")) {
163
+ if (!document.hidden && (currentStatus === "ready" || currentStatus === "error" || currentStatus === "attention")) {
157
164
  setFaviconStatus("idle");
158
165
  }
159
166
  });
@@ -178,7 +185,7 @@ async function ensureNotificationPermission() {
178
185
  return "denied";
179
186
  }
180
187
  }
181
- function notifyIfHidden(title, body) {
188
+ function notifyIfHidden(title, body, tag) {
182
189
  if (typeof document === "undefined" || !document.hidden) return;
183
190
  if (permissionState !== "granted") return;
184
191
  try {
@@ -186,10 +193,13 @@ function notifyIfHidden(title, body) {
186
193
  body,
187
194
  icon: "/favicon.ico",
188
195
  // Tag-collapse: if multiple notifications stack while the tab is
189
- // hidden, only the latest "WrongStack run" shows up so we don't
190
- // litter the OS notification center.
191
- tag: "wrongstack-run"
192
- // Auto-dismiss as soon as the user focuses the tab.
196
+ // hidden, only the latest with the same tag shows up so we don't
197
+ // litter the OS notification center. Permission alerts use a
198
+ // separate tag so they don't get swallowed by run-completion.
199
+ tag: tag ?? "wrongstack-run",
200
+ // Require interaction for permission alerts — the agent is stuck
201
+ // until the user responds, so auto-dismiss would be harmful.
202
+ requireInteraction: tag === "wrongstack-confirm"
193
203
  });
194
204
  n.onclick = () => {
195
205
  window.focus();
@@ -1172,6 +1182,19 @@ function installHandlers(ws) {
1172
1182
  input: payload.input,
1173
1183
  suggestedPattern: payload.suggestedPattern
1174
1184
  });
1185
+ try {
1186
+ playPermissionChime();
1187
+ } catch {
1188
+ }
1189
+ void ensureNotificationPermission();
1190
+ notifyIfHidden(
1191
+ "WrongStack needs approval",
1192
+ `Tool "${payload.toolName}" is waiting for your decision.`,
1193
+ "wrongstack-confirm"
1194
+ );
1195
+ if (typeof document !== "undefined" && document.hidden) {
1196
+ setFaviconStatus("attention");
1197
+ }
1175
1198
  });
1176
1199
  on("run.result", (msg) => {
1177
1200
  const payload = msg.payload;
@@ -5384,7 +5407,7 @@ function ChatView() {
5384
5407
 
5385
5408
  // src/components/ConfirmDialog.tsx
5386
5409
  import { AlertTriangle as AlertTriangle2, FileEdit, Globe, ShieldAlert, Terminal as Terminal3, Wrench as Wrench3 } from "lucide-react";
5387
- import { useEffect as useEffect13 } from "react";
5410
+ import { useEffect as useEffect13, useRef as useRef10 } from "react";
5388
5411
 
5389
5412
  // src/components/ui/dialog.tsx
5390
5413
  import * as DialogPrimitive from "@radix-ui/react-dialog";
@@ -5512,6 +5535,7 @@ function SmartInputPreview({
5512
5535
  function ConfirmDialog() {
5513
5536
  const { showConfirmDialog, confirmInfo, hideConfirm } = useUIStore();
5514
5537
  const { sendConfirm } = useWebSocket();
5538
+ const dialogRef = useRef10(null);
5515
5539
  const handleConfirm = (decision) => {
5516
5540
  if (confirmInfo) {
5517
5541
  sendConfirm(confirmInfo.id, decision);
@@ -5533,9 +5557,13 @@ function ConfirmDialog() {
5533
5557
  } else if (e.key === "a" || e.key === "A") {
5534
5558
  e.preventDefault();
5535
5559
  handleConfirm("always");
5560
+ } else if (e.key === "d" || e.key === "D") {
5561
+ e.preventDefault();
5562
+ handleConfirm("deny");
5536
5563
  }
5537
5564
  };
5538
5565
  window.addEventListener("keydown", onKey);
5566
+ dialogRef.current?.focus();
5539
5567
  return () => window.removeEventListener("keydown", onKey);
5540
5568
  }, [showConfirmDialog, confirmInfo?.id]);
5541
5569
  if (!confirmInfo) {
@@ -5543,11 +5571,11 @@ function ConfirmDialog() {
5543
5571
  }
5544
5572
  const Icon2 = pickToolIcon(confirmInfo.toolName);
5545
5573
  const isEdit = /edit|write/i.test(confirmInfo.toolName);
5546
- return /* @__PURE__ */ jsx19(Dialog, { open: showConfirmDialog, onOpenChange: () => hideConfirm(), children: /* @__PURE__ */ jsxs18(DialogContent, { className: "sm:max-w-2xl", children: [
5574
+ return /* @__PURE__ */ jsx19(Dialog, { open: showConfirmDialog, onOpenChange: () => hideConfirm(), children: /* @__PURE__ */ jsxs18(DialogContent, { className: "sm:max-w-2xl border-yellow-500/50", ref: dialogRef, tabIndex: -1, children: [
5547
5575
  /* @__PURE__ */ jsxs18(DialogHeader, { children: [
5548
5576
  /* @__PURE__ */ jsxs18(DialogTitle, { className: "flex items-center gap-2", children: [
5549
- /* @__PURE__ */ jsx19(ShieldAlert, { className: "h-5 w-5 text-yellow-500" }),
5550
- "Confirm: ",
5577
+ /* @__PURE__ */ jsx19(ShieldAlert, { className: "h-5 w-5 text-yellow-500 animate-pulse" }),
5578
+ "Approval required: ",
5551
5579
  confirmInfo.toolName
5552
5580
  ] }),
5553
5581
  /* @__PURE__ */ jsxs18(DialogDescription, { children: [
@@ -5582,14 +5610,17 @@ function ConfirmDialog() {
5582
5610
  ] })
5583
5611
  ] }),
5584
5612
  /* @__PURE__ */ jsxs18(DialogFooter, { className: "gap-2 sm:gap-2 flex-wrap", children: [
5585
- /* @__PURE__ */ jsx19(
5613
+ /* @__PURE__ */ jsxs18(
5586
5614
  Button,
5587
5615
  {
5588
5616
  variant: "outline",
5589
5617
  size: "sm",
5590
5618
  onClick: () => handleConfirm("deny"),
5591
- title: "Reject this and all future calls matching the pattern",
5592
- children: "Deny always"
5619
+ title: "Reject this and all future calls matching the pattern (d)",
5620
+ children: [
5621
+ "Deny always ",
5622
+ /* @__PURE__ */ jsx19("kbd", { className: "ml-1 text-[10px] border rounded px-1 bg-background", children: "d" })
5623
+ ]
5593
5624
  }
5594
5625
  ),
5595
5626
  /* @__PURE__ */ jsxs18(
@@ -5740,7 +5771,7 @@ var ErrorBoundary = class extends Component {
5740
5771
 
5741
5772
  // src/components/QuickModelSwitcher.tsx
5742
5773
  import { ArrowRight as ArrowRight2, Cpu as Cpu3, Search as Search4 } from "lucide-react";
5743
- import { useEffect as useEffect15, useMemo as useMemo5, useRef as useRef10, useState as useState15 } from "react";
5774
+ import { useEffect as useEffect15, useMemo as useMemo5, useRef as useRef11, useState as useState15 } from "react";
5744
5775
  import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
5745
5776
  function QuickModelSwitcher() {
5746
5777
  const open = useUIStore((s) => s.modelSwitcherOpen);
@@ -5749,7 +5780,7 @@ function QuickModelSwitcher() {
5749
5780
  const [selected, setSelected] = useState15(0);
5750
5781
  const [saved, setSaved] = useState15([]);
5751
5782
  const [modelsByProvider, setModelsByProvider] = useState15({});
5752
- const inputRef = useRef10(null);
5783
+ const inputRef = useRef11(null);
5753
5784
  const wsUrl = useConfigStore((s) => s.wsUrl);
5754
5785
  const currentProvider = useConfigStore((s) => s.provider);
5755
5786
  const currentModel = useConfigStore((s) => s.model);