@hachej/boring-core 0.1.42 → 0.1.44

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.
@@ -8,13 +8,13 @@ import {
8
8
  useSignIn,
9
9
  useSignUp,
10
10
  useWorkspaceRouteStatus
11
- } from "../../chunk-MLTJKZL4.js";
11
+ } from "../../chunk-VYXEXOCO.js";
12
12
  import "../../chunk-HYNKZSTF.js";
13
- import "../../chunk-H5KU6R6Y.js";
13
+ import "../../chunk-LIBHVT7V.js";
14
14
  import "../../chunk-MLKGABMK.js";
15
15
 
16
16
  // src/app/front/CoreWorkspaceAgentFront.tsx
17
- import { useEffect as useEffect2, useMemo, useState as useState3 } from "react";
17
+ import { useEffect as useEffect3, useMemo, useState as useState3 } from "react";
18
18
  import { Navigate, Route, useLocation as useLocation2, useParams } from "react-router-dom";
19
19
  import {
20
20
  WorkspaceAgentFront as WorkspaceAgentFront2
@@ -63,14 +63,17 @@ function ChatFirstAuthenticatedShell({
63
63
  provisionWorkspace: false,
64
64
  bootPreloadPaths: [],
65
65
  bridgeEndpoint: null,
66
- excludeDefaults: ["filesystem"],
67
- plugins: [],
66
+ excludeDefaults: workspaceProps.excludeDefaults ?? ["filesystem"],
67
+ plugins: workspaceProps.plugins ?? [],
68
68
  catalogs: [],
69
69
  commands: [],
70
70
  persistenceEnabled: false,
71
71
  navEnabled: false,
72
72
  defaultNavOpen: false,
73
- defaultSurfaceOpen: false,
73
+ defaultSurfaceOpen: workspaceProps.defaultSurfaceOpen ?? false,
74
+ defaultWorkbenchLeftTab: workspaceProps.defaultWorkbenchLeftTab,
75
+ defaultWorkbenchLeftOpen: false,
76
+ surfaceInitialPanels: workspaceProps.surfaceInitialPanels,
74
77
  beforeShell: showComposerBlocker ? /* @__PURE__ */ jsxs(Fragment, { children: [
75
78
  workspaceProps.beforeShell,
76
79
  /* @__PURE__ */ jsx(ChatFirstComposerBlocker, {})
@@ -91,8 +94,10 @@ function ChatFirstAuthenticatedShell({
91
94
  }
92
95
 
93
96
  // src/app/front/chatFirst/ChatFirstPublicShell.tsx
94
- import { useState as useState2 } from "react";
97
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
95
98
  import { useLocation } from "react-router-dom";
99
+ import { builtinCommands } from "@hachej/boring-agent/front";
100
+ import { postUiCommand } from "@hachej/boring-workspace";
96
101
 
97
102
  // src/app/front/chatFirst/AuthCard.tsx
98
103
  import { useState } from "react";
@@ -111,7 +116,6 @@ function AuthCard({
111
116
  const [password, setPassword] = useState("");
112
117
  const [error, setError] = useState(null);
113
118
  const [submitting, setSubmitting] = useState(false);
114
- const forgotPasswordHref = `${routes.forgotPassword}?redirect=${encodeURIComponent(returnTo)}`;
115
119
  async function submit(event) {
116
120
  event.preventDefault();
117
121
  setError(null);
@@ -130,23 +134,20 @@ function AuthCard({
130
134
  setSubmitting(false);
131
135
  }
132
136
  }
133
- return /* @__PURE__ */ jsxs2("div", { className: "w-full max-w-sm rounded-2xl border border-border bg-card p-4 shadow-2xl", children: [
137
+ return /* @__PURE__ */ jsxs2("div", { className: "w-full max-w-xs rounded-2xl border border-border bg-card p-3 shadow-2xl", children: [
134
138
  onClose ? /* @__PURE__ */ jsx2("div", { className: "mb-3 flex justify-end", children: /* @__PURE__ */ jsx2("button", { type: "button", className: "rounded-full px-2 py-1 text-sm text-muted-foreground hover:bg-muted", onClick: onClose, "aria-label": "Close sign in", children: "\xD7" }) }) : null,
135
- /* @__PURE__ */ jsx2("h2", { id: "auth-modal-title", className: "text-center text-xl font-semibold", children: mode === "signin" ? "Sign in to continue" : "Create your account" }),
136
- /* @__PURE__ */ jsx2("p", { className: "mt-2 text-center text-sm text-muted-foreground", children: "Keep your draft and unlock the full workspace." }),
137
- /* @__PURE__ */ jsxs2("div", { className: "mt-4 grid grid-cols-2 rounded-xl bg-muted p-1 text-sm", children: [
139
+ /* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-2 rounded-xl bg-muted p-1 text-sm", children: [
138
140
  /* @__PURE__ */ jsx2("button", { type: "button", className: `rounded-lg px-3 py-2 ${mode === "signin" ? "bg-background shadow-sm" : "text-muted-foreground"}`, onClick: () => setMode("signin"), children: "Sign in" }),
139
141
  /* @__PURE__ */ jsx2("button", { type: "button", className: `rounded-lg px-3 py-2 ${mode === "signup" ? "bg-background shadow-sm" : "text-muted-foreground"}`, onClick: () => setMode("signup"), children: "Sign up" })
140
142
  ] }),
141
- /* @__PURE__ */ jsxs2("form", { className: "mt-4 space-y-2.5", onSubmit: submit, children: [
143
+ /* @__PURE__ */ jsxs2("form", { className: "mt-3 space-y-2", onSubmit: submit, children: [
142
144
  error ? /* @__PURE__ */ jsx2("div", { className: "rounded-lg border border-destructive/30 bg-destructive/10 p-2 text-sm text-destructive", role: "alert", children: error }) : null,
143
145
  mode === "signup" ? /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", placeholder: "Name", value: name, onChange: (event) => setName(event.currentTarget.value) }) : null,
144
146
  /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "email", autoComplete: "email", placeholder: "Email", value: email, onChange: (event) => setEmail(event.currentTarget.value), required: true }),
145
147
  /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "password", autoComplete: mode === "signin" ? "current-password" : "new-password", placeholder: "Password", value: password, onChange: (event) => setPassword(event.currentTarget.value), required: true }),
146
- mode === "signin" ? /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2("a", { href: forgotPasswordHref, className: "text-xs text-muted-foreground hover:underline", children: "Forgot password?" }) }) : null,
148
+ mode === "signin" ? /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2("a", { href: routes.forgotPassword, className: "text-xs text-muted-foreground hover:underline", children: "Forgot password?" }) }) : null,
147
149
  /* @__PURE__ */ jsx2("button", { type: "submit", className: "w-full rounded-xl bg-primary px-3 py-2.5 text-sm font-medium text-primary-foreground disabled:opacity-50", disabled: submitting, children: submitting ? "Please wait\u2026" : mode === "signin" ? "Continue with email" : "Create account" })
148
- ] }),
149
- /* @__PURE__ */ jsx2("p", { className: "mt-4 text-center text-xs text-muted-foreground", children: "By continuing, you agree to continue into your private workspace." })
150
+ ] })
150
151
  ] });
151
152
  }
152
153
 
@@ -233,17 +234,17 @@ function workspaceIdFromPath(pathname, workspaceRoute, workspaceIdParam) {
233
234
  }
234
235
 
235
236
  // src/app/front/chatFirst/ChatFirstPublicShell.tsx
236
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
237
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
237
238
  var defaultPublicEmptyState = {
238
- eyebrow: "Start here",
239
- title: "What do you want to build?",
240
- description: "Type a prompt or pick an example. Sign in on send to unlock your private workspace."
239
+ eyebrow: "Private AI workspace",
240
+ title: "Start in a secure workspace",
241
+ description: "Draft a task, then continue in an isolated workspace where the assistant can inspect files, make changes, and run commands only inside that workspace."
241
242
  };
242
243
  var defaultPublicSuggestions = [
243
- { label: "Build an app from scratch", hint: "Creates files, installs deps, opens a preview", prompt: "Build a full-stack app with auth, a dashboard, and sample data." },
244
- { label: "Understand a codebase", hint: "Maps the repo and explains where to start", prompt: "Explain this codebase, map the architecture, and suggest first improvements." },
245
- { label: "Fix a bug safely", hint: "Finds the cause, edits files, runs tests", prompt: "Trace a bug, edit the right files, update tests, and summarize the diff." },
246
- { label: "Prototype an interface", hint: "Turns an idea into an interactive UI", prompt: "Build an interactive prototype and open it in the workspace." }
244
+ { label: "Audit a workspace", hint: "Map files, risks, and next steps", prompt: "Inspect this workspace, summarize what is here, and identify the safest next steps." },
245
+ { label: "Make a safe change", hint: "Edit files and verify the result", prompt: "Make a small, safe change, run the relevant checks, and summarize the diff." },
246
+ { label: "Explain access boundaries", hint: "Files, commands, and model use", prompt: "Explain what you can access in this workspace, what commands you may run, and how model processing works." },
247
+ { label: "Plan a project", hint: "Turn an idea into milestones", prompt: "Turn my idea into a concrete workspace plan with milestones, risks, and verification steps." }
247
248
  ];
248
249
  function readComposerDraftFromDom() {
249
250
  if (typeof document === "undefined") return "";
@@ -258,13 +259,132 @@ function ChatFirstPublicShell({
258
259
  }) {
259
260
  const location = useLocation();
260
261
  const [modalOpen, setModalOpen] = useState2(false);
262
+ const lastAutoRunCommandRef = useRef("");
261
263
  const returnTo = safeReturnTo(location.pathname, location.search, location.hash);
264
+ const promptedDraft = new URLSearchParams(location.search).get("prompt")?.trim() ?? "";
265
+ const pendingReturnTo = promptedDraft ? "/" : returnTo;
262
266
  const workspaceId = intendedWorkspaceId || "public";
263
267
  const openAuth = (draft = readComposerDraftFromDom()) => {
264
- writePendingChatEntry({ draft, returnTo, ...intendedWorkspaceId ? { intendedWorkspaceId } : {} });
268
+ writePendingChatEntry({ draft, returnTo: pendingReturnTo, ...intendedWorkspaceId ? { intendedWorkspaceId } : {} });
265
269
  setModalOpen(true);
266
270
  };
267
- return /* @__PURE__ */ jsxs3("div", { className: "relative h-screen min-h-0", children: [
271
+ const normalizePublicCommand = (draft) => draft.trim().toLowerCase().replace(/[’`]/g, "'");
272
+ const openLandingPage = () => postUiCommand({
273
+ kind: "openPanel",
274
+ params: { id: "public-landing-page", component: "public.launch.landing", title: "Landing page" }
275
+ });
276
+ const openLetsChat = () => postUiCommand({
277
+ kind: "openPanel",
278
+ params: { id: "public-lets-chat", component: "public.launch.lets-chat", title: "Let\u2019s chat" }
279
+ });
280
+ const publicCommands = [
281
+ {
282
+ name: "landing-page",
283
+ description: "Open the landing page workspace tab.",
284
+ kind: "local",
285
+ handler: () => {
286
+ openLandingPage();
287
+ return { message: "Opened Landing page." };
288
+ }
289
+ },
290
+ {
291
+ name: "reach-out",
292
+ description: "Open the Calendly workspace tab.",
293
+ kind: "local",
294
+ handler: () => {
295
+ openLetsChat();
296
+ return { message: "Opened Let\u2019s chat." };
297
+ }
298
+ }
299
+ ];
300
+ const runPublicCommand = (draft) => {
301
+ const command = normalizePublicCommand(draft);
302
+ if (command === "/landing-page") {
303
+ openLandingPage();
304
+ return true;
305
+ }
306
+ if (command === "/reach-out" || command === "/let's-chat" || command === "/lets-chat") {
307
+ openLetsChat();
308
+ return true;
309
+ }
310
+ return false;
311
+ };
312
+ useEffect2(() => {
313
+ const intercept = (event) => {
314
+ const draft = readComposerDraftFromDom();
315
+ if (!runPublicCommand(draft)) return false;
316
+ event.preventDefault();
317
+ event.stopPropagation();
318
+ return true;
319
+ };
320
+ const onClick = (event) => {
321
+ const target = event.target instanceof Element ? event.target : null;
322
+ if (!target?.closest('[data-boring-agent-part="composer-submit"], [aria-label="Submit"]')) return;
323
+ intercept(event);
324
+ };
325
+ const maybeAutoRunDraft = () => {
326
+ const draft = readComposerDraftFromDom();
327
+ const command = normalizePublicCommand(draft);
328
+ const isCommand = command === "/landing-page" || command === "/reach-out" || command === "/let's-chat" || command === "/lets-chat";
329
+ if (!isCommand) {
330
+ lastAutoRunCommandRef.current = "";
331
+ return;
332
+ }
333
+ if (lastAutoRunCommandRef.current === command) return;
334
+ lastAutoRunCommandRef.current = command;
335
+ runPublicCommand(draft);
336
+ };
337
+ const onKeyDown = (event) => {
338
+ const target = event.target instanceof Element ? event.target : null;
339
+ if (!target?.closest('[data-boring-agent-part="composer-input"]')) return;
340
+ if (event.key === "Enter" && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) intercept(event);
341
+ };
342
+ const onInput = (event) => {
343
+ const target = event.target instanceof Element ? event.target : null;
344
+ if (!target?.closest('[data-boring-agent-part="composer-input"]')) return;
345
+ window.setTimeout(maybeAutoRunDraft, 0);
346
+ };
347
+ document.addEventListener("click", onClick, true);
348
+ document.addEventListener("keydown", onKeyDown, true);
349
+ document.addEventListener("input", onInput, true);
350
+ return () => {
351
+ document.removeEventListener("click", onClick, true);
352
+ document.removeEventListener("keydown", onKeyDown, true);
353
+ document.removeEventListener("input", onInput, true);
354
+ };
355
+ });
356
+ return /* @__PURE__ */ jsxs3("div", { className: "public-chat-first-shell relative h-screen min-h-0 bg-background", children: [
357
+ publicShell?.showTeachingArrows !== false && /* @__PURE__ */ jsxs3(Fragment2, { children: [
358
+ /* @__PURE__ */ jsxs3("div", { className: "public-arrow public-arrow-computer", "aria-hidden": "true", children: [
359
+ /* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Your remote computer" }),
360
+ /* @__PURE__ */ jsxs3("svg", { className: "public-arrow-svg", viewBox: "0 0 190 140", fill: "none", children: [
361
+ /* @__PURE__ */ jsx4(
362
+ "path",
363
+ {
364
+ className: "paw-stroke",
365
+ d: "M14 122 C 58 134 104 128 134 100 C 150 85 160 64 164 40"
366
+ }
367
+ ),
368
+ /* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M164 40 C 158 49 149 56 138 60" }),
369
+ /* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M164 40 C 168 51 170 63 169 75" })
370
+ ] })
371
+ ] }),
372
+ /* @__PURE__ */ jsxs3("div", { className: "public-arrow public-arrow-agent", "aria-hidden": "true", children: [
373
+ /* @__PURE__ */ jsxs3("svg", { className: "public-arrow-svg", viewBox: "0 0 120 116", fill: "none", children: [
374
+ /* @__PURE__ */ jsx4(
375
+ "path",
376
+ {
377
+ className: "paw-stroke",
378
+ d: "M60 104 C 56 76 64 50 70 24 C 71 19 73 14 76 10"
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 70 16 62 19 53 20" }),
382
+ /* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 82 14 87 21 90 29" })
383
+ ] }),
384
+ /* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Your agent" })
385
+ ] })
386
+ ] }),
387
+ /* @__PURE__ */ jsx4("aside", { className: "pointer-events-none fixed bottom-6 left-6 z-20 w-[300px]", children: /* @__PURE__ */ jsx4("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx4(AuthCard, { returnTo }) }) }),
268
388
  /* @__PURE__ */ jsx4(
269
389
  ChatFirstAuthenticatedShell,
270
390
  {
@@ -279,27 +399,32 @@ function ChatFirstPublicShell({
279
399
  chatParams: {
280
400
  ...workspaceProps.chatParams,
281
401
  emptyPlacement: "hero",
282
- composerPlaceholder: publicShell?.composerPlaceholder ?? "Describe the app, bug, or repo task you want help with\u2026",
402
+ composerPlaceholder: publicShell?.composerPlaceholder ?? "Type /landing-page or /reach-out",
403
+ hideComposerSettings: true,
404
+ suppressPreSubmitCancelledWarning: true,
405
+ thinkingControl: false,
406
+ initialDraft: promptedDraft || void 0,
283
407
  emptyState: {
284
408
  ...defaultPublicEmptyState,
285
409
  ...publicShell?.emptyState
286
410
  },
287
411
  suggestions: publicShell?.suggestions ?? defaultPublicSuggestions,
412
+ commands: publicCommands,
413
+ excludeBuiltinCommands: builtinCommands.map((command) => command.name),
288
414
  onBeforeSubmit: (draft) => {
289
- openAuth(draft);
415
+ if (!runPublicCommand(draft)) openAuth(draft);
290
416
  return false;
291
417
  }
292
418
  }
293
419
  }
294
420
  }
295
421
  ),
296
- /* @__PURE__ */ jsx4("aside", { className: "pointer-events-none fixed bottom-6 right-6 z-20 w-[320px]", children: /* @__PURE__ */ jsx4("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx4(AuthCard, { returnTo }) }) }),
297
422
  modalOpen ? /* @__PURE__ */ jsx4(AuthModal, { returnTo, onClose: () => setModalOpen(false) }) : null
298
423
  ] });
299
424
  }
300
425
 
301
426
  // src/app/front/CoreWorkspaceAgentFront.tsx
302
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
427
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
303
428
  var DEFAULT_WORKSPACE_ROUTE = "/workspace/:id";
304
429
  var DEFAULT_WORKSPACE_ID_PARAM = "id";
305
430
  function DefaultTopBarRight() {
@@ -332,11 +457,30 @@ function WorkspaceLoadingPage({
332
457
  ] }) })
333
458
  ] });
334
459
  }
460
+ function mergePublicWorkspaceProps(workspaceProps, publicWorkspaceProps) {
461
+ if (!publicWorkspaceProps) return workspaceProps;
462
+ return {
463
+ ...workspaceProps,
464
+ ...publicWorkspaceProps,
465
+ requestHeaders: {
466
+ ...workspaceProps.requestHeaders,
467
+ ...publicWorkspaceProps.requestHeaders
468
+ },
469
+ authHeaders: {
470
+ ...workspaceProps.authHeaders,
471
+ ...publicWorkspaceProps.authHeaders
472
+ },
473
+ chatParams: {
474
+ ...workspaceProps.chatParams,
475
+ ...publicWorkspaceProps.chatParams
476
+ }
477
+ };
478
+ }
335
479
  function usePendingChatDraft() {
336
480
  const session = useSession();
337
481
  const userId = session.data?.user?.id ?? null;
338
482
  const [pending, setPending] = useState3(() => userId ? readPendingChatEntry() : null);
339
- useEffect2(() => {
483
+ useEffect3(() => {
340
484
  if (!userId) {
341
485
  setPending(null);
342
486
  return;
@@ -354,7 +498,8 @@ function HomeRedirect({
354
498
  chatEntryMode,
355
499
  appTitle,
356
500
  workspaceProps,
357
- chatFirstPublicShell
501
+ chatFirstPublicShell,
502
+ chatFirstPublicWorkspaceProps
358
503
  }) {
359
504
  const location = useLocation2();
360
505
  const session = useSession();
@@ -366,7 +511,16 @@ function HomeRedirect({
366
511
  location.search,
367
512
  location.hash
368
513
  );
369
- if (!session.data?.user && chatEntryMode === "chat-first") return /* @__PURE__ */ jsx5(ChatFirstPublicShell, { appTitle, publicShell: chatFirstPublicShell, workspaceProps });
514
+ if (!session.data?.user && chatEntryMode === "chat-first") {
515
+ return /* @__PURE__ */ jsx5(
516
+ ChatFirstPublicShell,
517
+ {
518
+ appTitle,
519
+ publicShell: chatFirstPublicShell,
520
+ workspaceProps: mergePublicWorkspaceProps(workspaceProps, chatFirstPublicWorkspaceProps)
521
+ }
522
+ );
523
+ }
370
524
  if (!workspace && chatEntryMode === "chat-first" && session.data?.user && restorePendingDraft) {
371
525
  return /* @__PURE__ */ jsx5(
372
526
  ChatFirstAuthenticatedShell,
@@ -378,7 +532,7 @@ function HomeRedirect({
378
532
  }
379
533
  );
380
534
  }
381
- if (!workspace) return /* @__PURE__ */ jsx5(Fragment2, { children: loadingFallback });
535
+ if (!workspace) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
382
536
  return /* @__PURE__ */ jsx5(Navigate, { to: workspaceHref(workspace.id), replace: true });
383
537
  }
384
538
  function WorkspaceRouteErrorPage({ status, message }) {
@@ -396,7 +550,8 @@ function WorkspaceRoute({
396
550
  chatEntryMode,
397
551
  appTitle,
398
552
  workspaceRoute,
399
- chatFirstPublicShell
553
+ chatFirstPublicShell,
554
+ chatFirstPublicWorkspaceProps
400
555
  }) {
401
556
  const params = useParams();
402
557
  const location = useLocation2();
@@ -420,9 +575,17 @@ function WorkspaceRoute({
420
575
  () => ({ ...workspaceProps.authHeaders, "x-boring-workspace-id": workspaceId }),
421
576
  [workspaceId, workspaceProps.authHeaders]
422
577
  );
423
- if (!workspaceId) return /* @__PURE__ */ jsx5(Fragment2, { children: loadingFallback });
578
+ if (!workspaceId) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
424
579
  if (!session.data?.user && chatEntryMode === "chat-first") {
425
- return /* @__PURE__ */ jsx5(ChatFirstPublicShell, { appTitle, intendedWorkspaceId: workspaceId, publicShell: chatFirstPublicShell, workspaceProps });
580
+ return /* @__PURE__ */ jsx5(
581
+ ChatFirstPublicShell,
582
+ {
583
+ appTitle,
584
+ intendedWorkspaceId: workspaceId,
585
+ publicShell: chatFirstPublicShell,
586
+ workspaceProps: mergePublicWorkspaceProps(workspaceProps, chatFirstPublicWorkspaceProps)
587
+ }
588
+ );
426
589
  }
427
590
  if (routeStatus.status === "not-found" || routeStatus.status === "forbidden" || routeStatus.status === "switch-failed") {
428
591
  return /* @__PURE__ */ jsx5(WorkspaceRouteErrorPage, { status: routeStatus.status, message: routeStatus.message });
@@ -438,7 +601,7 @@ function WorkspaceRoute({
438
601
  }
439
602
  );
440
603
  }
441
- if (routeStatus.status !== "matched" || currentWorkspace?.id !== workspaceId) return /* @__PURE__ */ jsx5(Fragment2, { children: loadingFallback });
604
+ if (routeStatus.status !== "matched" || currentWorkspace?.id !== workspaceId) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
442
605
  const shouldRestorePendingDraft = restorePendingDraft && Boolean(pendingChatEntry?.draft);
443
606
  const chatParams = {
444
607
  ...workspaceProps.chatParams,
@@ -456,6 +619,7 @@ function WorkspaceRoute({
456
619
  {
457
620
  ...workspaceProps,
458
621
  workspaceId,
622
+ workspaceLabel: workspaceProps.workspaceLabel ?? currentWorkspace.name,
459
623
  requestHeaders,
460
624
  authHeaders,
461
625
  chatParams,
@@ -481,11 +645,13 @@ function CoreWorkspaceAgentFront({
481
645
  bootPreloadPaths,
482
646
  topBarLeft = /* @__PURE__ */ jsx5(WorkspaceSwitcher, {}),
483
647
  topBarRight = /* @__PURE__ */ jsx5(DefaultTopBarRight, {}),
484
- appTitle = "Boring",
648
+ appTitle = "Sovereign Workspace",
485
649
  bridgeEndpoint = "/api/v1/ui",
486
650
  hotReload = false,
487
651
  chatEntryMode = "auth-first",
488
652
  chatFirstPublicShell,
653
+ chatFirstPublicWorkspaceProps,
654
+ publicPaths,
489
655
  ...workspaceProps
490
656
  }) {
491
657
  if (hotReload !== false) {
@@ -515,7 +681,7 @@ function CoreWorkspaceAgentFront({
515
681
  cspNonce,
516
682
  workspaceRoute,
517
683
  workspaceIdParam,
518
- publicPaths: chatEntryMode === "chat-first" ? chatFirstPublicPaths(workspaceRoute) : void 0,
684
+ publicPaths: chatEntryMode === "chat-first" ? [...chatFirstPublicPaths(workspaceRoute), ...publicPaths ?? []] : publicPaths,
519
685
  children: [
520
686
  /* @__PURE__ */ jsx5(
521
687
  Route,
@@ -529,7 +695,8 @@ function CoreWorkspaceAgentFront({
529
695
  chatEntryMode,
530
696
  appTitle,
531
697
  workspaceProps: resolvedWorkspaceProps,
532
- chatFirstPublicShell
698
+ chatFirstPublicShell,
699
+ chatFirstPublicWorkspaceProps
533
700
  }
534
701
  )
535
702
  }
@@ -548,7 +715,8 @@ function CoreWorkspaceAgentFront({
548
715
  chatEntryMode,
549
716
  appTitle,
550
717
  workspaceRoute,
551
- chatFirstPublicShell
718
+ chatFirstPublicShell,
719
+ chatFirstPublicWorkspaceProps
552
720
  }
553
721
  )
554
722
  }
@@ -558,6 +726,629 @@ function CoreWorkspaceAgentFront({
558
726
  }
559
727
  );
560
728
  }
729
+
730
+ // src/app/front/credits/CreditBalanceBadge.tsx
731
+ import { useState as useState5 } from "react";
732
+ import { Button, Popover, PopoverContent, PopoverTrigger } from "@hachej/boring-ui-kit";
733
+ import { Plus } from "lucide-react";
734
+
735
+ // src/app/front/credits/helpers.ts
736
+ function creditNetMicros(balance) {
737
+ const remaining = Number.isFinite(balance.remainingMicros) ? balance.remainingMicros : 0;
738
+ const debt = Number.isFinite(balance.debtMicros) ? balance.debtMicros : 0;
739
+ return remaining - debt;
740
+ }
741
+ function formatSignedCreditMicros(micros, currency = "EUR", locale) {
742
+ const major = (Number.isFinite(micros) ? micros : 0) / 1e6;
743
+ const sign = major > 0 ? "+" : major < 0 ? "\u2212" : "";
744
+ const abs = new Intl.NumberFormat(locale, { style: "currency", currency }).format(Math.abs(major));
745
+ return `${sign}${abs}`;
746
+ }
747
+ function formatMinorPrice(priceMinor, currency, locale) {
748
+ const major = (Number.isFinite(priceMinor) ? priceMinor : 0) / 100;
749
+ return new Intl.NumberFormat(locale, { style: "currency", currency, maximumFractionDigits: major % 1 === 0 ? 0 : 2 }).format(major);
750
+ }
751
+ function formatCreditMicros(micros, currency = "EUR", locale) {
752
+ const major = (Number.isFinite(micros) ? Math.max(0, micros) : 0) / 1e6;
753
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(major);
754
+ }
755
+ function isLowBalance(micros, thresholdMicros = 5e5) {
756
+ return Number.isFinite(micros) && micros <= thresholdMicros;
757
+ }
758
+ var PAYMENT_REQUIRED_ERROR_CODE = "PAYMENT_REQUIRED";
759
+ function isPaymentRequiredNotice(notice) {
760
+ return notice.errorCode === PAYMENT_REQUIRED_ERROR_CODE;
761
+ }
762
+
763
+ // src/app/front/credits/useCreditBalance.ts
764
+ import { useCallback, useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
765
+ var CREDITS_REFRESH_EVENT = "credits:refresh";
766
+ var CHECKOUT_BASELINE_STORAGE_KEY = "credits:checkout-baseline";
767
+ var CHECKOUT_BASELINE_TTL_MS = 60 * 60 * 1e3;
768
+ var RETRY_BURST_MS = [0, 1e3, 2e3, 4e3, 8e3];
769
+ function useCreditBalance({
770
+ apiBaseUrl = "",
771
+ pollMs = 3e4,
772
+ pack
773
+ } = {}) {
774
+ const [balance, setBalance] = useState4(null);
775
+ const [hidden, setHidden] = useState4(false);
776
+ const [buying, setBuying] = useState4(false);
777
+ const [lastUpdatedAt, setLastUpdatedAt] = useState4(null);
778
+ const [updating, setUpdating] = useState4(false);
779
+ const [error, setError] = useState4(null);
780
+ const buyingRef = useRef2(false);
781
+ const burstRef = useRef2(0);
782
+ const timersRef = useRef2([]);
783
+ const burstActiveRef = useRef2(false);
784
+ const balanceRef = useRef2(null);
785
+ const refresh = useCallback(async () => {
786
+ setUpdating(true);
787
+ try {
788
+ const res = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
789
+ if (res.status === 401) {
790
+ setHidden(true);
791
+ return;
792
+ }
793
+ if (!res.ok) {
794
+ setError("Could not load your balance.");
795
+ return;
796
+ }
797
+ const data = await res.json();
798
+ if (!data.enabled) {
799
+ setHidden(true);
800
+ return;
801
+ }
802
+ setBalance(data);
803
+ balanceRef.current = data;
804
+ setHidden(false);
805
+ setError(null);
806
+ setLastUpdatedAt(Date.now());
807
+ } catch {
808
+ setError("Could not load your balance.");
809
+ } finally {
810
+ if (!burstActiveRef.current) setUpdating(false);
811
+ }
812
+ }, [apiBaseUrl]);
813
+ const refreshWithRetry = useCallback(() => {
814
+ const token = burstRef.current += 1;
815
+ for (const t of timersRef.current) clearTimeout(t);
816
+ burstActiveRef.current = true;
817
+ setUpdating(true);
818
+ const lastIndex = RETRY_BURST_MS.length - 1;
819
+ timersRef.current = RETRY_BURST_MS.map(
820
+ (delay, index) => setTimeout(async () => {
821
+ if (burstRef.current !== token) return;
822
+ try {
823
+ await refresh();
824
+ } finally {
825
+ if (index === lastIndex && burstRef.current === token) {
826
+ burstActiveRef.current = false;
827
+ setUpdating(false);
828
+ }
829
+ }
830
+ }, delay)
831
+ );
832
+ }, [refresh]);
833
+ useEffect4(() => {
834
+ void refresh();
835
+ const interval = setInterval(() => void refresh(), pollMs);
836
+ const onFocus = () => void refresh();
837
+ const onRefreshEvent = () => refreshWithRetry();
838
+ window.addEventListener("focus", onFocus);
839
+ window.addEventListener(CREDITS_REFRESH_EVENT, onRefreshEvent);
840
+ let channel = null;
841
+ try {
842
+ channel = new BroadcastChannel("credits");
843
+ channel.onmessage = () => refreshWithRetry();
844
+ } catch {
845
+ }
846
+ return () => {
847
+ clearInterval(interval);
848
+ window.removeEventListener("focus", onFocus);
849
+ window.removeEventListener(CREDITS_REFRESH_EVENT, onRefreshEvent);
850
+ for (const t of timersRef.current) clearTimeout(t);
851
+ channel?.close();
852
+ };
853
+ }, [refresh, refreshWithRetry, pollMs]);
854
+ const buy = useCallback(async (overridePack) => {
855
+ if (buyingRef.current) return null;
856
+ buyingRef.current = true;
857
+ setBuying(true);
858
+ let win = null;
859
+ try {
860
+ win = window.open("about:blank", "_blank");
861
+ if (!win) return "Could not open the checkout tab. Please allow pop-ups for this site and try again.";
862
+ try {
863
+ win.opener = null;
864
+ } catch {
865
+ }
866
+ const chosen = overridePack ?? pack;
867
+ const res = await fetch(`${apiBaseUrl}/api/credits/checkout`, {
868
+ method: "POST",
869
+ credentials: "include",
870
+ headers: { "content-type": "application/json" },
871
+ body: JSON.stringify(chosen ? { pack: chosen } : {})
872
+ });
873
+ if (!res.ok) {
874
+ win.close();
875
+ return "Could not start checkout. Please try again.";
876
+ }
877
+ const { url } = await res.json();
878
+ if (!url) {
879
+ win.close();
880
+ return "Checkout is not available right now.";
881
+ }
882
+ try {
883
+ let current = balanceRef.current;
884
+ try {
885
+ const bres = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
886
+ if (bres.ok) {
887
+ const fresh = await bres.json();
888
+ if (fresh?.enabled) current = fresh;
889
+ }
890
+ } catch {
891
+ }
892
+ if (current) {
893
+ window.localStorage.setItem(
894
+ CHECKOUT_BASELINE_STORAGE_KEY,
895
+ // userId so the return handler can reject a baseline left by a DIFFERENT
896
+ // user (shared localStorage) instead of confirming a phantom purchase.
897
+ JSON.stringify({ net: creditNetMicros(current), ts: Date.now(), userId: current.userId })
898
+ );
899
+ }
900
+ } catch {
901
+ }
902
+ win.location.href = url;
903
+ return null;
904
+ } catch {
905
+ win?.close();
906
+ return "Could not reach the checkout service. Please try again.";
907
+ } finally {
908
+ buyingRef.current = false;
909
+ setBuying(false);
910
+ }
911
+ }, [apiBaseUrl, pack]);
912
+ return { balance, hidden, error, refresh, refreshWithRetry, buy, buying, lastUpdatedAt, updating };
913
+ }
914
+
915
+ // src/app/front/credits/CreditBalanceBadge.tsx
916
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
917
+ function CreditBalanceBadge({
918
+ apiBaseUrl = "",
919
+ buyEnabled = false,
920
+ pollMs = 3e4,
921
+ locale
922
+ }) {
923
+ const { balance, hidden, buy, buying } = useCreditBalance({ apiBaseUrl, pollMs });
924
+ const [open, setOpen] = useState5(false);
925
+ if (hidden || !balance) return null;
926
+ const inDebt = (balance.debtMicros ?? 0) > 0;
927
+ const low = inDebt || isLowBalance(balance.remainingMicros);
928
+ const showBuy = balance.checkoutEnabled ?? buyEnabled;
929
+ const packs = (balance.packs ?? []).filter((p) => !p.custom);
930
+ const currency = balance.packs?.[0]?.currency ?? "EUR";
931
+ const pick = async (packId) => {
932
+ setOpen(false);
933
+ await buy(packId);
934
+ };
935
+ return /* @__PURE__ */ jsxs5("div", { className: "inline-flex items-center gap-1.5", "data-low": low ? "true" : "false", "data-debt": inDebt ? "true" : "false", children: [
936
+ /* @__PURE__ */ jsx6(
937
+ "span",
938
+ {
939
+ title: inDebt ? "Amount owed \u2014 top up to resume" : "Remaining credits",
940
+ className: `text-[11px] tabular-nums ${low ? "text-destructive" : "text-muted-foreground"}`,
941
+ children: inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale)
942
+ }
943
+ ),
944
+ showBuy ? /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: setOpen, children: [
945
+ /* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx6(
946
+ Button,
947
+ {
948
+ type: "button",
949
+ variant: "ghost",
950
+ size: "icon-sm",
951
+ "aria-label": "Add credits",
952
+ disabled: buying,
953
+ className: "size-5 rounded-full text-muted-foreground hover:text-foreground",
954
+ children: /* @__PURE__ */ jsx6(Plus, { className: "size-3.5", "aria-hidden": "true" })
955
+ }
956
+ ) }),
957
+ /* @__PURE__ */ jsxs5(PopoverContent, { align: "end", className: "w-48 p-1.5", children: [
958
+ /* @__PURE__ */ jsx6("p", { className: "px-2 py-1 text-[11px] font-medium text-muted-foreground", children: "Add credits" }),
959
+ packs.length > 0 ? /* @__PURE__ */ jsx6("div", { className: "flex flex-col", children: packs.map((p) => /* @__PURE__ */ jsxs5(
960
+ "button",
961
+ {
962
+ type: "button",
963
+ onClick: () => void pick(p.id),
964
+ disabled: buying,
965
+ className: "flex items-center justify-between gap-3 rounded-md px-2 py-1.5 text-[13px] hover:bg-muted disabled:opacity-50",
966
+ children: [
967
+ /* @__PURE__ */ jsx6("span", { className: "tabular-nums font-medium", children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
968
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: formatCreditMicros(p.creditMicros, p.currency, locale) })
969
+ ]
970
+ },
971
+ p.id
972
+ )) }) : /* @__PURE__ */ jsx6(
973
+ "button",
974
+ {
975
+ type: "button",
976
+ onClick: () => void pick(),
977
+ disabled: buying,
978
+ className: "w-full rounded-md px-2 py-1.5 text-left text-[13px] hover:bg-muted disabled:opacity-50",
979
+ children: buying ? "Opening\u2026" : "Buy credits"
980
+ }
981
+ )
982
+ ] })
983
+ ] }) : null
984
+ ] });
985
+ }
986
+
987
+ // src/app/front/credits/CreditsSettingsPanel.tsx
988
+ import { useState as useState7 } from "react";
989
+ import {
990
+ Button as Button2,
991
+ DetailList,
992
+ DetailLine,
993
+ Notice,
994
+ SettingsPanel
995
+ } from "@hachej/boring-ui-kit";
996
+ import { CreditCard } from "lucide-react";
997
+
998
+ // src/app/front/credits/useCreditHistory.ts
999
+ import { useCallback as useCallback2, useState as useState6 } from "react";
1000
+ function useCreditHistory(apiBaseUrl = "", limit = 20) {
1001
+ const [entries, setEntries] = useState6(null);
1002
+ const [loading, setLoading] = useState6(false);
1003
+ const [error, setError] = useState6(false);
1004
+ const load = useCallback2(async () => {
1005
+ setLoading(true);
1006
+ setError(false);
1007
+ try {
1008
+ const res = await fetch(`${apiBaseUrl}/api/credits/history?limit=${encodeURIComponent(String(limit))}`, { credentials: "include" });
1009
+ if (!res.ok) {
1010
+ setError(true);
1011
+ return;
1012
+ }
1013
+ const data = await res.json();
1014
+ setEntries(Array.isArray(data.entries) ? data.entries : []);
1015
+ } catch {
1016
+ setError(true);
1017
+ } finally {
1018
+ setLoading(false);
1019
+ }
1020
+ }, [apiBaseUrl, limit]);
1021
+ return { entries, loading, error, load };
1022
+ }
1023
+
1024
+ // src/app/front/credits/CreditsSettingsPanel.tsx
1025
+ import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1026
+ function relativeTime(iso, locale) {
1027
+ const then = new Date(iso).getTime();
1028
+ if (!Number.isFinite(then)) return "";
1029
+ return new Date(then).toLocaleString(locale, { dateStyle: "medium", timeStyle: "short" });
1030
+ }
1031
+ function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
1032
+ const { balance, hidden, error, buy, buying, lastUpdatedAt, updating } = useCreditBalance({ apiBaseUrl });
1033
+ const history = useCreditHistory(apiBaseUrl);
1034
+ const [buyError, setBuyError] = useState7(null);
1035
+ const [selectedPack, setSelectedPack] = useState7(null);
1036
+ if (hidden) return null;
1037
+ if (!balance) {
1038
+ return /* @__PURE__ */ jsx7(
1039
+ SettingsPanel,
1040
+ {
1041
+ id: "billing",
1042
+ icon: /* @__PURE__ */ jsx7(CreditCard, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1043
+ title: "Billing & credits",
1044
+ description: "Your remaining AI credits, how to top up, and recent activity.",
1045
+ children: error ? /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: error }) : /* @__PURE__ */ jsx7("p", { className: "text-[13px] text-muted-foreground", children: "Loading your balance\u2026" })
1046
+ }
1047
+ );
1048
+ }
1049
+ const inDebt = (balance.debtMicros ?? 0) > 0;
1050
+ const low = inDebt || isLowBalance(balance.remainingMicros);
1051
+ const showBuy = balance.checkoutEnabled ?? false;
1052
+ const packs = balance.packs ?? [];
1053
+ const currency = packs[0]?.currency ?? "EUR";
1054
+ const activePack = selectedPack ?? packs.find((p) => p.isDefault)?.id ?? packs[0]?.id ?? null;
1055
+ const activePackObj = packs.find((p) => p.id === activePack) ?? null;
1056
+ const doBuy = async (pack) => {
1057
+ setBuyError(null);
1058
+ const error2 = await buy(pack);
1059
+ if (error2) setBuyError(error2);
1060
+ };
1061
+ return /* @__PURE__ */ jsx7(
1062
+ SettingsPanel,
1063
+ {
1064
+ id: "billing",
1065
+ icon: /* @__PURE__ */ jsx7(CreditCard, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
1066
+ title: "Billing & credits",
1067
+ description: "Your remaining AI credits, how to top up, and recent activity.",
1068
+ footer: showBuy && packs.length === 0 ? /* @__PURE__ */ jsx7(Button2, { type: "button", size: "sm", onClick: () => void doBuy(), disabled: buying, children: buying ? "Opening checkout\u2026" : "Buy credits" }) : void 0,
1069
+ children: /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
1070
+ buyError && /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: buyError }),
1071
+ inDebt && /* @__PURE__ */ jsx7(Notice, { role: "status", tone: "error", description: "Your balance is negative. Top up to resume running the agent." }),
1072
+ !inDebt && low && /* @__PURE__ */ jsx7(Notice, { role: "status", tone: "warning", description: "You're low on credits. Top up to avoid interruptions." }),
1073
+ /* @__PURE__ */ jsxs6(DetailList, { children: [
1074
+ /* @__PURE__ */ jsx7(DetailLine, { label: "Remaining balance", children: /* @__PURE__ */ jsxs6("p", { style: { fontVariantNumeric: "tabular-nums", fontWeight: 600 }, children: [
1075
+ inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale),
1076
+ updating && /* @__PURE__ */ jsx7("span", { className: "ml-2 text-[11px] font-normal text-muted-foreground", "aria-live": "polite", children: "Updating\u2026" })
1077
+ ] }) }),
1078
+ /* @__PURE__ */ jsx7(DetailLine, { label: "Used so far", children: /* @__PURE__ */ jsx7("p", { style: { fontVariantNumeric: "tabular-nums" }, children: formatCreditMicros(balance.usedMicros, currency, locale) }) })
1079
+ ] }),
1080
+ showBuy && packs.length > 0 && /* @__PURE__ */ jsxs6("fieldset", { className: "space-y-2", children: [
1081
+ /* @__PURE__ */ jsx7("legend", { className: "text-[12px] font-medium text-foreground", children: "Buy credits" }),
1082
+ /* @__PURE__ */ jsx7("div", { role: "radiogroup", "aria-label": "Credit pack", className: "flex flex-wrap gap-2", children: packs.map((p) => {
1083
+ const selected = p.id === activePack;
1084
+ return /* @__PURE__ */ jsxs6(
1085
+ "label",
1086
+ {
1087
+ className: `flex cursor-pointer items-center gap-2 rounded-md border px-3 py-1.5 text-[13px] ${selected ? "border-foreground" : "border-border/60"}`,
1088
+ children: [
1089
+ /* @__PURE__ */ jsx7(
1090
+ "input",
1091
+ {
1092
+ type: "radio",
1093
+ name: "credit-pack",
1094
+ value: p.id,
1095
+ checked: selected,
1096
+ onChange: () => setSelectedPack(p.id)
1097
+ }
1098
+ ),
1099
+ p.custom ? /* @__PURE__ */ jsxs6("span", { children: [
1100
+ "Custom",
1101
+ /* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
1102
+ " \xB7 from ",
1103
+ formatMinorPrice(p.priceMinor, p.currency, locale)
1104
+ ] })
1105
+ ] }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
1106
+ /* @__PURE__ */ jsx7("span", { style: { fontVariantNumeric: "tabular-nums" }, children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
1107
+ /* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
1108
+ "\xB7 ",
1109
+ formatCreditMicros(p.creditMicros, p.currency, locale)
1110
+ ] })
1111
+ ] })
1112
+ ]
1113
+ },
1114
+ p.id
1115
+ );
1116
+ }) }),
1117
+ /* @__PURE__ */ jsx7(
1118
+ Button2,
1119
+ {
1120
+ type: "button",
1121
+ size: "sm",
1122
+ onClick: () => activePack && void doBuy(activePack),
1123
+ disabled: buying || !activePack,
1124
+ children: buying ? "Opening checkout\u2026" : activePackObj?.custom ? "Choose amount" : activePackObj ? `Buy ${formatMinorPrice(activePackObj.priceMinor, activePackObj.currency, locale)}` : "Buy credits"
1125
+ }
1126
+ )
1127
+ ] }),
1128
+ /* @__PURE__ */ jsx7("p", { className: "text-[12px] leading-5 text-muted-foreground", children: showBuy ? "Credits are consumed as the agent runs (priced from model token usage). Checkout opens in a new tab; your balance updates automatically when payment completes." : "Credits are consumed as the agent runs (priced from model token usage). Purchasing more credits is not available in this deployment yet." }),
1129
+ /* @__PURE__ */ jsxs6("details", { onToggle: (e) => {
1130
+ if (e.currentTarget.open && history.entries === null && !history.loading) void history.load();
1131
+ }, children: [
1132
+ /* @__PURE__ */ jsx7("summary", { className: "cursor-pointer text-[12px] font-medium text-foreground", children: "Recent activity" }),
1133
+ /* @__PURE__ */ jsxs6("div", { className: "mt-2", children: [
1134
+ history.loading && /* @__PURE__ */ jsx7("p", { className: "text-[12px] text-muted-foreground", "aria-live": "polite", children: "Loading\u2026" }),
1135
+ history.error && /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: "Could not load activity. Try again." }),
1136
+ !history.loading && !history.error && history.entries?.length === 0 && /* @__PURE__ */ jsx7("p", { className: "text-[12px] text-muted-foreground", children: "No credit activity yet." }),
1137
+ !history.loading && !history.error && history.entries && history.entries.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "divide-y divide-border/40", children: history.entries.map((e) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center justify-between gap-3 py-1.5 text-[12px]", children: [
1138
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0", children: [
1139
+ /* @__PURE__ */ jsx7("span", { className: "text-foreground", children: e.description }),
1140
+ /* @__PURE__ */ jsx7("span", { className: "ml-2 text-muted-foreground", children: relativeTime(e.createdAt, locale) })
1141
+ ] }),
1142
+ /* @__PURE__ */ jsx7(
1143
+ "span",
1144
+ {
1145
+ style: { fontVariantNumeric: "tabular-nums" },
1146
+ className: e.amountMicros >= 0 ? "text-foreground" : "text-muted-foreground",
1147
+ children: formatSignedCreditMicros(e.amountMicros, currency, locale)
1148
+ }
1149
+ )
1150
+ ] }, e.id)) })
1151
+ ] })
1152
+ ] }),
1153
+ lastUpdatedAt && /* @__PURE__ */ jsxs6("p", { className: "text-[11px] text-muted-foreground", children: [
1154
+ "Updated ",
1155
+ new Date(lastUpdatedAt).toLocaleTimeString(locale)
1156
+ ] })
1157
+ ] })
1158
+ }
1159
+ );
1160
+ }
1161
+
1162
+ // src/app/front/credits/useCheckoutReturnHandler.ts
1163
+ import { useEffect as useEffect5, useState as useState8 } from "react";
1164
+ var CONFIRM_SCHEDULE_MS = [0, 1500, 3e3, 6e3, 1e4, 15e3, 22e3, 3e4, 45e3, 6e4];
1165
+ async function fetchBalance(apiBaseUrl) {
1166
+ try {
1167
+ const res = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
1168
+ if (!res.ok) return null;
1169
+ return await res.json();
1170
+ } catch {
1171
+ return null;
1172
+ }
1173
+ }
1174
+ function clearStoredBaseline() {
1175
+ try {
1176
+ window.localStorage.removeItem(CHECKOUT_BASELINE_STORAGE_KEY);
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ function takeStoredBaseline(now) {
1181
+ try {
1182
+ const raw = window.localStorage.getItem(CHECKOUT_BASELINE_STORAGE_KEY);
1183
+ if (raw === null) return null;
1184
+ window.localStorage.removeItem(CHECKOUT_BASELINE_STORAGE_KEY);
1185
+ const parsed = JSON.parse(raw);
1186
+ const net = Number(parsed?.net);
1187
+ const ts = Number(parsed?.ts);
1188
+ if (!Number.isFinite(net) || !Number.isFinite(ts)) return null;
1189
+ if (now - ts > CHECKOUT_BASELINE_TTL_MS) return null;
1190
+ return { net, userId: typeof parsed?.userId === "string" ? parsed.userId : void 0 };
1191
+ } catch {
1192
+ return null;
1193
+ }
1194
+ }
1195
+ function useCheckoutReturnHandler({ apiBaseUrl = "", param = "checkout" } = {}) {
1196
+ const [status, setStatus] = useState8("idle");
1197
+ useEffect5(() => {
1198
+ if (typeof window === "undefined") return;
1199
+ const url = new URL(window.location.href);
1200
+ const marker = url.searchParams.get(param);
1201
+ if (!marker) return;
1202
+ url.searchParams.delete(param);
1203
+ window.history.replaceState(window.history.state, "", url.toString());
1204
+ if (marker === "cancelled") {
1205
+ clearStoredBaseline();
1206
+ setStatus("cancelled");
1207
+ return;
1208
+ }
1209
+ let cancelled = false;
1210
+ const timers = [];
1211
+ let channel = null;
1212
+ setStatus("checking");
1213
+ const stored = takeStoredBaseline(Date.now());
1214
+ let baseline = null;
1215
+ void (async () => {
1216
+ window.dispatchEvent(new Event(CREDITS_REFRESH_EVENT));
1217
+ try {
1218
+ channel = new BroadcastChannel("credits");
1219
+ channel.postMessage("refresh");
1220
+ } catch {
1221
+ }
1222
+ for (const delay of CONFIRM_SCHEDULE_MS) {
1223
+ timers.push(setTimeout(async () => {
1224
+ if (cancelled) return;
1225
+ const bal = await fetchBalance(apiBaseUrl);
1226
+ if (cancelled || !bal) return;
1227
+ if (baseline === null) {
1228
+ baseline = stored && stored.userId === bal.userId ? stored.net : creditNetMicros(bal);
1229
+ }
1230
+ if (creditNetMicros(bal) > baseline) {
1231
+ setStatus("confirmed");
1232
+ cancelled = true;
1233
+ window.dispatchEvent(new Event(CREDITS_REFRESH_EVENT));
1234
+ try {
1235
+ channel?.postMessage("refresh");
1236
+ } catch {
1237
+ }
1238
+ }
1239
+ }, delay));
1240
+ }
1241
+ timers.push(setTimeout(() => {
1242
+ if (!cancelled) setStatus((s) => s === "checking" ? "processing" : s);
1243
+ }, CONFIRM_SCHEDULE_MS[CONFIRM_SCHEDULE_MS.length - 1] + 2e3));
1244
+ })();
1245
+ return () => {
1246
+ cancelled = true;
1247
+ for (const t of timers) clearTimeout(t);
1248
+ channel?.close();
1249
+ };
1250
+ }, [apiBaseUrl, param]);
1251
+ return { status, dismiss: () => setStatus("idle") };
1252
+ }
1253
+
1254
+ // src/app/front/credits/CheckoutReturnBanner.tsx
1255
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1256
+ var COPY = {
1257
+ checking: { tone: "info", text: "Checking your payment\u2026" },
1258
+ confirmed: { tone: "success", text: "Credits added \u2014 thank you!" },
1259
+ // Reached on timeout WITHOUT a confirmed balance increase: don't claim the payment
1260
+ // was received (we can't confirm that from the client), just that it's still pending.
1261
+ processing: { tone: "info", text: "Your purchase is still being confirmed \u2014 credits usually appear within a minute. Refresh if it doesn\u2019t update." },
1262
+ cancelled: { tone: "warning", text: "Checkout cancelled \u2014 no charge was made." }
1263
+ };
1264
+ function CheckoutReturnBanner({ apiBaseUrl = "", param }) {
1265
+ const { status, dismiss } = useCheckoutReturnHandler({ apiBaseUrl, ...param ? { param } : {} });
1266
+ if (status === "idle") return null;
1267
+ const copy = COPY[status];
1268
+ if (!copy) return null;
1269
+ return /* @__PURE__ */ jsxs7(
1270
+ "div",
1271
+ {
1272
+ role: "status",
1273
+ "aria-live": "polite",
1274
+ "data-tone": copy.tone,
1275
+ style: {
1276
+ position: "fixed",
1277
+ bottom: 16,
1278
+ right: 16,
1279
+ zIndex: 1e3,
1280
+ maxWidth: 360,
1281
+ display: "flex",
1282
+ alignItems: "center",
1283
+ gap: 12,
1284
+ padding: "10px 12px",
1285
+ borderRadius: 8,
1286
+ fontSize: 13,
1287
+ background: "var(--color-surface, #fff)",
1288
+ color: "var(--color-foreground, inherit)",
1289
+ border: "1px solid var(--color-border, rgba(0,0,0,0.12))",
1290
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12)"
1291
+ },
1292
+ children: [
1293
+ /* @__PURE__ */ jsx8("span", { children: copy.text }),
1294
+ /* @__PURE__ */ jsx8(
1295
+ "button",
1296
+ {
1297
+ type: "button",
1298
+ onClick: dismiss,
1299
+ "aria-label": "Dismiss",
1300
+ style: { marginLeft: "auto", background: "none", border: "none", cursor: "pointer", fontSize: 16, lineHeight: 1, color: "inherit" },
1301
+ children: "\xD7"
1302
+ }
1303
+ )
1304
+ ]
1305
+ }
1306
+ );
1307
+ }
1308
+
1309
+ // src/app/front/credits/BuyCreditsNoticeAction.tsx
1310
+ import { useState as useState9 } from "react";
1311
+ import { Button as Button3 } from "@hachej/boring-ui-kit";
1312
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1313
+ function BuyCreditsNoticeAction({ apiBaseUrl = "", label = "Buy credits" }) {
1314
+ const { buy, buying, balance, hidden } = useCreditBalance({ apiBaseUrl, pollMs: 6e4 });
1315
+ const [error, setError] = useState9(null);
1316
+ if (hidden || !balance || balance.checkoutEnabled === false) return null;
1317
+ const onBuy = async () => {
1318
+ setError(null);
1319
+ const message = await buy();
1320
+ if (message) setError(message);
1321
+ };
1322
+ return /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
1323
+ /* @__PURE__ */ jsx9(
1324
+ Button3,
1325
+ {
1326
+ type: "button",
1327
+ size: "sm",
1328
+ onClick: () => void onBuy(),
1329
+ disabled: buying,
1330
+ className: "bg-foreground text-background hover:bg-foreground/90",
1331
+ children: buying ? "Opening\u2026" : label
1332
+ }
1333
+ ),
1334
+ error ? /* @__PURE__ */ jsx9("span", { role: "alert", className: "text-xs text-destructive", children: error }) : null
1335
+ ] });
1336
+ }
561
1337
  export {
562
- CoreWorkspaceAgentFront
1338
+ BuyCreditsNoticeAction,
1339
+ CREDITS_REFRESH_EVENT,
1340
+ CheckoutReturnBanner,
1341
+ CoreWorkspaceAgentFront,
1342
+ CreditBalanceBadge,
1343
+ CreditsSettingsPanel,
1344
+ DefaultTopBarRight,
1345
+ PAYMENT_REQUIRED_ERROR_CODE,
1346
+ formatCreditMicros,
1347
+ formatMinorPrice,
1348
+ formatSignedCreditMicros,
1349
+ isLowBalance,
1350
+ isPaymentRequiredNotice,
1351
+ useCheckoutReturnHandler,
1352
+ useCreditBalance,
1353
+ useCreditHistory
563
1354
  };