@mjasnikovs/pi-task 0.39.1 → 0.39.3

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.
@@ -1 +1 @@
1
- export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set by setAppHeight (visualViewport.height, falling back to\n window.innerHeight) so the column height is a stable pixel value across\n an orientation change. 100dvh is the fallback for first paint / no-JS. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right, on its own surface bar. */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n /* Held mid-run input: what you typed is waiting for the next task turn.\n In normal flow, NOT absolutely positioned \u2014 as an overlay it would cover\n the task widget's progress row above the composer. */\n #held-bar {\n display: flex; align-items: center; gap: 8px; flex-shrink: 0;\n background: var(--mantle); border-top: 1px solid var(--surface1);\n padding: 6px 16px; font-size: 12px;\n }\n #held-label { color: var(--yellow); white-space: nowrap; }\n #held-text {\n color: var(--subtext0); overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; flex: 1;\n }\n #held-clear {\n background: none; border: none; color: var(--subtext0);\n cursor: pointer; font-size: 13px; padding: 0 2px;\n }\n #held-clear:hover { color: var(--red); }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble\n (one ticker paints every .spin), inline at the end of the streaming text. */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n /* Title gets its own line and wraps, clamped to 2 \u2014 never squeezed to a stub\n by the meta row beside it on a narrow screen. */\n .widget-title { display: -webkit-box; -webkit-box-orient: vertical;\n -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);\n font-size: 13px; line-height: 1.3; word-break: break-word; }\n .widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);\n font-size: 11px; font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n /* Fill carries a soft highlight band that sweeps left\u2192right through the mauve,\n so the bar reads as \"working\" even while the step count holds still. The\n gradient tile is 2x the fill width and shifts one full period per cycle,\n so the loop is seamless. Base color stays mauve for reduced-motion. */\n .widget-bar-fill { height: 100%; border-radius: 2px;\n background: linear-gradient(90deg,\n var(--mauve) 0%, var(--mauve) 38%, #ecdcfd 50%,\n var(--mauve) 62%, var(--mauve) 100%);\n background-size: 200% 100%;\n box-shadow: 0 0 6px rgba(203, 166, 247, 0.45);\n animation: bar-flow 2.4s linear infinite;\n transition: width 0.3s ease; }\n @keyframes bar-flow {\n from { background-position: 200% 0; }\n to { background-position: 0% 0; }\n }\n @media (prefers-reduced-motion: reduce) {\n .widget-bar-fill { animation: none; background: var(--mauve); }\n }\n /* Current action \u2014 the terminal's \u21B3 worker trailer, one dim ellipsized line. */\n .widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-variant-numeric: tabular-nums; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);\n border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;\n overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }\n #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n #held-bar { padding-left: calc((100% - 920px) / 2 + 16px); padding-right: calc((100% - 920px) / 2 + 16px); }\n }";
1
+ export declare const STYLES = " :root {\n --base: #1e1e2e; --mantle: #181825; --crust: #11111b;\n --surface0: #313244; --surface1: #45475a; --surface2: #585b70;\n --text: #cdd6f4; --subtext1: #a6adc8; --subtext0: #7f849c;\n --mauve: #cba6f7; --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8;\n --yellow: #f9e2af; --peach: #fab387; --teal: #94e2d5;\n }\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--base); color: var(--text);\n font-family: ui-monospace, monospace;\n /* --app-h is set by setAppHeight (visualViewport.height, falling back to\n window.innerHeight) so the column height is a stable pixel value across\n an orientation change. 100dvh is the fallback for first paint / no-JS. */\n height: var(--app-h, 100dvh);\n display: flex; flex-direction: column; overflow: hidden;\n padding: env(safe-area-inset-top, 0px) env(safe-area-inset-right, 0px)\n 0px env(safe-area-inset-left, 0px);\n }\n #context-bar { height: 4px; background: var(--surface0); flex-shrink: 0; }\n #context-bar-fill { height: 100%; background: var(--mauve); width: 0%; transition: width 0.4s ease; }\n #header {\n background: var(--mantle); padding: 8px 16px;\n display: flex; justify-content: space-between; align-items: center;\n font-size: 13px; flex-shrink: 0; border-bottom: 1px solid var(--surface0);\n }\n #header .title { font-weight: bold; color: var(--mauve); letter-spacing: 0.05em;\n position: relative; animation: glitch 5s steps(1) infinite; }\n @keyframes glitch {\n 0%, 88%, 100% { text-shadow: none; transform: translate(0, 0); }\n 90% { text-shadow: -1px 0 var(--red), 1px 0 var(--teal); transform: translate(1px, -1px); }\n 92% { text-shadow: 1px 0 var(--red), -1px 0 var(--blue); transform: translate(-1px, 1px); }\n 94% { text-shadow: -1px 0 var(--blue), 1px 0 var(--red); transform: translate(1px, 0); }\n 96% { text-shadow: 1px 0 var(--teal), -1px 0 var(--red); transform: translate(-1px, 0); }\n }\n @media (prefers-reduced-motion: reduce) { #header .title { animation: none; } }\n #header .hgroup { display: flex; align-items: center; gap: 10px; }\n #bell {\n background: none; border: none; color: var(--subtext1); cursor: pointer;\n font-size: 15px; line-height: 1; padding: 2px; font-family: inherit;\n }\n #bell:hover { color: var(--text); }\n #bell.on { color: var(--mauve); }\n /* Header status chip: connection dot + model name + context usage. */\n #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }\n #status-dot {\n width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;\n background: var(--surface2); transition: background 0.2s ease;\n }\n #status-dot.idle { background: var(--green); }\n #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }\n #status-dot.disconnected { background: var(--red); }\n @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }\n #status-model { color: var(--subtext1); }\n #status-model:empty { display: none; }\n #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }\n #status-ctx:empty { display: none; }\n /* Notification bell dropdown: a push toggle row + the recent-toast history. */\n #notif-panel {\n display: none; position: fixed; z-index: 80;\n top: calc(env(safe-area-inset-top, 0px) + 42px);\n right: calc(env(safe-area-inset-right, 0px) + 12px);\n width: min(320px, calc(100vw - 24px));\n background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;\n }\n #notif-panel.open { display: block; }\n #notif-toggle-row {\n display: flex; align-items: center; justify-content: space-between;\n padding: 8px 12px; border-bottom: 1px solid var(--surface0);\n }\n #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }\n #notif-toggle {\n background: var(--surface1); color: var(--text); border: none; border-radius: 6px;\n padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;\n }\n #notif-toggle:hover { filter: brightness(1.1); }\n #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }\n #notif-list { max-height: 40dvh; overflow-y: auto; }\n #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }\n .notif-item {\n display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;\n border-bottom: 1px solid var(--surface0); font-size: 12px;\n }\n .notif-item:last-child { border-bottom: none; }\n .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }\n .notif-item.warning .notif-dot { background: var(--peach); }\n .notif-item.error .notif-dot { background: var(--red); }\n .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }\n .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }\n #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }\n #chat-log {\n flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;\n display: flex; flex-direction: column; gap: 8px;\n }\n /* Floating jump-to-latest button \u2014 only shown when scrolled away from the\n bottom (toggled via .visible from the scroll handler). */\n #scroll-bottom {\n display: none; position: absolute; bottom: 16px; right: 16px; z-index: 40;\n width: 36px; height: 36px; border-radius: 50%; cursor: pointer;\n background: var(--surface1); color: var(--text); border: 1px solid var(--surface2);\n font-family: inherit; font-size: 18px; line-height: 1; padding: 0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n #scroll-bottom:hover { background: var(--surface2); color: var(--mauve); }\n #scroll-bottom.visible { display: block; }\n #chat-log::-webkit-scrollbar { width: 6px; }\n #chat-log::-webkit-scrollbar-track { background: transparent; }\n #chat-log::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 3px; }\n .bubble {\n max-width: 82%; padding: 8px 12px; border-radius: 8px;\n line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;\n }\n .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }\n .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }\n .bubble.error {\n background: var(--crust); color: var(--red); align-self: stretch;\n max-width: 100%; border: 1px solid var(--red); font-size: 12px;\n }\n /* Persistent inline system note (e.g. context compaction) \u2014 a muted centered\n divider, distinct from chat bubbles. */\n .sysnote {\n /* publishNote sends multi-line text (the /task-config table); textContent\n keeps the newlines only if the box does not collapse them. */\n white-space: pre-wrap;\n align-self: center; color: var(--subtext0); font-size: 11px;\n font-family: ui-monospace, monospace; letter-spacing: 0.5px;\n padding: 2px 10px; opacity: 0.85;\n }\n .bubble.thinking {\n display: flex; gap: 5px; align-items: center; padding: 10px 14px;\n }\n .bubble.thinking .spinner {\n color: var(--mauve); font-size: 15px; line-height: 1;\n font-family: ui-monospace, monospace;\n }\n /* Collapsed reasoning block (\"\u273B Thinking\u2026 (n lines)\"), muted + italic. */\n .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }\n .thinking-block > summary {\n color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;\n user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;\n }\n .thinking-block > summary::-webkit-details-marker { display: none; }\n .thinking-block .thinking-spin {\n color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;\n }\n .thinking-block .thinking-body {\n color: var(--subtext0); font-style: italic; white-space: pre-wrap;\n word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;\n padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);\n }\n /* Copy buttons: on code-block headers and (floating) on finished assistant\n bubbles. Wired by one delegated click handler in the client script. */\n .copy-btn {\n background: transparent; border: none; color: var(--subtext0); cursor: pointer;\n font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;\n line-height: 1.4;\n }\n .copy-btn:hover { color: var(--text); background: var(--surface1); }\n .copy-btn.copied { color: var(--green); }\n .bubble-copy {\n position: absolute; top: 4px; right: 4px; opacity: 0;\n background: var(--surface1); transition: opacity 0.12s ease;\n }\n .bubble.assistant:hover .bubble-copy { opacity: 1; }\n /* Touch devices have no hover \u2014 keep the button faintly visible. */\n @media (hover: none) { .bubble-copy { opacity: 0.55; } }\n .tool-call {\n background: var(--crust); border-radius: 6px; align-self: flex-start;\n max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);\n }\n .tool-call summary {\n padding: 6px 10px; color: var(--subtext1); cursor: pointer;\n user-select: none; list-style: none;\n display: flex; align-items: center; gap: 8px;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6\"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }\n .tool-call[open] > summary::before { content: \"\u25BC\"; }\n .tool-call.error > summary { color: var(--red); }\n /* The summary is a single line: the label ellipsizes (full text in the title\n tooltip / on expand), badges and timing stay pinned to the right. */\n .tool-label {\n flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: ui-monospace, monospace;\n }\n .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }\n .tool-call pre {\n padding: 8px 12px; overflow-y: auto;\n color: var(--subtext1); font-size: 11px; max-height: 280px;\n border-top: 1px solid var(--surface0);\n white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }\n /* Line diff for edit/write tools (tints derived from --green/--red). */\n .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }\n .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }\n .diff-line { white-space: pre; padding: 0 10px; }\n .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }\n .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }\n .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }\n .diff-ctx { color: var(--subtext1); }\n .code-block {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 6px; overflow: hidden; margin: 4px 0;\n align-self: stretch; max-width: 100%; font-size: 12px;\n }\n /* Header row above a code block: language label on the left, copy button on\n the right, on its own surface bar. */\n .code-head {\n display: flex; align-items: center; justify-content: space-between;\n background: var(--surface0);\n }\n .code-lang {\n color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\n .code-head .copy-btn { padding: 3px 10px; }\n .code-block code {\n display: block; padding: 10px 12px; overflow-x: auto;\n color: var(--text); white-space: pre; line-height: 1.55;\n }\n .hl-kw { color: var(--mauve); }\n .hl-str { color: var(--green); }\n .hl-cmt { color: var(--subtext0); font-style: italic; }\n .hl-num { color: var(--blue); }\n .hl-fn { color: var(--yellow); }\n /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation\n panel (both get the .md class from setContent). Block elements lay themselves\n out, so switch off the container's pre-wrap for these. */\n .md { white-space: normal; }\n .md > :first-child { margin-top: 0; }\n .md > :last-child { margin-bottom: 0; }\n .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }\n .md .md-h1 { font-size: 1.35em; }\n .md .md-h2 { font-size: 1.2em; }\n .md .md-h3 { font-size: 1.08em; }\n .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }\n .md .md-p { margin: 6px 0; }\n .md .md-list { margin: 6px 0; padding-left: 22px; }\n .md .md-list li { margin: 2px 0; }\n .md .md-task { list-style: none; margin-left: -22px; }\n .md .md-check { color: var(--green); }\n .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }\n .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }\n .md a { color: var(--blue); text-decoration: underline; }\n .md strong { color: var(--text); font-weight: 700; }\n .md em { font-style: italic; }\n .md del { color: var(--subtext0); }\n .md .md-code {\n background: var(--crust); border: 1px solid var(--surface0);\n border-radius: 4px; padding: 1px 4px; font-size: 0.92em;\n }\n .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }\n .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }\n .md .md-table th { background: var(--surface0); color: var(--subtext1); }\n #input-bar {\n background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));\n display: flex; gap: 8px; flex-shrink: 0;\n border-top: 1px solid var(--surface0);\n position: relative;\n }\n /* Held mid-run input: what you typed is waiting for the next task turn.\n In normal flow, NOT absolutely positioned \u2014 as an overlay it would cover\n the task widget's progress row above the composer. */\n #held-bar {\n display: flex; align-items: center; gap: 8px; flex-shrink: 0;\n background: var(--mantle); border-top: 1px solid var(--surface1);\n padding: 6px 16px; font-size: 12px;\n }\n #held-label { color: var(--yellow); white-space: nowrap; }\n #held-text {\n color: var(--subtext0); overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; flex: 1;\n }\n #held-clear {\n background: none; border: none; color: var(--subtext0);\n cursor: pointer; font-size: 13px; padding: 0 2px;\n }\n #held-clear:hover { color: var(--red); }\n #cmd-suggestions {\n display: none; position: absolute; bottom: 100%; left: 16px; right: 16px;\n background: var(--mantle); border: 1px solid var(--surface1);\n border-bottom: none; border-radius: 8px 8px 0 0;\n overflow: hidden; z-index: 10;\n }\n .cmd-item {\n display: flex; align-items: baseline; gap: 10px;\n padding: 7px 12px; cursor: pointer; font-size: 12px;\n border-bottom: 1px solid var(--surface0);\n }\n .cmd-item:last-child { border-bottom: none; }\n .cmd-item:hover, .cmd-item.active { background: var(--surface0); }\n .cmd-item .cmd-name { color: var(--blue); font-weight: bold; flex-shrink: 0; }\n .cmd-item .cmd-desc { color: var(--subtext0); }\n #input {\n flex: 1; background: var(--surface0); color: var(--text);\n border: none; border-radius: 6px; padding: 8px 12px;\n font-family: inherit; font-size: 13px; resize: none;\n outline: none; line-height: 1.5; min-height: 36px; max-height: 120px;\n }\n #input::placeholder { color: var(--subtext0); }\n #input:focus { box-shadow: 0 0 0 1px var(--mauve); }\n #send {\n background: var(--blue); color: var(--crust); border: none;\n border-radius: 6px; padding: 8px 16px; font-weight: bold;\n cursor: pointer; font-size: 13px; font-family: inherit;\n white-space: nowrap; align-self: flex-end;\n }\n #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }\n /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)\n state brightens it and adds a halo, mirroring the prompt card's cancel. */\n #send.stop { background: var(--red); color: var(--crust); }\n #send.stop.armed {\n filter: brightness(1.12);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);\n }\n #reconnect-overlay {\n display: none; position: fixed; inset: 0;\n background: rgba(30,30,46,0.88); color: var(--subtext1);\n justify-content: center; align-items: center;\n font-size: 13px; z-index: 100; letter-spacing: 0.03em;\n }\n #reconnect-overlay.visible { display: flex; }\n /* Trailing stream indicator: the same braille spinner as the thinking bubble\n (one ticker paints every .spin), inline at the end of the streaming text. */\n .cursor {\n color: var(--mauve); margin-left: 2px;\n font-family: ui-monospace, monospace;\n }\n #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);\n color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }\n /* Structured task widget (progress bar + phase badge + elapsed). Replaces the\n plain text lines when the server sends a structured data payload. */\n #status-panel.structured { white-space: normal; display: block; }\n /* Title gets its own line and wraps, clamped to 2 \u2014 never squeezed to a stub\n by the meta row beside it on a narrow screen. */\n .widget-title { display: -webkit-box; -webkit-box-orient: vertical;\n -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; color: var(--text);\n font-size: 13px; line-height: 1.3; word-break: break-word; }\n .widget-meta { display: flex; align-items: center; gap: 8px; margin-top: 5px; }\n .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);\n border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;\n text-transform: uppercase; }\n .widget-step { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-elapsed { flex-shrink: 0; margin-left: auto; color: var(--subtext0);\n font-size: 11px; font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n /* Fill carries a soft highlight band that sweeps left\u2192right through the mauve,\n so the bar reads as \"working\" even while the step count holds still. The\n gradient tile is 2x the fill width and shifts one full period per cycle,\n so the loop is seamless. Base color stays mauve for reduced-motion. */\n .widget-bar-fill { height: 100%; border-radius: 2px;\n background: linear-gradient(90deg,\n var(--mauve) 0%, var(--mauve) 38%, #ecdcfd 50%,\n var(--mauve) 62%, var(--mauve) 100%);\n background-size: 200% 100%;\n box-shadow: 0 0 6px rgba(203, 166, 247, 0.45);\n animation: bar-flow 2.4s linear infinite;\n transition: width 0.3s ease; }\n @keyframes bar-flow {\n from { background-position: 200% 0; }\n to { background-position: 0% 0; }\n }\n @media (prefers-reduced-motion: reduce) {\n .widget-bar-fill { animation: none; background: var(--mauve); }\n }\n /* Current action \u2014 the terminal's \u21B3 worker trailer, one dim ellipsized line. */\n .widget-action { margin-top: 6px; color: var(--subtext0); font-size: 11px;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-variant-numeric: tabular-nums; }\n /* Dim per-turn timestamp shown under a committed turn's bubble. */\n .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;\n margin-top: -2px; }\n .turn-time.user { align-self: flex-end; }\n .turn-time.assistant, .turn-time.system { align-self: flex-start; }\n .turn-time.system { align-self: center; }\n #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);\n border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));\n display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }\n #prompt-card .q-label { color: var(--mauve); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }\n #prompt-card .q { color: var(--text); margin-bottom: 12px; white-space: pre-wrap;\n font-size: 15px; line-height: 1.5; }\n #prompt-card .rec-panel { background: var(--surface0); border-left: 3px solid var(--green);\n border-radius: 6px; padding: 10px 12px; margin-bottom: 12px; }\n #prompt-card .rec-label { color: var(--green); font-size: 11px; font-weight: 700;\n text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }\n #prompt-card .rec-text { color: var(--text); font-size: 15px; line-height: 1.5;\n white-space: pre-wrap; overflow-wrap: anywhere; }\n #prompt-card textarea { width: 100%; background: var(--surface0); color: var(--text);\n border: 1px solid var(--surface2); border-radius: 6px; padding: 10px; font-size: 15px;\n font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }\n #prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;\n flex-wrap: wrap; }\n /* Recommendation answers can be long sentences, so stack them as a readable list. */\n #prompt-card .row.stacked { flex-direction: column; align-items: stretch; }\n #prompt-card .row.stacked button { flex: none; text-align: left; }\n #prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }\n #prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;\n font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }\n #prompt-card button:hover { filter: brightness(1.08); }\n #prompt-card button.primary { background: var(--green); color: var(--crust);\n font-weight: 700; flex: 1; min-width: 160px; }\n #prompt-card button.secondary { background: var(--surface1); color: var(--text);\n flex: 1; min-width: 160px; }\n #prompt-card button.cancel { margin-left: auto; align-self: center; background: transparent;\n color: var(--subtext0); font-size: 12px; font-weight: 500; padding: 8px 10px; }\n #prompt-card button.cancel:hover { color: var(--red); filter: none; }\n #prompt-card button.cancel.armed { background: var(--red); color: var(--crust); font-weight: 700; }\n .toast { position: fixed; top: calc(env(safe-area-inset-top, 0px) + 12px);\n right: calc(env(safe-area-inset-right, 0px) + 12px); max-width: calc(100vw - 24px);\n padding: 8px 12px; border-radius: 6px; overflow-wrap: anywhere; word-break: break-word;\n background: var(--surface1); color: var(--text); z-index: 60; }\n .toast.warning { background: var(--peach); color: var(--crust); }\n .toast.error { background: var(--red); color: var(--crust); }\n /* Desktop: center the transcript/status/input in a readable column instead of\n hugging the left edge. The scrollbar stays at the true window edge; only the\n content is inset. Mobile (below 960px) is unchanged. */\n @media (min-width: 960px) {\n #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }\n #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }\n #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }\n #held-bar { padding-left: calc((100% - 920px) / 2 + 16px); padding-right: calc((100% - 920px) / 2 + 16px); }\n }";
@@ -121,6 +121,9 @@ export const STYLES = ` :root {
121
121
  /* Persistent inline system note (e.g. context compaction) — a muted centered
122
122
  divider, distinct from chat bubbles. */
123
123
  .sysnote {
124
+ /* publishNote sends multi-line text (the /task-config table); textContent
125
+ keeps the newlines only if the box does not collapse them. */
126
+ white-space: pre-wrap;
124
127
  align-self: center; color: var(--subtext0); font-size: 11px;
125
128
  font-family: ui-monospace, monospace; letter-spacing: 0.5px;
126
129
  padding: 2px 10px; opacity: 0.85;
@@ -414,10 +417,6 @@ export const STYLES = ` :root {
414
417
  background: var(--surface1); color: var(--text); z-index: 60; }
415
418
  .toast.warning { background: var(--peach); color: var(--crust); }
416
419
  .toast.error { background: var(--red); color: var(--crust); }
417
- #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);
418
- border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;
419
- overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }
420
- #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
421
420
  /* Desktop: center the transcript/status/input in a readable column instead of
422
421
  hugging the left edge. The scrollbar stays at the true window edge; only the
423
422
  content is inset. Mobile (below 960px) is unchanged. */
package/dist/remote/ui.js CHANGED
@@ -79,7 +79,6 @@ ${STYLES}
79
79
  <textarea id="prompt-input" rows="3" placeholder="Type your answer…" style="display:none"></textarea>
80
80
  <div class="row" id="prompt-buttons"></div>
81
81
  </div>
82
- <div id="viewer"><span class="close" id="viewer-close">&#x2715;</span><div id="viewer-body"></div></div>
83
82
  <script>
84
83
  ${renderModule()}
85
84
  ${highlightModule()}
@@ -26,7 +26,7 @@ import { isFatalChildCause, prependHint, USER_CANCELLED } from './child-runner.j
26
26
  import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from './cancel-points.js';
27
27
  import { withRun, announceTerminal } from './run-bracket.js';
28
28
  import { refineExistingFilesBlock, SINGLE_READ_EXTENSION_PATH } from './phases.js';
29
- import { SessionUI, registerBridgeCommand, publishLifecycleNotice } from '../remote/bridge.js';
29
+ import { SessionUI, registerBridgeCommand, publishLifecycleNotice, notifyBoth, notifyRun, isRemoteOrigin } from '../remote/bridge.js';
30
30
  import { getParentContextWindow } from './context-usage.js';
31
31
  import { ChildStatus, runPlanningChild, statusCallbacks } from './child-status.js';
32
32
  import { buildGateDeps, collectTreeChanges } from './gate-deps.js';
@@ -421,7 +421,7 @@ async function schedulePendingRepairs(cwd, id, afterIndex, ctx, deps) {
421
421
  titles.splice(at + 1, 0, title);
422
422
  at += 1;
423
423
  await deps.record?.(cwd, id, `plan: inserted scoped repair step after step ${afterIndex + 1} — ${title}`);
424
- ctx.ui.notify(`${id}: queued a scoped repair for ${repair.file} (${repair.owner}'s file — root cause of ${repair.blamed.join(', ')}).`, 'warning');
424
+ notifyRun(ctx, `${id}: queued a scoped repair for ${repair.file} (${repair.owner}'s file — root cause of ${repair.blamed.join(', ')}).`, 'warning');
425
425
  }
426
426
  }
427
427
  catch {
@@ -623,7 +623,7 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
623
623
  }
624
624
  }
625
625
  if (transcript.length === 0) {
626
- ctx.ui.notify('No clarifying questions needed — planning tasks…', 'info');
626
+ notifyBoth(ctx, 'No clarifying questions needed — planning tasks…', 'info');
627
627
  }
628
628
  return transcript.forRecord();
629
629
  }
@@ -844,7 +844,7 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
844
844
  // mode (bare verdict, indistinguishable from a real one). The plan still
845
845
  // ships — the floor never rejects on count — but never silently.
846
846
  if (best.suspect) {
847
- ctx.ui.notify(`/task-auto: only ${best.plan.titles.length} task(s) planned for a large spec`
847
+ notifyRun(ctx, `/task-auto: only ${best.plan.titles.length} task(s) planned for a large spec`
848
848
  + ' and the regeneration did not grow the list — review the plan before running.', 'warning');
849
849
  }
850
850
  break;
@@ -889,7 +889,7 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
889
889
  if (unresolvedMissing !== null) {
890
890
  logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
891
891
  + unresolvedMissing.join('; ').slice(0, 300));
892
- ctx.ui.notify(`/task-auto: no task fully owns — ${unresolvedMissing.join('; ').slice(0, 200)}. `
892
+ notifyRun(ctx, `/task-auto: no task fully owns — ${unresolvedMissing.join('; ').slice(0, 200)}. `
893
893
  + 'Carried into every task via .pi-tasks/requirements.md, but not as a dedicated '
894
894
  + 'task. To give it one, stop now and add it to the plan in .pi-tasks/; otherwise '
895
895
  + 'it proceeds.', 'warning');
@@ -966,7 +966,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
966
966
  carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : '',
967
967
  carriedDangling.length > 0 ? `${carriedDangling.length} dangling-artifact` : ''
968
968
  ].filter(p => p.length > 0);
969
- ctx.ui.notify(`/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
969
+ notifyRun(ctx, `/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
970
970
  + ' — see .pi-tasks/requirements.md.', 'info');
971
971
  }
972
972
  // Thread the feature's spec doc(s) into every title so each per-task
@@ -1191,7 +1191,7 @@ async function reportStashDrift(active, deps, cwd, id, title, before) {
1191
1191
  const after = await deps.stashRef(cwd);
1192
1192
  if (after === before)
1193
1193
  return;
1194
- active.ui.notify(`${id}: the git stash stack changed during "${title}" and was left that way — `
1194
+ notifyRun(active, `${id}: the git stash stack changed during "${title}" and was left that way — `
1195
1195
  + 'inspect `git stash list`; an orphan stash later pops as an unresolvable conflict.', 'warning');
1196
1196
  }
1197
1197
  catch {
@@ -1269,8 +1269,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1269
1269
  // never learns how far the run has gotten — mirror it into the session
1270
1270
  // view too (transient toast, same as announceDone's info path).
1271
1271
  const progressMsg = `${id}: task ${next.index + 1}/${entries.length} — ${next.title}`;
1272
- active.ui.notify(progressMsg, 'info');
1273
- publishLifecycleNotice(progressMsg, 'info');
1272
+ notifyBoth(active, progressMsg, 'info');
1274
1273
  // If this entry already has a stamped inner id, it was started in a
1275
1274
  // previous (interrupted) run — resume it from its saved phase rather
1276
1275
  // than spawning a fresh task. But the stamped inner file can be gone
@@ -1298,7 +1297,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1298
1297
  // half. Only the success path is announced to keep the common no-op quiet.
1299
1298
  const checkpoint = await deps.commit(cwd, `chore: checkpoint before "${next.title}"`);
1300
1299
  if (checkpoint.committed) {
1301
- active.ui.notify(`${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
1300
+ notifyRun(active, `${id}: checkpointed uncommitted work before "${next.title}".`, 'info');
1302
1301
  }
1303
1302
  // Stash ref before the task: compared after the gates so a stash pushed
1304
1303
  // during the task (impl model or any child) and left behind is called
@@ -1444,8 +1443,11 @@ async function handleTaskAuto(args, ctx) {
1444
1443
  const cwd = ctx.cwd;
1445
1444
  const raw = args.trim();
1446
1445
  if (raw.length === 0) {
1447
- ctx.ui.setEditorText('/task-auto ');
1448
- ctx.ui.notify('Describe the feature after /task-auto (use @ for file completion).', 'info');
1446
+ // Prefilling the composer answers a LOCAL user. From the browser it would
1447
+ // type into a terminal nobody is sitting at; the notice is the answer there.
1448
+ if (!isRemoteOrigin(ctx))
1449
+ ctx.ui.setEditorText('/task-auto ');
1450
+ notifyBoth(ctx, 'Describe the feature after /task-auto (use @ for file completion).', 'info');
1449
1451
  return;
1450
1452
  }
1451
1453
  autoRunning = true;
@@ -1506,11 +1508,13 @@ async function handleTaskAutoResume(args, ctx) {
1506
1508
  : await findResumableAutoDetailed(cwd);
1507
1509
  const candidate = eligible ?? (unattended ? await findResumableAutoDetailed(cwd) : null);
1508
1510
  const decision = decideResume(candidate, Date.now(), unattended);
1509
- ctx.ui.notify(decision.banner, decision.level);
1510
1511
  // An unattended refusal happens with nobody watching the terminal — the
1511
- // remote view is the only surface that will still be there in the morning.
1512
+ // remote view is the only surface that will still be there in the morning,
1513
+ // which is why it is a run notice and not a toast.
1512
1514
  if (unattended)
1513
- publishLifecycleNotice(decision.banner, decision.level);
1515
+ notifyRun(ctx, decision.banner, decision.level);
1516
+ else
1517
+ notifyBoth(ctx, decision.banner, decision.level);
1514
1518
  if (!decision.resume || !candidate)
1515
1519
  return;
1516
1520
  const id = candidate.id;
@@ -1543,10 +1547,12 @@ async function handleTaskAutoResume(args, ctx) {
1543
1547
  async function handleTaskAutoCancel(_args, ctx) {
1544
1548
  if (!autoRunning) {
1545
1549
  ctx.ui.notify('No /task-auto loop is running.', 'info');
1550
+ publishLifecycleNotice('No /task-auto loop is running.', 'info');
1546
1551
  return;
1547
1552
  }
1548
1553
  requestAutoCancel();
1549
1554
  ctx.ui.notify(CANCEL_ACK, 'warning');
1555
+ publishLifecycleNotice(CANCEL_ACK, 'warning');
1550
1556
  }
1551
1557
  /**
1552
1558
  * What the user is told the moment the request lands. Deliberately does NOT
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { updateTaskFrontMatter } from './task-io.js';
6
6
  import { flashTerminalWidget } from './widget.js';
7
- import { publishLifecycleNotice } from '../remote/bridge.js';
7
+ import { notifyRun } from '../remote/bridge.js';
8
8
  import { ChildFailureError, USER_CANCELLED } from './child-runner.js';
9
9
  import { streamStallCause } from '../shared/stream-watchdog.js';
10
10
  // ─── Classifier ──────────────────────────────────────────────────────────────
@@ -80,9 +80,8 @@ export async function handleFailure(err, ctx, cwd, id, aborted) {
80
80
  const c = classifyFailure(err, aborted);
81
81
  await updateTaskFrontMatter(cwd, id, { state: c.state, reason: c.reason });
82
82
  flashTerminalWidget(ctx, c.state, id, c.flash);
83
- ctx.ui.notify(`${id} ${c.notify}`, c.level);
84
- // Mirror to remote viewers. `ctx.ui.notify` reaches the terminal UI only, so
85
- // without this call the remote view shows nothing when a task fails.
86
- publishLifecycleNotice(`${id} ${c.notify}`, c.level);
83
+ // How the run ENDED. Durable on the remote: a toast is gone before an absent
84
+ // user looks, and this is the one line that says the task did not finish.
85
+ notifyRun(ctx, `${id} ${c.notify}`, c.level);
87
86
  return c;
88
87
  }
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import { formatLoopHint } from './loop-detector.js';
28
28
  import { classifyEnforceChildFailure } from './enforce-guidelines.js';
29
+ import { notifyRun } from '../remote/bridge.js';
29
30
  /**
30
31
  * What each kind may do. Adding a child is a row; it cannot be added without
31
32
  * deciding all four questions, which is the point.
@@ -145,7 +146,7 @@ export function makeGateChild(deps) {
145
146
  log(`=== ${deps.kind} GIT-STATE GUARD — ${label}; `
146
147
  + `restored: ${rec.actions.join('; ')} ===`);
147
148
  if (rec.verdictTainted) {
148
- deps.ctx.ui.notify(`${deps.taskTitle}: ${deps.kind} child mutated repo state — `
149
+ notifyRun(deps.ctx, `${deps.taskTitle}: ${deps.kind} child mutated repo state — `
149
150
  + `restored (${rec.actions.join('; ').slice(0, 140)}).`, 'warning');
150
151
  }
151
152
  }
@@ -156,7 +157,7 @@ export function makeGateChild(deps) {
156
157
  // only thing that can block.
157
158
  if (r.loopHit) {
158
159
  log(`=== ${deps.kind} LOOP WARNING — ${formatLoopHint(r.loopHit)} ===`);
159
- deps.ctx.ui.notify(`${deps.taskTitle}: ${deps.kind} worker looped past the nudges — `
160
+ notifyRun(deps.ctx, `${deps.taskTitle}: ${deps.kind} worker looped past the nudges — `
160
161
  + 'continuing (not blocked).', 'warning');
161
162
  }
162
163
  const failure = classifyEnforceChildFailure(r);
@@ -27,7 +27,7 @@ import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSecti
27
27
  import { startWidget } from './widget.js';
28
28
  import { setupImplWidget } from './impl-widget.js';
29
29
  import { enterImplementationTurn } from './implementation-scope.js';
30
- import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
30
+ import { SessionUI, publishNotify, registerBridgeCommand, getBridge, notifyBoth, isRemoteOrigin } from '../remote/bridge.js';
31
31
  import { pushNotify } from '../remote/push.js';
32
32
  import { getConfig } from '../config/config.js';
33
33
  import { gateDebugWriter } from './debug-log.js';
@@ -681,8 +681,11 @@ async function handleTask(args, ctx) {
681
681
  const cwd = ctx.cwd;
682
682
  const raw = args.trim();
683
683
  if (raw.length === 0) {
684
- ctx.ui.setEditorText('/task ');
685
- ctx.ui.notify('Type your prompt after /task (use @ for file completion).', 'info');
684
+ // Prefilling the composer answers a LOCAL user. From the browser it would
685
+ // type into a terminal nobody is sitting at; the notice is the answer there.
686
+ if (!isRemoteOrigin(ctx))
687
+ ctx.ui.setEditorText('/task ');
688
+ notifyBoth(ctx, 'Type your prompt after /task (use @ for file completion).', 'info');
686
689
  return;
687
690
  }
688
691
  // When a gate is enabled, /task awaits the implementation and runs the same
@@ -696,7 +699,7 @@ async function handleTask(args, ctx) {
696
699
  }
697
700
  const { end } = await runSingleTask(ctx, cwd, raw, { notifyFinish: true });
698
701
  if (end.kind === 'no-session') {
699
- ctx.ui.notify('Could not start a fresh session for /task.', 'warning');
702
+ notifyBoth(ctx, 'Could not start a fresh session for /task.', 'warning');
700
703
  }
701
704
  }
702
705
  async function handleTaskList(_args, ctx) {
@@ -729,8 +732,12 @@ async function handleTaskList(_args, ctx) {
729
732
  if (lines.length === 0)
730
733
  lines.push('(no tasks in .pi-tasks/)');
731
734
  lines.push('', 'resume: /task-resume <id> (eligible: in_progress, pending, cancelled, failed)');
732
- publishViewer('Tasks', lines.join('\n'));
733
- await ctx.ui.editor('Tasks', lines.join('\n'));
735
+ await new SessionUI(ctx).show({
736
+ localTitle: 'Tasks',
737
+ localText: lines.join('\n'),
738
+ question: 'Tasks',
739
+ body: lines.join('\n')
740
+ });
734
741
  }
735
742
  async function handleTaskResume(args, ctx) {
736
743
  await ctx.waitForIdle();
@@ -742,8 +749,7 @@ async function handleTaskResume(args, ctx) {
742
749
  await fsp.access(taskFilePath(cwd, id));
743
750
  }
744
751
  catch {
745
- ctx.ui.notify(`${id} not found in .pi-tasks/`, 'error');
746
- publishNotify(`${id} not found in .pi-tasks/`, 'error');
752
+ notifyBoth(ctx, `${id} not found in .pi-tasks/`, 'error');
747
753
  return;
748
754
  }
749
755
  }
@@ -771,8 +777,7 @@ async function handleTaskResume(args, ctx) {
771
777
  }
772
778
  candidates.sort((a, b) => b.mtime - a.mtime);
773
779
  if (candidates.length === 0) {
774
- ctx.ui.notify('No resumable tasks.', 'info');
775
- publishNotify('No resumable tasks.', 'info');
780
+ notifyBoth(ctx, 'No resumable tasks.', 'info');
776
781
  return;
777
782
  }
778
783
  id = candidates[0].id;
@@ -785,19 +790,17 @@ async function handleTaskResume(args, ctx) {
785
790
  }
786
791
  const { end } = await runSingleTask(ctx, cwd, '', { resumeId: id, notifyFinish: true });
787
792
  if (end.kind === 'no-session') {
788
- ctx.ui.notify('Could not start a fresh session for /task-resume.', 'warning');
793
+ notifyBoth(ctx, 'Could not start a fresh session for /task-resume.', 'warning');
789
794
  }
790
795
  }
791
796
  // eslint-disable-next-line @typescript-eslint/require-await
792
797
  async function handleTaskCancel(_args, ctx) {
793
798
  if (!activeTask) {
794
- ctx.ui.notify('No task is running.', 'info');
795
- publishNotify('No task is running.', 'info');
799
+ notifyBoth(ctx, 'No task is running.', 'info');
796
800
  return;
797
801
  }
798
802
  activeTask.cancel();
799
- ctx.ui.notify(`Cancelling ${activeTask.taskId}…`, 'warning');
800
- publishNotify(`Cancelling ${activeTask.taskId}…`, 'warning');
803
+ notifyBoth(ctx, `Cancelling ${activeTask.taskId}…`, 'warning');
801
804
  }
802
805
  // ─── Entry point ─────────────────────────────────────────────────────────────
803
806
  export function registerTask(pi) {
@@ -40,7 +40,7 @@ import { detachUnsatisfiableRequirements, claimPendingRequirements, unclaimedPen
40
40
  import { trackedSourceOracle } from './owned-freeze-conflict.js';
41
41
  import { groupArgsForChild, runPhaseChild, runWithEmphasisRetry, prependHint, USER_CANCELLED, ChildFailureError, isFatalChildCause } from './child-runner.js';
42
42
  import { runResearchWorker, researchWorkerCacheHeading } from './research-worker.js';
43
- import { SessionUI } from '../remote/bridge.js';
43
+ import { SessionUI, notifyRun } from '../remote/bridge.js';
44
44
  import { isYoloMode, yoloPickAutoAnswer } from './yolo.js';
45
45
  import { QaTranscript, GRILL_QA_POLICY } from './qa-transcript.js';
46
46
  // ─── Re-export constants from their home modules ────────────────────────────
@@ -1113,7 +1113,7 @@ export async function critiqueWithFallback(d, p) {
1113
1113
  // it is the validated-shape fallback, and failing the run costs more.
1114
1114
  if (parseVerifyBlock(p.spec) === null)
1115
1115
  throw err;
1116
- p.ctx.ui.notify(msg === 'verify_grep_theater' ?
1116
+ notifyRun(p.ctx, msg === 'verify_grep_theater' ?
1117
1117
  'Critique rewrite kept a grep-only VERIFY — using compose draft. Consider adding a command that RUNS the deliverable.'
1118
1118
  : "Critique couldn't produce a VERIFY block — using compose draft. Edit the spec manually if needed.", 'warning');
1119
1119
  return p.spec;
@@ -34,7 +34,7 @@ import { deriveTitle } from './parsers.js';
34
34
  import { renderInlineMarkdown } from './inline-markdown.js';
35
35
  import { expandFeatureMentions } from './auto-orchestrator.js';
36
36
  import { runSingleTask, runGatedTask } from './orchestrator.js';
37
- import { SessionUI, publishViewer, publishLifecycleNotice, registerBridgeCommand } from '../remote/bridge.js';
37
+ import { SessionUI, registerBridgeCommand, notifyBoth, notifyRun, isRemoteOrigin } from '../remote/bridge.js';
38
38
  import { withRun, announceTerminal } from './run-bracket.js';
39
39
  import { getConfig } from '../config/config.js';
40
40
  import { isYoloMode } from './yolo.js';
@@ -121,9 +121,12 @@ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
121
121
  allowSkip: true
122
122
  }),
123
123
  showAnswer: async (question, answer) => {
124
- const text = `You asked:\n${question}\n\n${answer}\n`;
125
- publishViewer(ASK_TITLE, text);
126
- await ctx.ui.editor(ASK_TITLE, text);
124
+ await ui.show({
125
+ localTitle: ASK_TITLE,
126
+ localText: `You asked:\n${question}\n\n${answer}\n`,
127
+ question: `You asked: ${question}`,
128
+ body: answer
129
+ });
127
130
  },
128
131
  onEntries: async (entries) => {
129
132
  await persistEntries(cwd, planId, entries);
@@ -147,13 +150,7 @@ export function buildPlanDeps(ctx, cwd, planId, task, signal) {
147
150
  async function reportReadOnlyViolation(ctx, cwd, planId, step, touched, logDebug) {
148
151
  const line = formatReadOnlyViolation(step, touched);
149
152
  logDebug?.(line);
150
- try {
151
- ctx.ui.notify(line, 'error');
152
- }
153
- catch {
154
- /* stale ctx — the record below is what matters */
155
- }
156
- publishLifecycleNotice(line, 'error');
153
+ notifyRun(ctx, line, 'error');
157
154
  try {
158
155
  const existing = (await readSection(cwd, planId, 'read-only violations')) ?? '';
159
156
  await setTaskSection(cwd, planId, 'read-only violations', existing ? `${existing}\n- ${line}` : `- ${line}`);
@@ -193,7 +190,7 @@ async function defaultHandoff(ctx, cwd, prompt) {
193
190
  }
194
191
  const { taskId, end } = await runSingleTask(ctx, cwd, prompt, { notifyFinish: true });
195
192
  if (end.kind === 'no-session') {
196
- ctx.ui.notify('Could not start a fresh session for /task-plan.', 'warning');
193
+ notifyBoth(ctx, 'Could not start a fresh session for /task-plan.', 'warning');
197
194
  return undefined;
198
195
  }
199
196
  return taskId || undefined;
@@ -239,8 +236,11 @@ export async function handleTaskPlan(args, ctx, commandDeps = DEFAULT_COMMAND_DE
239
236
  const cwd = ctx.cwd;
240
237
  const raw = args.trim();
241
238
  if (raw.length === 0) {
242
- ctx.ui.setEditorText('/task-plan ');
243
- ctx.ui.notify('Describe the task after /task-plan (use @ for file completion).', 'info');
239
+ // Prefilling the composer answers a LOCAL user. From the browser it would
240
+ // type into a terminal nobody is sitting at; the notice is the answer there.
241
+ if (!isRemoteOrigin(ctx))
242
+ ctx.ui.setEditorText('/task-plan ');
243
+ notifyBoth(ctx, 'Describe the task after /task-plan (use @ for file completion).', 'info');
244
244
  return;
245
245
  }
246
246
  // Inline any @file the user referenced, exactly as /task-auto's planner does:
@@ -1,7 +1,7 @@
1
1
  import { AutofixLedger } from './autofix-ledger.js';
2
2
  import { describeDebt, recordDebt } from './accept-debt.js';
3
3
  import { cancelCheckpoint } from './cancel-points.js';
4
- import { SessionUI } from '../remote/bridge.js';
4
+ import { SessionUI, notifyBoth, notifyRun } from '../remote/bridge.js';
5
5
  import { isYoloMode, yoloFinalGateChoice, YOLO_STAMP } from './yolo.js';
6
6
  import { ignoredWriteTrailLine, ignoredWriteDebtReason } from './write-guard.js';
7
7
  import { readOwnedRequirements } from './requirements.js';
@@ -76,7 +76,7 @@ export async function runFinalGateStage(active, deps, p) {
76
76
  // Set when the gate finished having observed NOTHING dynamic. Declared out here so
77
77
  // the run-completion announcement can say so.
78
78
  let unobservedNote = null;
79
- active.ui.notify(`${id}: running final integration gate…`, 'info');
79
+ notifyBoth(active, `${id}: running final integration gate…`, 'info');
80
80
  // Run-level gate trail on the parent task file — same durable auditability
81
81
  // contract as the per-task `## gates` records.
82
82
  const recGate = async (line) => {
@@ -111,7 +111,7 @@ export async function runFinalGateStage(active, deps, p) {
111
111
  unobservedNote = fin.unobserved;
112
112
  await recGate(`final-gate: UNOBSERVED — ${fin.reason.slice(0, 300)}`);
113
113
  await carryDebt(fin.unobserved);
114
- active.ui.notify(`${id}: the final integration gate observed NOTHING dynamic — `
114
+ notifyRun(active, `${id}: the final integration gate observed NOTHING dynamic — `
115
115
  + 'the run completed on static checks alone. Nothing verified '
116
116
  + 'that the assembled product builds, boots or works.', 'warning');
117
117
  }
@@ -132,7 +132,7 @@ export async function runFinalGateStage(active, deps, p) {
132
132
  for (const d of debts) {
133
133
  await recGate(`defect STILL OPEN — ${d.taskId || '(unknown task)'}: ${describeDebt(d)}: ${d.reason.slice(0, 240)}${d.conflict ? ` [CONFLICTING CLAIM — ${d.conflict}]` : ''}`);
134
134
  }
135
- active.ui.notify(`${id}: ${debts.length} recorded verify-FAIL defect(s) are STILL unresolved at run end — see the gate trail.`, 'warning');
135
+ notifyRun(active, `${id}: ${debts.length} recorded verify-FAIL defect(s) are STILL unresolved at run end — see the gate trail.`, 'warning');
136
136
  };
137
137
  // What was REPORTED, so a post-autofix re-derivation can be compared against it
138
138
  // rather than blindly re-printed.
@@ -149,7 +149,7 @@ export async function runFinalGateStage(active, deps, p) {
149
149
  + ` ${(o.pending ?? []).join(', ')}]`);
150
150
  }
151
151
  if (unclaimed.length > 0) {
152
- active.ui.notify(`${id}: ${unclaimed.length} authoritative design requirement(s) ended the run`
152
+ notifyRun(active, `${id}: ${unclaimed.length} authoritative design requirement(s) ended the run`
153
153
  + ' owned by NO task — the task they were mapped to could not touch the'
154
154
  + ' file, and nothing else claimed it. See the gate trail.', 'warning');
155
155
  }
@@ -315,7 +315,7 @@ export async function runFinalGateStage(active, deps, p) {
315
315
  // commit — the ACCEPT is a decision about the FAILING gate, never an
316
316
  // instruction to throw away work.
317
317
  await commitStranded('accepted');
318
- active.ui.notify(`${id}: final integration gate FAIL accepted by user — completing.`
318
+ notifyRun(active, `${id}: final integration gate FAIL accepted by user — completing.`
319
319
  + (ledger.stranded().length > 0 ?
320
320
  ` ${ledger.stranded().length} uncommitted fix-pass change(s) committed separately.`
321
321
  : ''), 'warning');
@@ -324,7 +324,7 @@ export async function runFinalGateStage(active, deps, p) {
324
324
  if (choice.action === 'autofix' && canAutofix) {
325
325
  const attempt = ledger.attempt();
326
326
  await recGate(`final-gate: user chose AUTOFIX (attempt ${attempt}/${MAX_FINAL_GATE_AUTOFIX})`);
327
- active.ui.notify(`${id}: final-gate autofix (${attempt}/${MAX_FINAL_GATE_AUTOFIX}) — bounded fix pass, then the gate re-runs…`, 'info');
327
+ notifyRun(active, `${id}: final-gate autofix (${attempt}/${MAX_FINAL_GATE_AUTOFIX}) — bounded fix pass, then the gate re-runs…`, 'info');
328
328
  const seed = choice.guidance ? `${fin.reason}\n\nUser guidance: ${choice.guidance}` : fin.reason;
329
329
  const fix = await deps.finalGateFix(active, cwd, seed, [...ledger.ignoredWrites()]);
330
330
  // IGNORED-PATH WRITES. The pass wrote file(s) git ignores, so
@@ -355,7 +355,7 @@ export async function runFinalGateStage(active, deps, p) {
355
355
  await carryDebt(fix.unobserved);
356
356
  }
357
357
  await recGate(`final-gate: autofix ${fix.unobserved ? 'ended UNOBSERVED' : 'converged'} — ${fix.reason.slice(0, 200)}`);
358
- active.ui.notify(`${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
358
+ notifyRun(active, `${id}: final integration gate ${fix.unobserved ? 'is UNOBSERVED' : 'PASSES'} after autofix — ${fix.reason.slice(0, 140)}`, fix.unobserved ? 'warning' : 'info');
359
359
  // The gate's own outcome, WHOLE, with this door's reason on it.
360
360
  // Rebuilding it as a two-key literal would drop `openDebts` and
361
361
  // `observedFailures` from the value entirely.
@@ -380,7 +380,7 @@ export async function runFinalGateStage(active, deps, p) {
380
380
  await recGate(`final-gate: autofix attempt ${attempt} left ${ledger.stranded().length} `
381
381
  + `uncommitted change(s) — ${ledger.stranded().slice(0, 8).join(', ')}`);
382
382
  }
383
- active.ui.notify(`${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
383
+ notifyRun(active, `${id}: final-gate autofix did not converge — ${fix.reason.slice(0, 140)}`, 'warning');
384
384
  // NON-PROGRESS CLASSIFIER. An attempt that changed the tree, re-ran the
385
385
  // gate, and got back the SAME ranked-first failure as the previous such
386
386
  // attempt is evidence about the CHECK, not about the fix — a check that
@@ -396,7 +396,7 @@ export async function runFinalGateStage(active, deps, p) {
396
396
  await recGate(`final-gate: check DEMOTED to UNOBSERVED after ${attempt} tree-changing `
397
397
  + `attempts returned an identical failure — carried as debt (origin final-gate) `
398
398
  + `and re-checked by the next run's gate: ${verdict.detail.slice(0, 240)}`);
399
- active.ui.notify(`${id}: final-gate check is unfalsifiable in this environment — carried as debt; `
399
+ notifyRun(active, `${id}: final-gate check is unfalsifiable in this environment — carried as debt; `
400
400
  + 'the remaining checks decide convergence.', 'warning');
401
401
  }
402
402
  // Convergence on the REMAINING checks: a demoted signature no longer counts
@@ -409,7 +409,7 @@ export async function runFinalGateStage(active, deps, p) {
409
409
  const converged = `converged on all remaining checks; ${ledger.demotedCount()} check(s) `
410
410
  + 'carried as UNOBSERVED debt (unfalsifiable in this environment)';
411
411
  await recGate(`final-gate: ${converged}`);
412
- active.ui.notify(`${id}: final integration gate converged — ${converged}.`, 'warning');
412
+ notifyRun(active, `${id}: final integration gate converged — ${converged}.`, 'warning');
413
413
  // The gate's own outcome, with this door's reason on it — the
414
414
  // whole value, so `openDebts` survives the assignment.
415
415
  fin = { ...fix.gate, ok: true, reason: converged };