frizz 0.13.0 → 0.13.2

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.
Files changed (2) hide show
  1. package/dist/frizz.js +225 -55
  2. package/package.json +3 -3
package/dist/frizz.js CHANGED
@@ -180,10 +180,21 @@ var init_local_origin = __esm({
180
180
  }
181
181
  });
182
182
 
183
+ // packages/server/src/local-path.ts
184
+ function normalizeLocalPath(path, platform = process.platform) {
185
+ return platform === "win32" ? path.replace(/^\/([a-zA-Z]:[\\/])/, "$1") : path;
186
+ }
187
+ var init_local_path = __esm({
188
+ "packages/server/src/local-path.ts"() {
189
+ "use strict";
190
+ }
191
+ });
192
+
183
193
  // packages/server/src/local-image.ts
184
194
  import { readFileSync as readFileSync2, realpathSync, statSync } from "node:fs";
185
195
  import { extname, isAbsolute } from "node:path";
186
196
  function resolveLocalImage(rawPath) {
197
+ if (rawPath) rawPath = normalizeLocalPath(rawPath);
187
198
  if (!rawPath || !isAbsolute(rawPath)) return { status: 400 };
188
199
  const contentType = IMAGE_CONTENT_TYPE[extname(rawPath).toLowerCase()];
189
200
  if (!contentType) return { status: 400 };
@@ -204,6 +215,7 @@ var IMAGE_CONTENT_TYPE;
204
215
  var init_local_image = __esm({
205
216
  "packages/server/src/local-image.ts"() {
206
217
  "use strict";
218
+ init_local_path();
207
219
  IMAGE_CONTENT_TYPE = {
208
220
  ".png": "image/png",
209
221
  ".jpg": "image/jpeg",
@@ -11775,7 +11787,7 @@ var init_interactions = __esm({
11775
11787
  InteractionFieldId = external_exports.string().min(1).max(128).regex(/^[A-Za-z_][A-Za-z0-9_.:-]*$/).refine((value) => !["__proto__", "constructor", "prototype"].includes(value), "reserved field id");
11776
11788
  InteractionDecisionId = external_exports.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
11777
11789
  InteractionProvider = external_exports.object({
11778
- kind: external_exports.enum(["claude", "codex", "frizz"]),
11790
+ kind: external_exports.enum(["claude", "codex", "acp", "frizz"]),
11779
11791
  name: InteractionLabel.optional(),
11780
11792
  version: InteractionOpaqueId.optional()
11781
11793
  }).strict();
@@ -12322,6 +12334,13 @@ var init_claim = __esm({
12322
12334
  }
12323
12335
  });
12324
12336
 
12337
+ // packages/shared/src/claude-editions.ts
12338
+ var init_claude_editions = __esm({
12339
+ "packages/shared/src/claude-editions.ts"() {
12340
+ "use strict";
12341
+ }
12342
+ });
12343
+
12325
12344
  // packages/shared/src/code-fences.ts
12326
12345
  var init_code_fences = __esm({
12327
12346
  "packages/shared/src/code-fences.ts"() {
@@ -12466,10 +12485,15 @@ var init_shell_writes = __esm({
12466
12485
  });
12467
12486
 
12468
12487
  // packages/shared/src/index.ts
12488
+ function requireEffortOutsideAcp(profile, ctx) {
12489
+ if (profile.backend !== "acp" && profile.effort === void 0) {
12490
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
12491
+ }
12492
+ }
12469
12493
  function fallbackPort(base) {
12470
12494
  return base + 1e4;
12471
12495
  }
12472
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
12496
+ var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
12473
12497
  var init_src = __esm({
12474
12498
  "packages/shared/src/index.ts"() {
12475
12499
  "use strict";
@@ -12478,6 +12502,7 @@ var init_src = __esm({
12478
12502
  init_interactions();
12479
12503
  init_thread_slug();
12480
12504
  init_claim();
12505
+ init_claude_editions();
12481
12506
  init_code_fences();
12482
12507
  init_delta();
12483
12508
  init_drainable_worker();
@@ -12588,7 +12613,7 @@ var init_src = __esm({
12588
12613
  "exited"
12589
12614
  // the worker process that owned this session is gone, or is no longer driving its turn
12590
12615
  ]);
12591
- Backend = external_exports.enum(["claude", "codex"]);
12616
+ Backend = external_exports.enum(["claude", "codex", "acp"]);
12592
12617
  CodexModel = external_exports.object({
12593
12618
  slug: external_exports.string(),
12594
12619
  displayName: external_exports.string(),
@@ -12602,9 +12627,36 @@ var init_src = __esm({
12602
12627
  contextWindow: external_exports.number().int().positive().optional(),
12603
12628
  maxContextWindow: external_exports.number().int().positive().optional()
12604
12629
  });
12630
+ ClaudeModel = external_exports.object({
12631
+ alias: external_exports.string(),
12632
+ label: external_exports.string(),
12633
+ resolvedModel: external_exports.string().optional(),
12634
+ // The edition alone ("5.5") — the tail of `label`, which the picker sets dimmer than the family word.
12635
+ // Absent exactly when `resolvedModel` is.
12636
+ edition: external_exports.string().optional()
12637
+ });
12638
+ AcpAgent = external_exports.object({
12639
+ id: external_exports.string(),
12640
+ label: external_exports.string(),
12641
+ command: external_exports.string(),
12642
+ available: external_exports.boolean()
12643
+ });
12644
+ AcpAgentModel = external_exports.object({ id: external_exports.string(), name: external_exports.string() });
12645
+ AcpAgentModels = external_exports.object({
12646
+ agentId: external_exports.string(),
12647
+ models: external_exports.array(AcpAgentModel),
12648
+ /** The model the agent opens a session on when Frizz asks for none. */
12649
+ current: external_exports.string().optional(),
12650
+ /** Why the list is empty, when the probe could not open a session (not installed, not logged in…). */
12651
+ error: external_exports.string().optional(),
12652
+ probedAt: external_exports.string()
12653
+ });
12654
+ AcpAgentModelsInput = external_exports.object({ agentId: external_exports.string().min(1).max(100), refresh: external_exports.boolean().optional() }).strict();
12605
12655
  ThreadProfileOption = external_exports.object({
12606
12656
  model: external_exports.string().min(1),
12607
12657
  label: external_exports.string().min(1),
12658
+ // The edition at the end of `label` ("5.5" of "Opus 5.5"), for a Claude row the pinned runtime resolved.
12659
+ edition: external_exports.string().optional(),
12608
12660
  defaultEffort: external_exports.string().min(1),
12609
12661
  efforts: external_exports.array(external_exports.string().min(1)).min(1)
12610
12662
  });
@@ -12779,7 +12831,7 @@ var init_src = __esm({
12779
12831
  questions: external_exports.array(AskQuestion)
12780
12832
  });
12781
12833
  AwaitingHint = external_exports.object({
12782
- kind: external_exports.enum(["shell", "agent", "timer", "pr", "for", "title"]),
12834
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title"]),
12783
12835
  value: external_exports.string()
12784
12836
  });
12785
12837
  RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
@@ -12929,6 +12981,20 @@ var init_src = __esm({
12929
12981
  * must hear about. Empty when nothing is failing. */
12930
12982
  failureSig: external_exports.string().optional()
12931
12983
  }).strict();
12984
+ GithubIssueStatus = external_exports.object({
12985
+ /** OPEN | CLOSED, lowercased. A closed issue ends the wait outright. */
12986
+ state: external_exports.enum(["open", "closed"]),
12987
+ /** GitHub's own reason for a close — `completed`, `not_planned`, `duplicate` — lowercased, when it
12988
+ * gave one. Absent while open. */
12989
+ stateReason: external_exports.string().optional(),
12990
+ /** The issue's title, as of the last poll — the one fact a human needs to place a bare `owner/repo#N`
12991
+ * on the card. Capped, because a title is a headline and the card is not the place to read one. */
12992
+ title: external_exports.string().max(200).optional(),
12993
+ /** The conversation's size, so the card can say "14 comments" the way GitHub's list does. */
12994
+ comments: external_exports.number().int().nonnegative(),
12995
+ /** When frizz last heard from GitHub, for the same reason `GithubWatchStatus.polledAt` carries it. */
12996
+ polledAt: external_exports.string()
12997
+ }).strict();
12932
12998
  ThreadWatchView = external_exports.object({
12933
12999
  id: external_exports.string(),
12934
13000
  kind: ThreadWatchKind,
@@ -12937,6 +13003,15 @@ var init_src = __esm({
12937
13003
  createdAt: external_exports.string(),
12938
13004
  /** `github` rows only, and absent until the first successful poll. */
12939
13005
  github: GithubWatchStatus.optional(),
13006
+ /** `github` rows only: WHAT the row watches. A pull request and an issue share the kind — they are one
13007
+ * registry, one ref grammar (`owner/repo#N`) and one place on every surface that lists waits — and
13008
+ * differ in what the poll can read off them, which is what this field lets a card branch on.
13009
+ * Optional, and absent means a pull request — a row from a server that predates issues reads as the
13010
+ * PR it always was, and the non-github kinds never carry it. */
13011
+ subject: external_exports.enum(["pull", "issue"]).optional(),
13012
+ /** `github` rows watching an ISSUE only, and absent until the first successful poll — the twin of the
13013
+ * `github` half above, for the rows whose subject has no checks to report. */
13014
+ issue: GithubIssueStatus.optional(),
12940
13015
  /** `timer` rows only: the armed timer's own registration, which is everything the row renders — the
12941
13016
  * worker's prompt is the row's NAME (a `tmr_…` id names nothing to a human) and the fire instant is
12942
13017
  * its status. Unlike `github` there is no polled half to be absent: a timer that exists is fully
@@ -12949,10 +13024,15 @@ var init_src = __esm({
12949
13024
  id: external_exports.string(),
12950
13025
  /** `owner/repo#N`, normalized — the same string the board's row and the status book are keyed by. */
12951
13026
  target: external_exports.string(),
13027
+ /** A pull request or an issue. Both live in one registry (`pr_watch`) and answer to one drop verb;
13028
+ * `watch_pr` and `watch_issue` each list only their own. Defaulted for a row an older server sends. */
13029
+ kind: external_exports.enum(["pull", "issue"]).default("pull"),
12952
13030
  state: external_exports.enum(["armed", "dropped", "settled"]),
12953
13031
  createdAt: external_exports.string(),
12954
13032
  /** The PR's checks/mergeability as the poller last saw it. Absent until the first successful poll. */
12955
- github: GithubWatchStatus.optional()
13033
+ github: GithubWatchStatus.optional(),
13034
+ /** An ISSUE's state as the poller last saw it — `kind: "issue"` rows only, absent until polled. */
13035
+ issue: GithubIssueStatus.optional()
12956
13036
  }).strict();
12957
13037
  AddOwnPrWatchInput = external_exports.object({
12958
13038
  slug: ThreadSlug,
@@ -12977,7 +13057,13 @@ var init_src = __esm({
12977
13057
  * already running, with no recourse from inside those threads. Absent ⇒ PR_WATCH_DEFAULT_FOR, which
12978
13058
  * still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
12979
13059
  * is better bounded than broken. */
12980
- for: external_exports.string().trim().min(1).max(16).optional()
13060
+ for: external_exports.string().trim().min(1).max(16).optional(),
13061
+ /** What `target` names. `issue` (from `mcp__frizz__watch_issue`, 2026-09-14) registers a watcher on a
13062
+ * GitHub issue — same registry, same poll cadence, same drop verb, but a different probe (`gh issue
13063
+ * view`), a different GraphQL fragment and a report with no CI in it. ABSENT MEANS A PULL REQUEST,
13064
+ * for the same reason `for` is optional on the wire: every `watch_pr` binary dispatched before this
13065
+ * field existed sends nothing here and must go on meaning what it always did. */
13066
+ kind: external_exports.enum(["pull", "issue"]).optional()
12981
13067
  }).strict();
12982
13068
  AddOwnPrWatchResult = external_exports.object({
12983
13069
  id: external_exports.string(),
@@ -13443,6 +13529,16 @@ var init_src = __esm({
13443
13529
  // values forward-compatible; absent when neither durable source knows → the UI renders no guess.
13444
13530
  model: external_exports.string().optional(),
13445
13531
  effort: external_exports.string().optional(),
13532
+ // The Claude EDITION this thread's worker last ran ("Opus 5"), read off the model id on its own
13533
+ // transcript. It can be older than the edition the picker's row names ("Opus 5.5"): a worker keeps the
13534
+ // runtime it was started on, and a new pin only reaches the workers started after it. Emitted only when
13535
+ // that id belongs to the family `model` names, so a pending model switch never shows the old family's.
13536
+ runningModelLabel: external_exports.string().optional(),
13537
+ // Present when the pinned runtime now resolves this thread's family to a NEWER edition than it runs.
13538
+ // `staged`: no live worker holds the old edition, so the next turn starts on `label` with nothing
13539
+ // further to do; otherwise the composer offers the upgrade, and the first message after the thread's
13540
+ // next compaction takes it on its own.
13541
+ modelUpgrade: external_exports.object({ label: external_exports.string(), staged: external_exports.boolean() }).optional(),
13446
13542
  // How full the session's context window is right now — the footer's fullness readout. BOTH halves
13447
13543
  // are provider-measured and the field is emitted ONLY when both are present, so a client never has
13448
13544
  // to decide what to do with half a fraction: absent ⇒ no reading, never a 0% dial. Codex reports
@@ -13525,14 +13621,15 @@ var init_src = __esm({
13525
13621
  codex: ProviderAuth,
13526
13622
  emails: AccountEmails
13527
13623
  });
13528
- AccountLogoutInput = external_exports.object({ backend: external_exports.enum(["claude", "codex"]) }).strict();
13624
+ AccountBackend = external_exports.enum(["claude", "codex"]);
13625
+ AccountLogoutInput = external_exports.object({ backend: AccountBackend }).strict();
13529
13626
  AccountLogoutResult = external_exports.object({
13530
13627
  status: external_exports.enum(["done", "blocked", "failed"]),
13531
13628
  auth: ProviderAuth,
13532
13629
  activeThreads: external_exports.number().int().positive().optional(),
13533
13630
  detail: external_exports.string().max(200).optional()
13534
13631
  });
13535
- AccountLoginStartInput = external_exports.object({ backend: external_exports.enum(["claude", "codex"]) }).strict();
13632
+ AccountLoginStartInput = external_exports.object({ backend: AccountBackend }).strict();
13536
13633
  AccountLoginStartResult = external_exports.object({ attemptId: ThreadSlug });
13537
13634
  AccountLoginStatusInput = external_exports.object({ attemptId: ThreadSlug }).strict();
13538
13635
  AccountLoginStatusResult = external_exports.object({
@@ -13574,13 +13671,13 @@ var init_src = __esm({
13574
13671
  * to, not furniture you sit beside. Hidden, the way back is a breadcrumb in the status bar, which
13575
13672
  * costs a click exactly when you meant to switch and nothing when you did not.
13576
13673
  *
13577
- * Machine-level, like the font: which chrome you want is a property of the person, not the repo.
13674
+ * Machine-level: which chrome you want is a property of the person, not the repo.
13578
13675
  */
13579
13676
  projectRail: external_exports.boolean(),
13580
- // UI type family. `mono` (default) is the mono-forward system; `sans` swaps prose/UI chrome to a
13581
- // sans stack while code / tool lines / the terminal stay mono. Optional so an old settings blob
13582
- // parses; defaultSettings pins "mono".
13583
- font: external_exports.enum(["mono", "sans"]).optional(),
13677
+ // There is no `font` key any more. The interface rendered in one of two type families as a machine
13678
+ // setting until 2026-09-19 (maintainer: "let's drop monospace as an option"); every surface is sans
13679
+ // now, and index.html pins `data-font="sans"` on <html> directly. Settings is a non-strict object,
13680
+ // so a stored `font` is stripped the moment an old blob parses — no migration.
13584
13681
  // Default action for a vetted non-image local path in agent markdown. Image clicks always use the
13585
13682
  // OS default viewer so screenshots retain their expected behavior.
13586
13683
  localFileOpener: LocalFileOpener.optional(),
@@ -13607,6 +13704,15 @@ var init_src = __esm({
13607
13704
  // it (codex compacts at 90% of the resolved window). Unset ⇒ nothing is sent and the model's stock
13608
13705
  // window applies. Optional so an old blob parses; defaultSettings leaves it unset.
13609
13706
  codexContextWindow: external_exports.number().int().positive().optional(),
13707
+ // The operator's OWN Agent Client Protocol agents, merged over the built-in catalogue by id
13708
+ // (server/backend/acp-agents.ts). `command` is an executable name on PATH or an absolute path;
13709
+ // `args` is what puts it into ACP mode (`["acp"]`, `["--acp"]`). Optional so an old blob parses.
13710
+ acpAgents: external_exports.array(external_exports.object({
13711
+ id: external_exports.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9._-]*$/i),
13712
+ label: external_exports.string().trim().min(1).max(80),
13713
+ command: external_exports.string().trim().min(1).max(1024),
13714
+ args: external_exports.array(external_exports.string().max(1024)).max(32).optional()
13715
+ })).max(32).optional(),
13610
13716
  // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
13611
13717
  // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
13612
13718
  // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
@@ -13633,28 +13739,38 @@ var init_src = __esm({
13633
13739
  DispatchPreferences = external_exports.object({
13634
13740
  backend: Backend,
13635
13741
  claude: DispatchProviderPreferences,
13636
- codex: DispatchProviderPreferences
13742
+ codex: DispatchProviderPreferences,
13743
+ // Optional because every record written before the ACP backend existed lacks it, and a required
13744
+ // key would fail those records' parse and silently reset the operator's saved profile.
13745
+ acp: DispatchProviderPreferences.optional()
13637
13746
  });
13638
- DispatchProfileSnapshot = external_exports.object({
13747
+ DispatchProfileSnapshotShape = external_exports.object({
13639
13748
  backend: Backend,
13640
13749
  model: external_exports.string().trim().min(1).max(200),
13641
- effort: Settings.shape.effort.unwrap(),
13750
+ // Required for Claude and Codex (the refinement below); absent for an ACP agent, which runs with
13751
+ // whatever model and effort its own CLI is configured for — Frizz has no effort axis to offer there.
13752
+ effort: Settings.shape.effort,
13642
13753
  // IGNORED: dispatch permission is decided server-side (workerDispatchPermission) from the
13643
13754
  // non-interactive floor plus the operator's Settings choice, never per dispatch. Optional so old
13644
13755
  // clients that still send it parse.
13645
13756
  permissionMode: PermissionMode.optional()
13646
13757
  }).strict();
13758
+ DispatchProfileSnapshot = DispatchProfileSnapshotShape.superRefine(requireEffortOutsideAcp);
13647
13759
  SetDispatchPreferenceInput = external_exports.discriminatedUnion("field", [
13648
13760
  external_exports.object({ field: external_exports.literal("backend"), value: Backend }),
13649
13761
  external_exports.object({
13650
13762
  field: external_exports.literal("profile"),
13651
13763
  backend: Backend,
13652
13764
  model: external_exports.string().trim().min(1).max(200),
13653
- effort: Settings.shape.effort.unwrap()
13765
+ // Optional ONLY for an ACP profile (see DispatchProfileSnapshot); a Claude/Codex profile without
13766
+ // an effort is refused below rather than silently stored as "default".
13767
+ effort: Settings.shape.effort
13654
13768
  }),
13655
13769
  external_exports.object({ field: external_exports.literal("model"), backend: Backend, value: external_exports.string().trim().min(1).max(200) }),
13656
13770
  external_exports.object({ field: external_exports.literal("effort"), backend: Backend, value: Settings.shape.effort.unwrap() })
13657
- ]);
13771
+ ]).superRefine((update, ctx) => {
13772
+ if (update.field === "profile") requireEffortOutsideAcp(update, ctx);
13773
+ });
13658
13774
  DispatchInput = external_exports.object({
13659
13775
  // Optional: when omitted, dispatch derives a fallback title from the prompt (Claude later renames
13660
13776
  // the session via ai-title, which the UI prefers for display). The thread FILE always gets a
@@ -13702,8 +13818,9 @@ var init_src = __esm({
13702
13818
  freshProcess: external_exports.boolean().optional(),
13703
13819
  // PREEMPT the operation the worker is running right now, so this message is read at once instead of
13704
13820
  // when that operation finishes. The operator's "Interrupt and send" verb, and opt-in for the same
13705
- // reason `freshProcess` is: it costs the in-flight tool call's result and the worker's in-memory
13706
- // sub-agents.
13821
+ // reason `freshProcess` is: it costs the in-flight tool call's result. (It no longer costs the
13822
+ // worker's sub-agents: since 2026-09-24 the broker's SDK query declares `perTaskStopAffordance`,
13823
+ // so the interrupt aborts only the turn and background agents run on.)
13707
13824
  //
13708
13825
  // It exists because delivery is ALREADY as fast as queueing can be. Measured over 14 days of this
13709
13826
  // project's own transcripts, Claude Code drains its queue at the first sampling boundary that
@@ -13806,7 +13923,7 @@ var init_src = __esm({
13806
13923
  timers: external_exports.array(ThreadTimerView)
13807
13924
  }).strict();
13808
13925
  ThreadActivityItem = external_exports.object({
13809
- kind: external_exports.enum(["shell", "agent", "timer", "pr"]),
13926
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue"]),
13810
13927
  /** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
13811
13928
  * was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
13812
13929
  id: external_exports.string(),
@@ -13910,11 +14027,15 @@ var init_src = __esm({
13910
14027
  SetThreadProfileInput = external_exports.object({
13911
14028
  slug: ThreadSlug,
13912
14029
  model: external_exports.string().trim().min(1).max(200),
13913
- effort: external_exports.string().trim().min(1).max(100)
14030
+ // Absent ONLY for an ACP thread, whose model slug (`acp:<agent>@<model>`) carries no effort axis; the
14031
+ // router refuses a Claude/Codex profile without one rather than storing "".
14032
+ effort: external_exports.string().trim().min(1).max(100).optional()
13914
14033
  }).strict();
13915
14034
  SetThreadProfileResult = external_exports.object({
13916
14035
  effect: external_exports.enum(["applied", "next-resume"])
13917
14036
  });
14037
+ UpgradeThreadModelInput = external_exports.object({ slug: ThreadSlug, sessionId: external_exports.string().min(1) }).strict();
14038
+ UpgradeThreadModelResult = external_exports.object({ effect: external_exports.literal("next-turn"), label: external_exports.string() });
13918
14039
  DISPATCH_TASK_BANNER = [
13919
14040
  "===============================================================",
13920
14041
  "====================== YOUR TASK ========================",
@@ -14044,9 +14165,9 @@ ${DISPATCH_TASK_BANNER}
14044
14165
  page: external_exports.number().int().positive(),
14045
14166
  pageCount: external_exports.number().int().positive()
14046
14167
  });
14047
- GithubBatchInput = DispatchProfileSnapshot.extend({
14168
+ GithubBatchInput = DispatchProfileSnapshotShape.extend({
14048
14169
  items: external_exports.array(external_exports.object({ kind: external_exports.enum(["issue", "pr"]), number: external_exports.number().int().positive() })).min(1)
14049
- }).strict();
14170
+ }).strict().superRefine(requireEffortOutsideAcp);
14050
14171
  GithubBatchResult = external_exports.object({
14051
14172
  dispatched: external_exports.array(external_exports.object({ number: external_exports.number(), kind: external_exports.string(), slug: ThreadSlug })),
14052
14173
  failed: external_exports.array(external_exports.object({ number: external_exports.number(), kind: external_exports.string(), error: external_exports.string() }))
@@ -14490,6 +14611,10 @@ ${DISPATCH_TASK_BANNER}
14490
14611
  /** An operator's uploaded icon, rather than one the scan found. Drives what the menu offers. */
14491
14612
  iconIsCustom: external_exports.boolean().optional()
14492
14613
  });
14614
+ ProjectRailCounts = external_exports.object({
14615
+ queued: external_exports.number().int().nonnegative(),
14616
+ running: external_exports.number().int().nonnegative()
14617
+ });
14493
14618
  PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
14494
14619
  DirectoryPickResult = external_exports.discriminatedUnion("kind", [
14495
14620
  external_exports.object({ kind: external_exports.literal("picked"), project: ProjectCard }),
@@ -19486,11 +19611,13 @@ function readBootProgress(stateDir) {
19486
19611
  return null;
19487
19612
  }
19488
19613
  }
19489
- var BOOT_PROGRESS_NAME;
19614
+ var BOOT_PROGRESS_NAME, BOOT_STALL_TIMEOUT_MS, BOOT_HARD_TIMEOUT_MS;
19490
19615
  var init_boot_progress = __esm({
19491
19616
  "packages/server/src/boot-progress.ts"() {
19492
19617
  "use strict";
19493
19618
  BOOT_PROGRESS_NAME = "boot.progress";
19619
+ BOOT_STALL_TIMEOUT_MS = 3e4;
19620
+ BOOT_HARD_TIMEOUT_MS = 10 * 6e4;
19494
19621
  }
19495
19622
  });
19496
19623
 
@@ -22146,12 +22273,12 @@ var qrcode_default = qrcode;
22146
22273
  var stringToBytes = qrcode.stringToBytes;
22147
22274
 
22148
22275
  // packages/server/src/qr.ts
22149
- var LIGHT = "\x1B[38;5;15m";
22150
22276
  var LIGHT_BG = "\x1B[48;5;15m";
22151
22277
  var DARK = "\x1B[38;5;0m";
22152
22278
  var DARK_BG = "\x1B[48;5;0m";
22153
22279
  var RESET = "\x1B[0m";
22154
22280
  var UPPER_HALF = "\u2580";
22281
+ var LOWER_HALF = "\u2584";
22155
22282
  var QUIET_ZONE = 4;
22156
22283
  function encode(value, errorCorrection) {
22157
22284
  const code = qrcode_default(0, errorCorrection);
@@ -22182,7 +22309,8 @@ function renderQrLines(value, options2 = {}) {
22182
22309
  line += top && bottom ? "#" : top ? "^" : bottom ? "v" : " ";
22183
22310
  continue;
22184
22311
  }
22185
- line += `${top ? DARK : LIGHT}${bottom ? DARK_BG : LIGHT_BG}${UPPER_HALF}`;
22312
+ if (top === bottom) line += `${top ? DARK_BG : LIGHT_BG} `;
22313
+ else line += `${DARK}${LIGHT_BG}${top ? UPPER_HALF : LOWER_HALF}`;
22186
22314
  }
22187
22315
  lines.push(options2.plain ? line : `${line}${RESET}`);
22188
22316
  }
@@ -22207,12 +22335,29 @@ var BASE_STYLE = `
22207
22335
  :root{
22208
22336
  color-scheme:dark;
22209
22337
  --bg:#0d0e10;--panel:#131519;--panel-2:#181b20;
22210
- --border:#26282d;--border-strong:#33363c;
22338
+ --border:#26282d;--border-strong:#33363c;--control-border:#26282d;--control-strong:#33363c;
22211
22339
  --fg:#e6e7e9;--muted:#8b8f96;--accent:#e8b923;--danger:#fca5a5;
22340
+ --focus-border:color-mix(in srgb,var(--accent) 55%,transparent);--focus-ring:color-mix(in srgb,var(--accent) 22%,transparent);
22212
22341
  --sans:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
22213
22342
  --mono:ui-monospace,"JetBrains Mono","SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
22214
22343
  --ease:cubic-bezier(0,0,.2,1);
22215
22344
  }
22345
+ :root[data-theme=light]{
22346
+ color-scheme:light;
22347
+ --bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
22348
+ --border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
22349
+ --fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
22350
+ --focus-border:#416896;--focus-ring:#416896;
22351
+ }
22352
+ @media(prefers-color-scheme:light){
22353
+ :root:not([data-theme]){
22354
+ color-scheme:light;
22355
+ --bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
22356
+ --border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
22357
+ --fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
22358
+ --focus-border:#416896;--focus-ring:#416896;
22359
+ }
22360
+ }
22216
22361
  html,body{background:var(--bg)}
22217
22362
  body{
22218
22363
  margin:0;min-height:100vh;min-height:100dvh;
@@ -22220,7 +22365,6 @@ body{
22220
22365
  color:var(--fg);font-family:var(--sans);font-size:12.5px;line-height:1.55;
22221
22366
  -webkit-font-smoothing:antialiased;
22222
22367
  }
22223
- html[data-font=mono] body{font-family:var(--mono)}
22224
22368
  main{width:100%;max-width:28rem;padding:18px;border:1px solid var(--border-strong);border-radius:12px;background:var(--panel-2)}
22225
22369
  h1{margin:0;display:flex;align-items:baseline;gap:7px;font-size:14px;font-weight:600;letter-spacing:-.01em;line-height:1.4}
22226
22370
  p{margin:0;text-wrap:pretty}
@@ -22234,13 +22378,13 @@ var RECOVERY_STYLE = `
22234
22378
  /* ErrorBoundary's ACTION_CLASS, one size up for a page that is alone in a viewport. */
22235
22379
  .btn{
22236
22380
  appearance:none;display:inline-flex;align-items:center;
22237
- border:1px solid var(--border);border-radius:6px;background:transparent;
22381
+ border:1px solid var(--control-border);border-radius:6px;background:transparent;
22238
22382
  padding:5px 10px;font:inherit;font-size:11.5px;color:color-mix(in srgb,var(--fg) 90%,transparent);
22239
22383
  cursor:pointer;text-decoration:none;
22240
22384
  transition:background-color 120ms var(--ease),border-color 120ms var(--ease),color 120ms var(--ease);
22241
22385
  }
22242
- .btn:hover{background:var(--panel);border-color:var(--border-strong);color:var(--fg)}
22243
- .btn:focus-visible{outline:none;border-color:color-mix(in srgb,var(--accent) 55%,transparent);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 22%,transparent)}
22386
+ .btn:hover{background:var(--panel);border-color:var(--control-strong);color:var(--fg)}
22387
+ .btn:focus-visible{outline:none;border-color:var(--focus-border);box-shadow:0 0 0 2px var(--focus-ring)}
22244
22388
  .btn:disabled{opacity:.5;pointer-events:none}
22245
22389
  /* min-height holds the slot open while it is LIVE, so a changing status line never nudges the card;
22246
22390
  :empty collapses it when there is nothing live to say, so a halted card has no trailing gutter. */
@@ -22260,8 +22404,7 @@ var RECOVERY_STYLE = `
22260
22404
  Alignment is an INK problem. A baseline-aligned flex item with no text of its own contributes its
22261
22405
  BORDER-BOX BOTTOM as the baseline, so the dot's ink centre lands 3px above it while the cap band's
22262
22406
  centre sits 0.5cap above it. \`cap\` is the resolved font's own cap height, so the correction holds
22263
- in BOTH board fonts and at any size, with nothing to re-measure when the type scale moves or the
22264
- font setting flips. The em spelling is the fallback for a browser without the unit (.35em ~= 0.5 x a
22407
+ at any size, with nothing to re-measure when the type scale moves. The em spelling is the fallback for a browser without the unit (.35em ~= 0.5 x a
22265
22408
  0.7em cap) and is overwritten wherever \`cap\` parses. */
22266
22409
  .dot{
22267
22410
  width:6px;height:6px;flex:0 0 auto;border-radius:9999px;background:var(--accent);
@@ -22292,7 +22435,7 @@ var RECOVERY_STYLE = `
22292
22435
  .probe[data-live=yes]{animation:none;background:none;color:var(--muted)}
22293
22436
  }
22294
22437
  `.trim();
22295
- var FONT_SCRIPT = `try{document.documentElement.dataset.font=localStorage.getItem("frizz-font")==="mono"?"mono":"sans"}catch{}`;
22438
+ var THEME_SCRIPT = `var p;try{p=localStorage.getItem("frizz-theme")}catch{}var d=false;try{d=matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches}catch{}var t=p==="dark"||p==="light"?p:d?"dark":"light";document.documentElement.dataset.theme=t;document.documentElement.style.colorScheme=t;document.querySelector('meta[name="theme-color"]').content=t==="dark"?"#0d0e10":"#f7f7f7"`;
22296
22439
  var RECOVERY_SCRIPT = `
22297
22440
  (function(){
22298
22441
  var target=document.body.dataset.target||"/"
@@ -22391,8 +22534,8 @@ var RECOVERY_SCRIPT = `
22391
22534
  })()
22392
22535
  `.trim();
22393
22536
  function documentShell(options2) {
22394
- return `<!doctype html><html lang="en" data-font="sans"><head><meta charset="utf-8"><title>${options2.title}</title><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><meta name="theme-color" content="#0d0e10"><style>${options2.style ? `${BASE_STYLE}
22395
- ${options2.style}` : BASE_STYLE}</style>` + (options2.font ? `<script>${FONT_SCRIPT}</script>` : "") + `</head><body${options2.bodyAttributes ? ` ${options2.bodyAttributes}` : ""}><main>${options2.body}</main>` + (options2.script ? `<script>${options2.script}</script>` : "") + `</body></html>`;
22537
+ return `<!doctype html><html lang="en" data-font="sans"><head><meta charset="utf-8"><title>${options2.title}</title><meta name="viewport" content="width=device-width,initial-scale=1">` + (options2.appearance ? `<meta name="color-scheme" content="dark light"><meta name="theme-color" content="#0d0e10">` : `<meta name="color-scheme" content="dark light"><meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0d0e10"><meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7">`) + `<style>${options2.style ? `${BASE_STYLE}
22538
+ ${options2.style}` : BASE_STYLE}</style>` + (options2.appearance ? `<script>${THEME_SCRIPT}</script>` : "") + `</head><body${options2.bodyAttributes ? ` ${options2.bodyAttributes}` : ""}><main>${options2.body}</main>` + (options2.script ? `<script>${options2.script}</script>` : "") + `</body></html>`;
22396
22539
  }
22397
22540
  var RECOVERY_COPY = {
22398
22541
  starting: {
@@ -22413,7 +22556,7 @@ function recoveryPage(url, variant = "starting") {
22413
22556
  return documentShell({
22414
22557
  title: copy.title,
22415
22558
  style: RECOVERY_STYLE,
22416
- font: true,
22559
+ appearance: true,
22417
22560
  bodyAttributes: `data-target="${target2}"`,
22418
22561
  body: `<h1><span class="dot" id="dot" aria-hidden="true"></span><span id="heading">${copy.heading}</span></h1><p class="detail" id="detail">${copy.detail}</p><pre class="log" id="log" hidden></pre><p class="note">${THREADS_NOTE}</p><div class="actions"><a class="btn" href="${target2}">Try again</a><button class="btn" type="button" id="restart">Restart Frizz</button></div><p class="probe" id="probe" aria-live="polite"></p>`,
22419
22562
  script: RECOVERY_SCRIPT
@@ -22430,7 +22573,7 @@ function unlistedHostPage(name) {
22430
22573
  const shown = escapeHtml(name);
22431
22574
  return documentShell({
22432
22575
  title: "Not served by this name",
22433
- font: true,
22576
+ appearance: true,
22434
22577
  body: `<h1>Not served by this name</h1><p class="detail">Frizz does not answer to <code>${shown}</code>. A name a browser has to resolve is how DNS rebinding makes another site's page count as this board, so an exposed board accepts only its own hostname and the names its operator lists.</p><p class="note">Open the board by its IP address, or relaunch with <code>--allowed-host ${shown}</code> (or <code>FRIZZ_ALLOWED_HOSTS=${shown}</code>) to accept this one.</p>`
22435
22578
  });
22436
22579
  }
@@ -23125,6 +23268,7 @@ function createAccessPane(options2) {
23125
23268
  import { homedir as homedir5 } from "node:os";
23126
23269
 
23127
23270
  // src/cloud.ts
23271
+ init_frizz_paths();
23128
23272
  init_src();
23129
23273
  import { spawn as spawn2 } from "node:child_process";
23130
23274
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "node:fs";
@@ -23138,8 +23282,8 @@ init_src();
23138
23282
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
23139
23283
  import { homedir as homedir3 } from "node:os";
23140
23284
  import { dirname as dirname3, join as join4 } from "node:path";
23141
- function claimIdentityPath(home = homedir3()) {
23142
- return join4(frizzPaths({ home }).state, "identity.key");
23285
+ function claimIdentityPath(home = homedir3(), env) {
23286
+ return join4(frizzPaths({ home, env }).state, "identity.key");
23143
23287
  }
23144
23288
  async function loadOrCreateClaimIdentity(home = homedir3()) {
23145
23289
  const path = claimIdentityPath(home);
@@ -23684,7 +23828,7 @@ async function reconcileCloudConfig(config, port, home = homedir4(), onNotice, o
23684
23828
  return establishCloudConfig(label, port, home, origin, github);
23685
23829
  }
23686
23830
  function cloudConfigPath(home = homedir4()) {
23687
- return join5(home, ".frizz", "cloud.json");
23831
+ return join5(frizzPaths({ home }).data, "cloud.json");
23688
23832
  }
23689
23833
  function readCloudConfig(home = homedir4()) {
23690
23834
  try {
@@ -23707,7 +23851,7 @@ function readCloudConfig(home = homedir4()) {
23707
23851
  }
23708
23852
  }
23709
23853
  function tunnelTokenPath(home = homedir4()) {
23710
- return join5(home, ".frizz", "tunnel-token");
23854
+ return join5(frizzPaths({ home }).state, "tunnel-token");
23711
23855
  }
23712
23856
  function readTunnelToken(home = homedir4()) {
23713
23857
  try {
@@ -24652,8 +24796,8 @@ init_logging();
24652
24796
  init_src();
24653
24797
  init_project_registry();
24654
24798
  var PORT_SCAN_COUNT = 100;
24655
- var LAUNCH_TIMEOUT_MS = 3e4;
24656
- var LAUNCH_HARD_TIMEOUT_MS = 10 * 6e4;
24799
+ var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
24800
+ var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
24657
24801
  var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
24658
24802
  function retiredFlagMessage(flag) {
24659
24803
  return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a private or custom frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
@@ -24766,11 +24910,14 @@ function parseCliArgs(argv) {
24766
24910
  function boardAddress(url) {
24767
24911
  return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
24768
24912
  }
24913
+ var SANDBOX_DROPPED_XDG_ROOTS = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"];
24769
24914
  function prepareSandbox(env = process.env, realHome = homedir10()) {
24770
24915
  const home = mkdtempSync(join13(tmpdir2(), "frizz-sandbox-"));
24771
- shareCredentials(realHome, home);
24916
+ const inherited = { ...env };
24772
24917
  env.HOME = home;
24773
24918
  env.USERPROFILE = home;
24919
+ for (const name of SANDBOX_DROPPED_XDG_ROOTS) delete env[name];
24920
+ shareCredentials(realHome, home, inherited, env);
24774
24921
  const project = join13(home, "sandbox");
24775
24922
  mkdirSync10(project, { recursive: true });
24776
24923
  writeFileSync10(join13(project, "README.md"), "# frizz sandbox\n\nA throwaway project; everything here is deleted when the sandbox exits.\n");
@@ -24782,15 +24929,15 @@ function prepareSandbox(env = process.env, realHome = homedir10()) {
24782
24929
  process.chdir(project);
24783
24930
  return { home, project };
24784
24931
  }
24785
- function shareCredentials(realHome, home) {
24932
+ function shareCredentials(realHome, home, realEnv, sandboxEnv) {
24786
24933
  const link = (from, to) => {
24787
24934
  try {
24788
24935
  symlinkSync2(from, to);
24789
24936
  } catch {
24790
24937
  }
24791
24938
  };
24792
- const realKey = claimIdentityPath(realHome);
24793
- const sandboxKey = claimIdentityPath(home);
24939
+ const realKey = claimIdentityPath(realHome, realEnv);
24940
+ const sandboxKey = claimIdentityPath(home, sandboxEnv);
24794
24941
  mkdirSync10(dirname10(realKey), { recursive: true });
24795
24942
  mkdirSync10(dirname10(sandboxKey), { recursive: true });
24796
24943
  link(realKey, sandboxKey);
@@ -25187,12 +25334,13 @@ import { basename as basename6, dirname as dirname11, extname as extname2, isAbs
25187
25334
  import { fileURLToPath } from "node:url";
25188
25335
  import watcher from "@parcel/watcher";
25189
25336
  init_logging();
25337
+ init_boot_progress();
25190
25338
  var DEV_RESTART_DEBOUNCE_MS = 180;
25191
25339
  var DEV_CRASH_STABLE_MS = 5e3;
25192
25340
  var DEV_CRASH_RETRY_BASE_MS = 500;
25193
25341
  var DEV_CRASH_RETRY_MAX_MS = 1e4;
25194
25342
  var CHILD_STOP_TIMEOUT_MS = 15e3;
25195
- var STABLE_UPDATE_READY_TIMEOUT_MS = 3e4;
25343
+ var STABLE_UPDATE_READY_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
25196
25344
  var STABLE_UPDATE_STABILIZE_MS = 1e3;
25197
25345
  var CHILD_KILL_GRACE_MS = 5e3;
25198
25346
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".mjs", ".cjs", ".json"]);
@@ -25444,6 +25592,7 @@ var Supervisor = class {
25444
25592
  updateMode;
25445
25593
  commitUpdate;
25446
25594
  updateReadyTimeoutMs;
25595
+ updateHardTimeoutMs;
25447
25596
  updateStabilizeMs;
25448
25597
  rollbackUpdate;
25449
25598
  durableReexec;
@@ -25488,6 +25637,7 @@ var Supervisor = class {
25488
25637
  this.updateMode = opts.updateMode ?? "durableReexec";
25489
25638
  this.commitUpdate = opts.commitUpdate;
25490
25639
  this.updateReadyTimeoutMs = Math.max(1, opts.updateReadyTimeoutMs ?? STABLE_UPDATE_READY_TIMEOUT_MS);
25640
+ this.updateHardTimeoutMs = Math.max(1, opts.updateHardTimeoutMs ?? BOOT_HARD_TIMEOUT_MS);
25491
25641
  this.updateStabilizeMs = Math.max(0, opts.updateStabilizeMs ?? STABLE_UPDATE_STABILIZE_MS);
25492
25642
  this.rollbackUpdate = opts.rollbackUpdate;
25493
25643
  this.durableReexec = opts.durableReexec;
@@ -25732,7 +25882,8 @@ var Supervisor = class {
25732
25882
  if (this.closed) return this.failChildUpdate("Frizz supervisor stopped while starting the update", true);
25733
25883
  const ready = await this.spawnChild({
25734
25884
  updateCandidate: true,
25735
- readinessTimeoutMs: this.updateReadyTimeoutMs
25885
+ readinessTimeoutMs: this.updateReadyTimeoutMs,
25886
+ hardTimeoutMs: this.updateHardTimeoutMs
25736
25887
  });
25737
25888
  const candidate = this.child;
25738
25889
  if (!ready || !candidate) {
@@ -25912,9 +26063,28 @@ var Supervisor = class {
25912
26063
  settled(ready);
25913
26064
  };
25914
26065
  if (options2.readinessTimeoutMs !== void 0) {
25915
- readinessTimer = setTimeout(() => {
26066
+ const stallMs = options2.readinessTimeoutMs;
26067
+ const hardMs = options2.hardTimeoutMs ?? BOOT_HARD_TIMEOUT_MS;
26068
+ const hardDeadline = Date.now() + hardMs;
26069
+ let stallDeadline = Date.now() + stallMs;
26070
+ let lastStep = -1;
26071
+ let lastPhase;
26072
+ const checkProgress = () => {
25916
26073
  if (spawnSettled) return;
25917
- const message = `update candidate did not become ready within ${options2.readinessTimeoutMs}ms`;
26074
+ const now = Date.now();
26075
+ const progress = readBootProgress(this.launchTarget.stateDir);
26076
+ if (progress && progress.pid === child.pid && progress.step > lastStep) {
26077
+ lastStep = progress.step;
26078
+ lastPhase = progress.phase;
26079
+ stallDeadline = now + stallMs;
26080
+ }
26081
+ if (now < stallDeadline && now < hardDeadline) {
26082
+ readinessTimer = setTimeout(checkProgress, Math.min(250, stallDeadline - now, hardDeadline - now));
26083
+ readinessTimer.unref();
26084
+ return;
26085
+ }
26086
+ const reason = now >= hardDeadline ? `update candidate did not become ready within ${hardMs}ms` : `update candidate made no boot progress for ${stallMs}ms`;
26087
+ const message = lastPhase ? `${reason}; last boot step: ${lastPhase}` : reason;
25918
26088
  this.lastRestartFailure = message;
25919
26089
  this.errorLine(`[frizz] ${message}`);
25920
26090
  this.writeStatus("failed", message, null);
@@ -25923,8 +26093,8 @@ var Supervisor = class {
25923
26093
  } catch {
25924
26094
  }
25925
26095
  settleSpawn(false);
25926
- }, options2.readinessTimeoutMs);
25927
- readinessTimer.unref();
26096
+ };
26097
+ checkProgress();
25928
26098
  }
25929
26099
  child.on("message", (message) => {
25930
26100
  if (stopOwnerMessage(message) && message.token === this.ownerToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frizz",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "private": false,
5
5
  "description": "A local web UI for running many coding agents at once",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  "frizz-dev:check": "nub scripts/install-global-cli.mjs --check",
36
36
  "frizz-dev:uninstall": "nub scripts/install-global-cli.mjs --uninstall",
37
37
  "install:global": "nub scripts/install-global-cli.mjs",
38
- "typecheck": "tsc -b packages/shared packages/rpc packages/server packages/registrar packages/relay . packages/web",
38
+ "typecheck": "tsc -b packages/shared packages/rpc packages/claude-agent-sdk-runtime packages/server packages/registrar packages/relay . packages/web",
39
39
  "test": "nub scripts/run-tests.mjs",
40
40
  "test:e2e": "nub scripts/e2e-web.mjs",
41
41
  "prepack": "node ./scripts/prepare-package.mjs --server && node ./scripts/build-package.mjs --server && node ./scripts/build-package.mjs --shell && node ./scripts/publish-manifest.mjs --strip",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "frizzServer": {
67
67
  "package": "frizz-server",
68
- "version": "0.13.0",
68
+ "version": "0.15.0",
69
69
  "protocol": 1,
70
70
  "dataEpoch": 1
71
71
  }