@wrongstack/webui 0.3.3 → 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();
@@ -212,6 +222,9 @@ var WrongStackWebSocketClient = class {
212
222
  messageQueue = [];
213
223
  pendingConfirms = /* @__PURE__ */ new Map();
214
224
  sessionId = null;
225
+ /** Auth token received from server in session.start payload.
226
+ * Used for reconnection so the client doesn't need to know the token upfront. */
227
+ wsToken = null;
215
228
  /** Stored last close reason / error message so the UI can show "what
216
229
  * went wrong" while reconnecting instead of a generic spinner. */
217
230
  lastErrorText;
@@ -245,7 +258,8 @@ var WrongStackWebSocketClient = class {
245
258
  }
246
259
  this.setStatus({ state: "connecting" });
247
260
  try {
248
- this.ws = new WebSocket(this.url);
261
+ const wsUrl = this.wsToken ? `${this.url}${this.url.includes("?") ? "&" : "?"}token=${this.wsToken}` : this.url;
262
+ this.ws = new WebSocket(wsUrl);
249
263
  this.ws.binaryType = "arraybuffer";
250
264
  const connectTimeout = setTimeout(() => {
251
265
  reject(new Error("Connection timeout"));
@@ -363,6 +377,9 @@ var WrongStackWebSocketClient = class {
363
377
  if (msg.type === "session.start") {
364
378
  const payload = msg.payload;
365
379
  this.sessionId = payload.sessionId;
380
+ if (payload.wsToken) {
381
+ this.wsToken = payload.wsToken;
382
+ }
366
383
  }
367
384
  this.emit(msg);
368
385
  }
@@ -1165,6 +1182,19 @@ function installHandlers(ws) {
1165
1182
  input: payload.input,
1166
1183
  suggestedPattern: payload.suggestedPattern
1167
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
+ }
1168
1198
  });
1169
1199
  on("run.result", (msg) => {
1170
1200
  const payload = msg.payload;
@@ -5377,7 +5407,7 @@ function ChatView() {
5377
5407
 
5378
5408
  // src/components/ConfirmDialog.tsx
5379
5409
  import { AlertTriangle as AlertTriangle2, FileEdit, Globe, ShieldAlert, Terminal as Terminal3, Wrench as Wrench3 } from "lucide-react";
5380
- import { useEffect as useEffect13 } from "react";
5410
+ import { useEffect as useEffect13, useRef as useRef10 } from "react";
5381
5411
 
5382
5412
  // src/components/ui/dialog.tsx
5383
5413
  import * as DialogPrimitive from "@radix-ui/react-dialog";
@@ -5505,6 +5535,7 @@ function SmartInputPreview({
5505
5535
  function ConfirmDialog() {
5506
5536
  const { showConfirmDialog, confirmInfo, hideConfirm } = useUIStore();
5507
5537
  const { sendConfirm } = useWebSocket();
5538
+ const dialogRef = useRef10(null);
5508
5539
  const handleConfirm = (decision) => {
5509
5540
  if (confirmInfo) {
5510
5541
  sendConfirm(confirmInfo.id, decision);
@@ -5526,9 +5557,13 @@ function ConfirmDialog() {
5526
5557
  } else if (e.key === "a" || e.key === "A") {
5527
5558
  e.preventDefault();
5528
5559
  handleConfirm("always");
5560
+ } else if (e.key === "d" || e.key === "D") {
5561
+ e.preventDefault();
5562
+ handleConfirm("deny");
5529
5563
  }
5530
5564
  };
5531
5565
  window.addEventListener("keydown", onKey);
5566
+ dialogRef.current?.focus();
5532
5567
  return () => window.removeEventListener("keydown", onKey);
5533
5568
  }, [showConfirmDialog, confirmInfo?.id]);
5534
5569
  if (!confirmInfo) {
@@ -5536,11 +5571,11 @@ function ConfirmDialog() {
5536
5571
  }
5537
5572
  const Icon2 = pickToolIcon(confirmInfo.toolName);
5538
5573
  const isEdit = /edit|write/i.test(confirmInfo.toolName);
5539
- 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: [
5540
5575
  /* @__PURE__ */ jsxs18(DialogHeader, { children: [
5541
5576
  /* @__PURE__ */ jsxs18(DialogTitle, { className: "flex items-center gap-2", children: [
5542
- /* @__PURE__ */ jsx19(ShieldAlert, { className: "h-5 w-5 text-yellow-500" }),
5543
- "Confirm: ",
5577
+ /* @__PURE__ */ jsx19(ShieldAlert, { className: "h-5 w-5 text-yellow-500 animate-pulse" }),
5578
+ "Approval required: ",
5544
5579
  confirmInfo.toolName
5545
5580
  ] }),
5546
5581
  /* @__PURE__ */ jsxs18(DialogDescription, { children: [
@@ -5575,14 +5610,17 @@ function ConfirmDialog() {
5575
5610
  ] })
5576
5611
  ] }),
5577
5612
  /* @__PURE__ */ jsxs18(DialogFooter, { className: "gap-2 sm:gap-2 flex-wrap", children: [
5578
- /* @__PURE__ */ jsx19(
5613
+ /* @__PURE__ */ jsxs18(
5579
5614
  Button,
5580
5615
  {
5581
5616
  variant: "outline",
5582
5617
  size: "sm",
5583
5618
  onClick: () => handleConfirm("deny"),
5584
- title: "Reject this and all future calls matching the pattern",
5585
- 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
+ ]
5586
5624
  }
5587
5625
  ),
5588
5626
  /* @__PURE__ */ jsxs18(
@@ -5733,7 +5771,7 @@ var ErrorBoundary = class extends Component {
5733
5771
 
5734
5772
  // src/components/QuickModelSwitcher.tsx
5735
5773
  import { ArrowRight as ArrowRight2, Cpu as Cpu3, Search as Search4 } from "lucide-react";
5736
- 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";
5737
5775
  import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
5738
5776
  function QuickModelSwitcher() {
5739
5777
  const open = useUIStore((s) => s.modelSwitcherOpen);
@@ -5742,7 +5780,7 @@ function QuickModelSwitcher() {
5742
5780
  const [selected, setSelected] = useState15(0);
5743
5781
  const [saved, setSaved] = useState15([]);
5744
5782
  const [modelsByProvider, setModelsByProvider] = useState15({});
5745
- const inputRef = useRef10(null);
5783
+ const inputRef = useRef11(null);
5746
5784
  const wsUrl = useConfigStore((s) => s.wsUrl);
5747
5785
  const currentProvider = useConfigStore((s) => s.provider);
5748
5786
  const currentModel = useConfigStore((s) => s.model);