frizz 0.13.5 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/frizz.js +367 -109
  2. package/package.json +2 -2
package/dist/frizz.js CHANGED
@@ -12485,6 +12485,106 @@ var init_shell_writes = __esm({
12485
12485
  });
12486
12486
 
12487
12487
  // packages/shared/src/index.ts
12488
+ function signoffNudgeAwaitingLines(needsInput) {
12489
+ if (!needsInput) {
12490
+ return [
12491
+ "- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. FRONTMATTER, THEN MARKDOWN:",
12492
+ " one YAML list per kind of thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
12493
+ " as much prose as you want. The prose is OPTIONAL; the lines above it are not.",
12494
+ "",
12495
+ " ```awaiting",
12496
+ " shells: [<the id your runtime gave you>]",
12497
+ " prs: [owner/repo#123]",
12498
+ " for: 2h",
12499
+ " ---",
12500
+ " What you are waiting for, in your own words \u2014 this is what the human reads on your card.",
12501
+ " ```"
12502
+ ];
12503
+ }
12504
+ return [
12505
+ "- `` ```awaiting `` \u2014 you are WAITING on work that is actually running: a background shell, a",
12506
+ " sub-agent, a timer or a registered PR. FRONTMATTER, THEN MARKDOWN: one YAML list per kind of thing",
12507
+ " you are waiting on, a REQUIRED `for:` duration, a REQUIRED `needs_input:` answer, then optionally a",
12508
+ " `---` line and prose.",
12509
+ "",
12510
+ " ```awaiting",
12511
+ " agents: [<the id your runtime gave you>]",
12512
+ " needs_input: false",
12513
+ " for: 2h",
12514
+ " ```",
12515
+ "",
12516
+ " `needs_input: false` \u2014 nothing for the human yet. The thread stays out of their queue until the work",
12517
+ " wakes you, and you owe NO write-up: the fence alone is the whole message. `needs_input: true` \u2014 the",
12518
+ " human can read, try or act on something NOW while the work runs (a partial result, a file you wrote,",
12519
+ " a server to try), even if you need nothing back from them; the thread goes into their queue, and the",
12520
+ " prose under `---` says what to look at. If you wrote ANY words for the human at this rest, it is",
12521
+ " `true` \u2014 a `false` rest is never put in front of them. A rest on running work with NO fence is a",
12522
+ " bare rest, and it lands in the human's queue."
12523
+ ];
12524
+ }
12525
+ function signoffNudgeText(needsInput) {
12526
+ return [
12527
+ `${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
12528
+ "",
12529
+ "You rested without a fence, so this thread cannot be triaged.",
12530
+ "",
12531
+ "**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
12532
+ "original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
12533
+ "is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
12534
+ "neither is naming the next step or writing it into a scratch file.",
12535
+ "",
12536
+ "**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
12537
+ "FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
12538
+ "the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
12539
+ "thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
12540
+ "changes they now have to review. If it should be done, name it in one line and let the human dispatch",
12541
+ "it.",
12542
+ "",
12543
+ "**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
12544
+ "investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
12545
+ "work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
12546
+ "the write-up.",
12547
+ "",
12548
+ "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
12549
+ "right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
12550
+ "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
12551
+ "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
12552
+ "unanswered question is not permission to go build the answer.",
12553
+ "",
12554
+ "Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
12555
+ "",
12556
+ "- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
12557
+ " with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
12558
+ " recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
12559
+ "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
12560
+ " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
12561
+ " **bolded verb phrase**.",
12562
+ ...signoffNudgeAwaitingLines(needsInput),
12563
+ "",
12564
+ " Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
12565
+ " A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
12566
+ " awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
12567
+ " `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
12568
+ "",
12569
+ "**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
12570
+ "draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
12571
+ "the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
12572
+ "or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
12573
+ "`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
12574
+ "None of them is a `done`. And what is not",
12575
+ 'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
12576
+ "in the handoff.",
12577
+ "",
12578
+ "**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
12579
+ "fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
12580
+ "The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
12581
+ "ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
12582
+ "",
12583
+ "Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
12584
+ "seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
12585
+ "frizz \u2014 so anything you assumed they had followed, they have not."
12586
+ ].join("\n");
12587
+ }
12488
12588
  function requireEffortOutsideAcp(profile, ctx) {
12489
12589
  if (profile.backend !== "acp" && profile.effort === void 0) {
12490
12590
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
@@ -12493,7 +12593,7 @@ function requireEffortOutsideAcp(profile, ctx) {
12493
12593
  function fallbackPort(base) {
12494
12594
  return base + 1e4;
12495
12595
  }
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, SettledQuestionView, ThreadSettledQuestionsResult, 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, 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;
12596
+ 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, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, 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, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, 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;
12497
12597
  var init_src = __esm({
12498
12598
  "packages/shared/src/index.ts"() {
12499
12599
  "use strict";
@@ -12831,7 +12931,7 @@ var init_src = __esm({
12831
12931
  questions: external_exports.array(AskQuestion)
12832
12932
  });
12833
12933
  AwaitingHint = external_exports.object({
12834
- kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title"]),
12934
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "needs_input"]),
12835
12935
  value: external_exports.string()
12836
12936
  });
12837
12937
  RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
@@ -12870,77 +12970,8 @@ var init_src = __esm({
12870
12970
  TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
12871
12971
  TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
12872
12972
  SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
12873
- SIGNOFF_NUDGE_MESSAGE = [
12874
- `${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
12875
- "",
12876
- "You rested without a fence, so this thread cannot be triaged.",
12877
- "",
12878
- "**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
12879
- "original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
12880
- "is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
12881
- "neither is naming the next step or writing it into a scratch file.",
12882
- "",
12883
- "**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
12884
- "FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
12885
- "the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
12886
- "thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
12887
- "changes they now have to review. If it should be done, name it in one line and let the human dispatch",
12888
- "it.",
12889
- "",
12890
- "**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
12891
- "investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
12892
- "work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
12893
- "the write-up.",
12894
- "",
12895
- "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
12896
- "right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
12897
- "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
12898
- "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
12899
- "unanswered question is not permission to go build the answer.",
12900
- "",
12901
- "Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
12902
- "",
12903
- "- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
12904
- " with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
12905
- " recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
12906
- "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
12907
- " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
12908
- " **bolded verb phrase**.",
12909
- "- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. FRONTMATTER, THEN MARKDOWN:",
12910
- " one structural line per thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
12911
- " as much prose as you want. The prose is OPTIONAL; the lines above it are not.",
12912
- "",
12913
- " ```awaiting",
12914
- " shell: <the id your runtime gave you>",
12915
- " pr: owner/repo#123",
12916
- " for: 2h",
12917
- " ---",
12918
- " What you are waiting for, in your own words \u2014 this is what the human reads on your card.",
12919
- " ```",
12920
- "",
12921
- " Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
12922
- " A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
12923
- " awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
12924
- " `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
12925
- "",
12926
- "**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
12927
- "draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
12928
- "the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
12929
- "or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
12930
- "`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
12931
- "None of them is a `done`. And what is not",
12932
- 'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
12933
- "in the handoff.",
12934
- "",
12935
- "**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
12936
- "fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
12937
- "The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
12938
- "ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
12939
- "",
12940
- "Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
12941
- "seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
12942
- "frizz \u2014 so anything you assumed they had followed, they have not."
12943
- ].join("\n");
12973
+ SIGNOFF_NUDGE_MESSAGE = signoffNudgeText(false);
12974
+ SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT = signoffNudgeText(true);
12944
12975
  ThreadWatchKind = external_exports.enum(["shell", "github", "timer"]);
12945
12976
  GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
12946
12977
  GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
@@ -13307,6 +13338,11 @@ var init_src = __esm({
13307
13338
  UpsertOwnLinkResult = external_exports.object({ link: ThreadLinkView }).strict();
13308
13339
  DropOwnLinkInput = external_exports.object({ slug: ThreadSlug, id: external_exports.string().min(1) }).strict();
13309
13340
  DropOwnLinkResult = external_exports.object({ dropped: external_exports.boolean() }).strict();
13341
+ LiveTool = external_exports.object({
13342
+ name: external_exports.string(),
13343
+ desc: external_exports.string().optional(),
13344
+ detail: external_exports.string().optional()
13345
+ });
13310
13346
  ThreadView = external_exports.object({
13311
13347
  id: ThreadSlug,
13312
13348
  // slug; filename is <slug>.md
@@ -13345,6 +13381,15 @@ var init_src = __esm({
13345
13381
  // user hid the row from the nav; respawn/resume un-archives
13346
13382
  lastAssistant: external_exports.string().optional(),
13347
13383
  // trimmed preview of last assistant text
13384
+ // The FIRST non-empty line of that same text, markdown intact and newlines honoured (capped) — the
13385
+ // handoff's verdict line ("**Fixed** — …"), which `lastAssistant` cannot give back because its preview
13386
+ // collapses every newline to a space. The phone board's rested row reads it. Optional so old
13387
+ // snapshots parse.
13388
+ lastAssistantLine: external_exports.string().optional(),
13389
+ // The newest tool call the agent has issued and not yet had a result for (Claude session threads),
13390
+ // in the shape `toolActivityLabel` reads, so a list row can say "Running the focused tests" with the
13391
+ // gerund the chat's working indicator shows. Absent between calls, at rest, and for other backends.
13392
+ liveTool: LiveTool.optional(),
13348
13393
  spawnedAt: external_exports.string().optional(),
13349
13394
  // ISO8601
13350
13395
  lastActivityAt: external_exports.string().optional(),
@@ -13403,6 +13448,11 @@ var init_src = __esm({
13403
13448
  * same card and the swap is invisible. Absent whenever there is nothing in flight, which is almost
13404
13449
  * always — it exists for the seconds between the human sending and the worker being handed it. */
13405
13450
  answersInFlight: external_exports.string().optional(),
13451
+ /** The human's message — a follow-up or a registered answer — is on its way to the worker and the turn
13452
+ * has not started yet (board.deriveDeliveryInFlight). The queue has already let the thread go; this
13453
+ * tells the rail to draw it as working in every tab, not only the one that sent it (web
13454
+ * lib/steering.ts). Absent otherwise. */
13455
+ deliveryInFlight: external_exports.boolean().optional(),
13406
13456
  // ISO8601 of the newest REAL user interaction (answer/steer/dispatch) — the chronological listing
13407
13457
  // sort key. Optional; the listing falls back to spawnedAt when absent (a dispatch IS an interaction).
13408
13458
  lastUserAt: external_exports.string().optional(),
@@ -13835,8 +13885,18 @@ var init_src = __esm({
13835
13885
  // Broker-backed Claude only — that is every Claude thread dispatched since the broker cutover. On
13836
13886
  // any other runtime the message is delivered normally and this is ignored, never refused: a send
13837
13887
  // that arrives is always better than a send that errors.
13838
- interrupt: external_exports.boolean().optional()
13888
+ interrupt: external_exports.boolean().optional(),
13889
+ // The deliveryId of a FAILED send this one re-sends — the failed bubble's Retry. The server drops that
13890
+ // failed entry in the same write that opens this send's own, so the retry replaces the failed bubble
13891
+ // rather than standing beside it. A new deliveryId, never the old one: the old send's failure may be
13892
+ // ambiguous, and only the operator decides to risk a second copy.
13893
+ supersedes: external_exports.string().min(1).max(200).optional()
13839
13894
  });
13895
+ DismissFailedFollowUpInput = external_exports.object({
13896
+ slug: ThreadSlug,
13897
+ deliveryId: external_exports.string().min(1).max(200)
13898
+ }).strict();
13899
+ DismissFailedFollowUpResult = external_exports.object({ dismissed: external_exports.boolean() }).strict();
13840
13900
  UnqueueFollowUpInput = external_exports.object({
13841
13901
  slug: ThreadSlug,
13842
13902
  // Same staleness guard as followUp: a stale tab must not unqueue against a re-dispatched session.
@@ -13856,6 +13916,11 @@ var init_src = __esm({
13856
13916
  interrupted: external_exports.boolean(),
13857
13917
  reason: external_exports.string().optional()
13858
13918
  }).strict();
13919
+ CompactThreadInput = external_exports.object({
13920
+ slug: ThreadSlug,
13921
+ // Same staleness guard as followUp: a stale tab must not compact a re-dispatched session.
13922
+ sessionId: external_exports.string().min(1)
13923
+ }).strict();
13859
13924
  SetThreadSnoozeInput = external_exports.object({
13860
13925
  slug: ThreadSlug,
13861
13926
  sessionId: external_exports.string().min(1),
@@ -14464,8 +14529,13 @@ ${DISPATCH_TASK_BANNER}
14464
14529
  // yet — renders as an ordinary (un-grayed) user bubble. "unconfirmed": no evidence appeared within
14465
14530
  // the timeout — the injection likely mutated/never landed; the client renders a quiet warning.
14466
14531
  // Once the real transcript record lands the ledger drops the item and this field goes with it.
14467
- // Additive + optional.
14468
- deliveryState: external_exports.enum(["pending", "enqueued", "delivered", "unconfirmed"]).optional(),
14532
+ // "sending": the server's WRITE-AHEAD entry, opened before any transport is touched, so the text is
14533
+ // the server's from the instant it arrives. "failed": the transport threw, or never answered — the
14534
+ // text is kept until the operator retries, edits or dismisses it, and nothing retries it on its own.
14535
+ // Additive + optional: an older client renders both as a plain bubble (gray for "sending").
14536
+ deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed"]).optional(),
14537
+ // Why a "failed" send failed, verbatim from the transport. Set only alongside deliveryState "failed".
14538
+ deliveryError: external_exports.string().optional(),
14469
14539
  // FRIZZ wrote this user turn, not the human: it is a scheduler wake delivery (isWakeDelivery). The
14470
14540
  // client renders it as a first-party card rather than the human's off-white right-justified bubble,
14471
14541
  // which was claiming the operator had typed a message the watcher composed. Additive + optional: an
@@ -14617,7 +14687,8 @@ ${DISPATCH_TASK_BANNER}
14617
14687
  });
14618
14688
  ProjectRailCounts = external_exports.object({
14619
14689
  queued: external_exports.number().int().nonnegative(),
14620
- running: external_exports.number().int().nonnegative()
14690
+ running: external_exports.number().int().nonnegative(),
14691
+ asks: external_exports.number().int().nonnegative()
14621
14692
  });
14622
14693
  PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
14623
14694
  DirectoryPickResult = external_exports.discriminatedUnion("kind", [
@@ -20558,6 +20629,29 @@ var AccessStore = class _AccessStore {
20558
20629
  const id = _AccessStore.idOf(payload);
20559
20630
  return !(id && this.sessions.isRevoked(id));
20560
20631
  }
20632
+ /**
20633
+ * Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
20634
+ *
20635
+ * The id comes from the session itself, and only after its signature verifies, so the one thing this
20636
+ * can ever revoke is the credential the caller just proved it holds. There is deliberately no
20637
+ * parameter naming any other id: signing out SOMEONE ELSE stays loopback-only, because a stolen
20638
+ * session must not be able to evict the owner and keep the board.
20639
+ *
20640
+ * - `revoked`: the id is on the denylist now, so a surviving copy of this cookie is refused too.
20641
+ * - `legacy`: a session minted before ids existed verifies but has no id to revoke on its own. The
20642
+ * caller can still clear the cookie; rotating the key remains the way to kill a copy of it.
20643
+ * - `no-session`: nothing valid was presented — absent, forged, expired, or already signed out.
20644
+ */
20645
+ signOutSession(session) {
20646
+ if (!session || !this.verifySession(session)) return { result: "no-session" };
20647
+ const id = _AccessStore.idOf(session.slice(0, session.lastIndexOf(".")));
20648
+ if (!id) return { result: "legacy" };
20649
+ if (!this.sessions.revoke(id)) {
20650
+ this.sessions.record({ id, label: "unknown device", createdAt: this.options.now() });
20651
+ this.sessions.revoke(id);
20652
+ }
20653
+ return this.sessions.isRevoked(id) ? { result: "revoked", id } : { result: "no-session" };
20654
+ }
20561
20655
  /**
20562
20656
  * Drop codes past their expiry so a long-lived board does not accumulate them forever.
20563
20657
  *
@@ -22270,13 +22364,26 @@ var qrcode_default = qrcode;
22270
22364
  var stringToBytes = qrcode.stringToBytes;
22271
22365
 
22272
22366
  // packages/server/src/qr.ts
22273
- var LIGHT_BG = "\x1B[48;5;15m";
22274
- var DARK = "\x1B[38;5;0m";
22275
- var DARK_BG = "\x1B[48;5;0m";
22367
+ var DARK = 232;
22368
+ var LIGHT = 231;
22369
+ var fg = (dark) => `\x1B[38;5;${dark ? DARK : LIGHT}m`;
22370
+ var bg = (dark) => `\x1B[48;5;${dark ? DARK : LIGHT}m`;
22276
22371
  var RESET = "\x1B[0m";
22277
22372
  var UPPER_HALF = "\u2580";
22278
22373
  var LOWER_HALF = "\u2584";
22279
22374
  var QUIET_ZONE = 4;
22375
+ var BLOCK_DRAWING_PROGRAMS = /* @__PURE__ */ new Set(["iTerm.app", "WezTerm", "ghostty", "WarpTerminal", "vscode"]);
22376
+ var BLOCK_DRAWING_TERMS = /* @__PURE__ */ new Set(["xterm-kitty", "xterm-ghostty", "alacritty", "wezterm"]);
22377
+ function drawsBlockElements(env = process.env) {
22378
+ const term = env.TERM ?? "";
22379
+ if (env.TMUX || env.STY || env.TERM_PROGRAM === "tmux" || term.startsWith("screen") || term.startsWith("tmux")) return false;
22380
+ if (BLOCK_DRAWING_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return true;
22381
+ if (BLOCK_DRAWING_TERMS.has(term)) return true;
22382
+ if (env.LC_TERMINAL === "iTerm2") return true;
22383
+ return Boolean(
22384
+ env.KITTY_WINDOW_ID || env.ALACRITTY_WINDOW_ID || env.WEZTERM_PANE || env.GHOSTTY_RESOURCES_DIR || env.WT_SESSION || env.VTE_VERSION
22385
+ );
22386
+ }
22280
22387
  function encode(value, errorCorrection) {
22281
22388
  const code = qrcode_default(0, errorCorrection);
22282
22389
  code.addData(value);
@@ -22293,26 +22400,70 @@ function encode(value, errorCorrection) {
22293
22400
  }
22294
22401
  };
22295
22402
  }
22296
- function renderQrLines(value, options2 = {}) {
22297
- if (!value) throw new Error("renderQr requires a value");
22298
- const { size, at } = encode(value, options2.errorCorrection ?? "M");
22403
+ function sizeOf(size, style) {
22404
+ return style === "paint" ? { columns: size * 2, rows: size } : { columns: size, rows: Math.ceil(size / 2) };
22405
+ }
22406
+ function qrSize(value, style = "half", errorCorrection = "M") {
22407
+ return sizeOf(encode(value, errorCorrection).size, style);
22408
+ }
22409
+ function qrStyleFor(value, options2 = {}) {
22410
+ const { area } = options2;
22411
+ if (!area || drawsBlockElements(options2.env)) return "half";
22412
+ const paint = qrSize(value, "paint", options2.errorCorrection);
22413
+ return paint.columns <= area.columns && paint.rows <= area.rows ? "paint" : "half";
22414
+ }
22415
+ function qrAreaOf(stream, frame) {
22416
+ if (!stream.columns || !stream.rows) return void 0;
22417
+ return { columns: stream.columns - frame.indent, rows: stream.rows - frame.rows };
22418
+ }
22419
+ function halfLines(size, at, plain) {
22299
22420
  const lines = [];
22300
22421
  for (let y = 0; y < size; y += 2) {
22301
22422
  let line = "";
22302
22423
  for (let x = 0; x < size; x++) {
22303
22424
  const top = at(x, y);
22304
22425
  const bottom = y + 1 < size ? at(x, y + 1) : false;
22305
- if (options2.plain) {
22426
+ if (plain) {
22306
22427
  line += top && bottom ? "#" : top ? "^" : bottom ? "v" : " ";
22307
22428
  continue;
22308
22429
  }
22309
- if (top === bottom) line += `${top ? DARK_BG : LIGHT_BG} `;
22310
- else line += `${DARK}${LIGHT_BG}${top ? UPPER_HALF : LOWER_HALF}`;
22430
+ if (top === bottom) {
22431
+ line += `${bg(top)} `;
22432
+ continue;
22433
+ }
22434
+ const above = y > 0 ? at(x, y - 1) : false;
22435
+ const below = y + 2 < size ? at(x, y + 2) : false;
22436
+ line += below === bottom && above !== top ? `${fg(top)}${bg(bottom)}${UPPER_HALF}` : `${fg(bottom)}${bg(top)}${LOWER_HALF}`;
22437
+ }
22438
+ lines.push(plain ? line : `${line}${RESET}`);
22439
+ }
22440
+ return lines;
22441
+ }
22442
+ function paintLines(size, at, plain) {
22443
+ const lines = [];
22444
+ for (let y = 0; y < size; y++) {
22445
+ let line = "";
22446
+ let last;
22447
+ for (let x = 0; x < size; x++) {
22448
+ const dark = at(x, y);
22449
+ if (plain) line += dark ? "##" : " ";
22450
+ else {
22451
+ if (dark !== last) line += bg(dark);
22452
+ line += " ";
22453
+ }
22454
+ last = dark;
22311
22455
  }
22312
- lines.push(options2.plain ? line : `${line}${RESET}`);
22456
+ lines.push(plain ? line : `${line}${RESET}`);
22313
22457
  }
22314
22458
  return lines;
22315
22459
  }
22460
+ function renderQrLines(value, options2 = {}) {
22461
+ if (!value) throw new Error("renderQr requires a value");
22462
+ const errorCorrection = options2.errorCorrection ?? "M";
22463
+ const { size, at } = encode(value, errorCorrection);
22464
+ const style = options2.style ?? qrStyleFor(value, { area: options2.area, env: options2.env, errorCorrection });
22465
+ return style === "paint" ? paintLines(size, at, options2.plain ?? false) : halfLines(size, at, options2.plain ?? false);
22466
+ }
22316
22467
 
22317
22468
  // packages/server/src/restart-supervisor.ts
22318
22469
  import { request as requestHttp, createServer } from "node:http";
@@ -22583,6 +22734,7 @@ var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restar
22583
22734
  var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
22584
22735
  var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
22585
22736
  var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
22737
+ var SUPERVISOR_SIGN_OUT_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sign-out`;
22586
22738
  var SUPERVISOR_CONTROL_PROTOCOL = 1;
22587
22739
  function responseJson(res, status, value) {
22588
22740
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
@@ -22611,7 +22763,7 @@ function readCookie(header, name) {
22611
22763
  }
22612
22764
  function isControlRequest(req) {
22613
22765
  const url = new URL(req.url ?? "/", "http://frizz.invalid");
22614
- return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH;
22766
+ return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
22615
22767
  }
22616
22768
  function isLocalImageRequest(req) {
22617
22769
  if (req.method !== "GET" && req.method !== "HEAD") return false;
@@ -22817,6 +22969,11 @@ var RestartSupervisorProxy = class {
22817
22969
  }
22818
22970
  async handleControl(req, res) {
22819
22971
  const pathname = new URL(req.url ?? "/", "http://frizz.invalid").pathname;
22972
+ if (pathname !== SUPERVISOR_SIGN_OUT_PATH && this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
22973
+ res.writeHead(401, { "cache-control": "no-store" });
22974
+ res.end();
22975
+ return;
22976
+ }
22820
22977
  const sameOrigin = req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req);
22821
22978
  const allowMissingOrigin = pathname === SUPERVISOR_STATUS_PATH && req.method === "GET" && sameOrigin;
22822
22979
  if (!this.authorityAccepted(req, allowMissingOrigin)) {
@@ -22880,8 +23037,24 @@ var RestartSupervisorProxy = class {
22880
23037
  responseJson(res, 200, { signedOut: 1 });
22881
23038
  return;
22882
23039
  }
23040
+ if (pathname === SUPERVISOR_SIGN_OUT_PATH) {
23041
+ if (req.method !== "POST") {
23042
+ res.writeHead(405, { allow: "POST" });
23043
+ res.end();
23044
+ return;
23045
+ }
23046
+ this.signOutThisDevice(req, res);
23047
+ return;
23048
+ }
22883
23049
  if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
22884
- responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
23050
+ responseJson(res, 200, {
23051
+ protocol: SUPERVISOR_CONTROL_PROTOCOL,
23052
+ ...this.status(),
23053
+ // Sent ONLY when true, so a loopback tab's payload is byte-identical to what it was. The web
23054
+ // app cannot tell on its own: the session cookie is HttpOnly, and the page is the same bundle
23055
+ // whichever way it was reached. It is a fact about THIS request, never about the board.
23056
+ ...this.carriesRemoteSession(req) ? { remoteSession: true } : {}
23057
+ });
22885
23058
  return;
22886
23059
  }
22887
23060
  if (pathname !== SUPERVISOR_RESTART_PATH && pathname !== SUPERVISOR_UPDATE_RESTART_PATH || req.method !== "POST") {
@@ -22959,6 +23132,42 @@ var RestartSupervisorProxy = class {
22959
23132
  if (!this.publicOrigin) return true;
22960
23133
  return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
22961
23134
  }
23135
+ /** Did this request arrive through the public origin carrying a live session? Loopback never does. */
23136
+ carriesRemoteSession(req) {
23137
+ return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
23138
+ }
23139
+ /**
23140
+ * End the session this request carries, and nothing else — see SUPERVISOR_SIGN_OUT_PATH in
23141
+ * handleControl for why that makes it safe to reach from the tunnel.
23142
+ *
23143
+ * A loopback request is a no-op even if it somehow carries a cookie: the operator's own tab has no
23144
+ * remote session to end, and loopback is the place `--sign-out` already covers.
23145
+ */
23146
+ signOutThisDevice(req, res) {
23147
+ const reply = (status, body, clearCookie) => {
23148
+ res.writeHead(status, {
23149
+ "content-type": "application/json; charset=utf-8",
23150
+ "cache-control": "no-store",
23151
+ // Same attributes the exchange set, so the browser matches and drops that exact cookie.
23152
+ ...clearCookie ? { "set-cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0` } : {}
23153
+ });
23154
+ res.end(JSON.stringify(body));
23155
+ };
23156
+ if (!this.arrivedPublicly(req)) {
23157
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, false);
23158
+ return;
23159
+ }
23160
+ const outcome = this.access.signOutSession(readCookie(req.headers.cookie, SESSION_COOKIE));
23161
+ if (outcome.result === "revoked") {
23162
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "signed-out", id: outcome.id }, true);
23163
+ return;
23164
+ }
23165
+ if (outcome.result === "legacy") {
23166
+ reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "cookie-cleared" }, true);
23167
+ return;
23168
+ }
23169
+ reply(401, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, true);
23170
+ }
22962
23171
  /**
22963
23172
  * Trade `?frizz_code=…` for a cookie, then bounce to the same URL
22964
23173
  * without the secret, so it never lands in history, a Referer header, or a screenshot.
@@ -23177,12 +23386,20 @@ function installPaneHost(options2) {
23177
23386
  input.pause();
23178
23387
  process.off("exit", restore);
23179
23388
  };
23389
+ const reclaim = () => {
23390
+ if (disposed) return;
23391
+ try {
23392
+ input.setRawMode(false);
23393
+ input.setRawMode(true);
23394
+ } catch {
23395
+ }
23396
+ };
23180
23397
  input.setRawMode(true);
23181
23398
  input.resume();
23182
23399
  input.setEncoding("utf8");
23183
23400
  input.on("data", onKey);
23184
23401
  process.on("exit", restore);
23185
- return { dispose: restore, isOpen: () => active !== null };
23402
+ return { dispose: restore, isOpen: () => active !== null, reclaim };
23186
23403
  }
23187
23404
 
23188
23405
  // src/access-pane.ts
@@ -23196,6 +23413,18 @@ var RESET2 = "\x1B[0m";
23196
23413
  function secondsUntil(expiresAt, now) {
23197
23414
  return Math.max(0, Math.ceil((expiresAt - now) / 1e3));
23198
23415
  }
23416
+ function fitScreen(entries, rows, columns) {
23417
+ const width = columns ?? 80;
23418
+ const height = (text) => Math.max(1, Math.ceil([...text.replace(/\x1b\[[0-9;]*m/g, "")].length / width));
23419
+ const kept = entries.map((entry) => typeof entry === "string" ? { text: entry, drop: Infinity } : entry);
23420
+ const total = () => kept.reduce((sum, entry) => sum + height(entry.text), 0);
23421
+ while (rows && total() > rows) {
23422
+ const next = kept.reduce((lowest, entry, index) => entry.drop < (kept[lowest]?.drop ?? Infinity) ? index : lowest, -1);
23423
+ if (next < 0) break;
23424
+ kept.splice(next, 1);
23425
+ }
23426
+ return kept.map((entry) => entry.text).join("\n");
23427
+ }
23199
23428
  function createAccessPane(options2) {
23200
23429
  const output = options2.output ?? process.stdout;
23201
23430
  const now = options2.now ?? Date.now;
@@ -23207,16 +23436,25 @@ function createAccessPane(options2) {
23207
23436
  if (!shown) return;
23208
23437
  const remaining = secondsUntil(shown.expiresAt, now());
23209
23438
  const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
23439
+ const footer = `${status} Press any other key to close.`;
23440
+ const width = output.columns ?? 80;
23441
+ const wrapped = (text) => Math.max(1, Math.ceil((text.length + 2) / width));
23442
+ const qr = renderQrLines(shown.url, { area: qrAreaOf(output, { indent: 2, rows: wrapped(shown.url) + wrapped(footer) + 1 }) });
23210
23443
  output.write(CLEAR);
23211
- output.write("\n");
23212
- for (const row of renderQrLines(shown.url)) output.write(` ${row}
23213
- `);
23214
- output.write(`
23215
- ${shown.url}
23216
- `);
23217
- output.write(`
23218
- ${DIM}${status} Press any other key to close.${RESET2}
23219
- `);
23444
+ output.write(
23445
+ fitScreen(
23446
+ [
23447
+ { text: "", drop: 1 },
23448
+ ...qr.map((row) => ` ${row}`),
23449
+ { text: "", drop: 3 },
23450
+ ` ${shown.url}`,
23451
+ { text: "", drop: 2 },
23452
+ ` ${DIM}${footer}${RESET2}`
23453
+ ],
23454
+ output.rows,
23455
+ output.columns
23456
+ )
23457
+ );
23220
23458
  };
23221
23459
  return {
23222
23460
  open() {
@@ -24163,9 +24401,8 @@ function createRemotePane(options2) {
24163
24401
  const write = (lines) => {
24164
24402
  if (!open) return;
24165
24403
  output.write(CLEAR);
24166
- output.write("\n");
24167
- for (const line of lines) output.write(` ${line}
24168
- `);
24404
+ const indented = lines.map((line) => typeof line === "string" ? ` ${line}` : { ...line, text: ` ${line.text}` });
24405
+ output.write(fitScreen([{ text: "", drop: 0 }, ...indented], output.rows, output.columns));
24169
24406
  };
24170
24407
  const check = (ok, text) => ok ? `${text} \u2713` : text;
24171
24408
  const paint = () => {
@@ -24207,13 +24444,25 @@ function createRemotePane(options2) {
24207
24444
  return;
24208
24445
  }
24209
24446
  if (s.name === "done") {
24210
- const lines = [s.message, ""];
24211
- if (s.link) {
24212
- for (const row of renderQrLines(s.link.url)) lines.push(row);
24213
- lines.push("", s.link.url, "", `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`);
24447
+ if (!s.link) {
24448
+ write([s.message, "", `${DIM}press any key to return${RESET2}`]);
24449
+ return;
24214
24450
  }
24215
- lines.push("", `${DIM}press any key to return${RESET2}`);
24216
- write(lines);
24451
+ const note = `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`;
24452
+ const width = output.columns ?? 80;
24453
+ const wrapped = (text) => Math.max(1, Math.ceil((text.length + 2) / width));
24454
+ const qr = renderQrLines(s.link.url, { area: qrAreaOf(output, { indent: 2, rows: wrapped(s.message) + 1 + wrapped(s.link.url) + 1 }) });
24455
+ write([
24456
+ s.message,
24457
+ { text: "", drop: 4 },
24458
+ ...qr,
24459
+ { text: "", drop: 6 },
24460
+ s.link.url,
24461
+ { text: "", drop: 2 },
24462
+ { text: note, drop: 5 },
24463
+ { text: "", drop: 3 },
24464
+ `${DIM}press any key to return${RESET2}`
24465
+ ]);
24217
24466
  return;
24218
24467
  }
24219
24468
  write([`Could not apply that: ${s.message}`, "", `${DIM}press any key to go back${RESET2}`]);
@@ -25578,6 +25827,7 @@ var Supervisor = class {
25578
25827
  logLine;
25579
25828
  errorLine;
25580
25829
  onActivity;
25830
+ onChildExit;
25581
25831
  /** Suppresses activity beats until the first boot has settled; until then the readout owns the terminal. */
25582
25832
  booted = false;
25583
25833
  /** When the restart currently in flight began, so its "ready" beat can report a duration. */
@@ -25630,6 +25880,7 @@ var Supervisor = class {
25630
25880
  this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
25631
25881
  this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
25632
25882
  this.onActivity = opts.onActivity;
25883
+ this.onChildExit = opts.onChildExit;
25633
25884
  this.updateRestart = opts.updateRestart;
25634
25885
  this.updateMode = opts.updateMode ?? "durableReexec";
25635
25886
  this.commitUpdate = opts.commitUpdate;
@@ -26128,6 +26379,10 @@ var Supervisor = class {
26128
26379
  settleSpawn(false);
26129
26380
  });
26130
26381
  child.once("exit", (code, signal) => {
26382
+ try {
26383
+ this.onChildExit?.();
26384
+ } catch {
26385
+ }
26131
26386
  if (this.child === child) {
26132
26387
  this.child = null;
26133
26388
  this.childPort = void 0;
@@ -27096,7 +27351,7 @@ if (options.link) {
27096
27351
  process.exit(1);
27097
27352
  }
27098
27353
  const { url } = await response.json();
27099
- if (process.stdout.isTTY) for (const row of renderQrLines(url)) console.log(` ${row}`);
27354
+ if (process.stdout.isTTY) for (const row of renderQrLines(url, { area: qrAreaOf(process.stdout, { indent: 2, rows: 3 }) })) console.log(` ${row}`);
27100
27355
  console.log(process.stdout.isTTY ? `
27101
27356
  ${url}` : url);
27102
27357
  process.exit(0);
@@ -27274,6 +27529,9 @@ async function runSupervisor(port, token, onPrepared = () => {
27274
27529
  // here is triggered from a browser tab or by a crash, so without this the foreground process is
27275
27530
  // the last place to learn what happened to it.
27276
27531
  onActivity: (event) => renderSupervisorActivity(activityReadout, event),
27532
+ // Every child leaves the terminal as it found it at its own fork, and the first one was forked
27533
+ // before the keyboard went raw — so without this R stopped working after the first update.
27534
+ onChildExit: () => paneHost?.reclaim(),
27277
27535
  updateRestart: async () => {
27278
27536
  previous = active;
27279
27537
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frizz",
3
- "version": "0.13.5",
3
+ "version": "0.13.7",
4
4
  "private": false,
5
5
  "description": "A local web UI for running many coding agents at once",
6
6
  "keywords": [
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "frizzServer": {
68
68
  "package": "frizz-server",
69
- "version": "0.15.3",
69
+ "version": "0.15.8",
70
70
  "protocol": 1,
71
71
  "dataEpoch": 1
72
72
  }