@hachej/boring-core 0.1.41 → 0.1.43

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