@mjasnikovs/pi-task 0.17.22 → 0.17.23

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 from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\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 #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; }\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 .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 .code-lang {\n background: var(--surface0); color: var(--subtext0);\n font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;\n }\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 #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 #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 inline at the end of the streaming text (not a green blinking block). */\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 #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 }";
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 from window.innerHeight (see setAppHeight) so the column\n height is a stable pixel value across an orientation change. 100dvh is a\n fallback for first paint / no-JS: iOS Safari interpolates dvh during the\n rotation animation, which makes the whole flex column resize repeatedly\n (\"spazzing out\") \u2014 a fixed px height does not. */\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 (the surface bar that used to live on .code-lang). */\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 #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 inline at the end of the streaming text (not a green blinking block). */\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 .widget-top { display: flex; align-items: center; gap: 8px; }\n .widget-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;\n white-space: nowrap; color: var(--text); font-size: 13px; }\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-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px;\n font-variant-numeric: tabular-nums; }\n .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;\n overflow: hidden; margin-top: 6px; }\n .widget-bar-fill { height: 100%; background: var(--mauve); border-radius: 2px;\n transition: width 0.3s ease; }\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 }";
@@ -43,6 +43,54 @@ export const STYLES = ` :root {
43
43
  }
44
44
  #bell:hover { color: var(--text); }
45
45
  #bell.on { color: var(--mauve); }
46
+ /* Header status chip: connection dot + model name + context usage. */
47
+ #status-chip { display: flex; align-items: center; gap: 7px; font-size: 11px; color: var(--subtext0); }
48
+ #status-dot {
49
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
50
+ background: var(--surface2); transition: background 0.2s ease;
51
+ }
52
+ #status-dot.idle { background: var(--green); }
53
+ #status-dot.running { background: var(--mauve); animation: dot-pulse 1.2s ease-in-out infinite; }
54
+ #status-dot.disconnected { background: var(--red); }
55
+ @keyframes dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
56
+ @media (prefers-reduced-motion: reduce) { #status-dot.running { animation: none; } }
57
+ #status-model { color: var(--subtext1); }
58
+ #status-model:empty { display: none; }
59
+ #status-ctx { color: var(--subtext0); font-variant-numeric: tabular-nums; }
60
+ #status-ctx:empty { display: none; }
61
+ /* Notification bell dropdown: a push toggle row + the recent-toast history. */
62
+ #notif-panel {
63
+ display: none; position: fixed; z-index: 80;
64
+ top: calc(env(safe-area-inset-top, 0px) + 42px);
65
+ right: calc(env(safe-area-inset-right, 0px) + 12px);
66
+ width: min(320px, calc(100vw - 24px));
67
+ background: var(--mantle); border: 1px solid var(--surface1); border-radius: 8px;
68
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); overflow: hidden;
69
+ }
70
+ #notif-panel.open { display: block; }
71
+ #notif-toggle-row {
72
+ display: flex; align-items: center; justify-content: space-between;
73
+ padding: 8px 12px; border-bottom: 1px solid var(--surface0);
74
+ }
75
+ #notif-title { font-size: 12px; color: var(--subtext1); font-weight: 700; }
76
+ #notif-toggle {
77
+ background: var(--surface1); color: var(--text); border: none; border-radius: 6px;
78
+ padding: 4px 10px; font-family: inherit; font-size: 11px; cursor: pointer;
79
+ }
80
+ #notif-toggle:hover { filter: brightness(1.1); }
81
+ #notif-toggle.on { background: var(--mauve); color: var(--crust); font-weight: 700; }
82
+ #notif-list { max-height: 40dvh; overflow-y: auto; }
83
+ #notif-empty { padding: 14px 12px; color: var(--subtext0); font-size: 11px; text-align: center; }
84
+ .notif-item {
85
+ display: flex; align-items: baseline; gap: 8px; padding: 7px 12px;
86
+ border-bottom: 1px solid var(--surface0); font-size: 12px;
87
+ }
88
+ .notif-item:last-child { border-bottom: none; }
89
+ .notif-item .notif-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--blue); align-self: center; }
90
+ .notif-item.warning .notif-dot { background: var(--peach); }
91
+ .notif-item.error .notif-dot { background: var(--red); }
92
+ .notif-item .notif-msg { flex: 1; min-width: 0; color: var(--text); overflow-wrap: anywhere; word-break: break-word; }
93
+ .notif-item .notif-time { flex-shrink: 0; color: var(--subtext0); font-size: 10px; font-variant-numeric: tabular-nums; }
46
94
  #chat-wrap { position: relative; flex: 1; min-height: 0; display: flex; }
47
95
  #chat-log {
48
96
  flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 16px;
@@ -67,7 +115,7 @@ export const STYLES = ` :root {
67
115
  line-height: 1.6; white-space: pre-wrap; word-break: break-word; font-size: 13px;
68
116
  }
69
117
  .bubble.user { background: var(--surface1); color: var(--text); align-self: flex-end; }
70
- .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; }
118
+ .bubble.assistant { background: var(--surface0); color: var(--text); align-self: flex-start; position: relative; }
71
119
  .bubble.error {
72
120
  background: var(--crust); color: var(--red); align-self: stretch;
73
121
  max-width: 100%; border: 1px solid var(--red); font-size: 12px;
@@ -86,6 +134,37 @@ export const STYLES = ` :root {
86
134
  color: var(--mauve); font-size: 15px; line-height: 1;
87
135
  font-family: ui-monospace, monospace;
88
136
  }
137
+ /* Collapsed reasoning block ("✻ Thinking… (n lines)"), muted + italic. */
138
+ .thinking-block { align-self: flex-start; max-width: 90%; font-size: 12px; }
139
+ .thinking-block > summary {
140
+ color: var(--subtext0); font-style: italic; cursor: pointer; list-style: none;
141
+ user-select: none; display: flex; align-items: center; gap: 8px; padding: 2px 0;
142
+ }
143
+ .thinking-block > summary::-webkit-details-marker { display: none; }
144
+ .thinking-block .thinking-spin {
145
+ color: var(--mauve); font-style: normal; font-family: ui-monospace, monospace;
146
+ }
147
+ .thinking-block .thinking-body {
148
+ color: var(--subtext0); font-style: italic; white-space: pre-wrap;
149
+ word-break: break-word; line-height: 1.5; margin: 4px 0 0 4px;
150
+ padding: 4px 0 2px 12px; border-left: 2px solid var(--surface1);
151
+ }
152
+ /* Copy buttons: on code-block headers and (floating) on finished assistant
153
+ bubbles. Wired by one delegated click handler in the client script. */
154
+ .copy-btn {
155
+ background: transparent; border: none; color: var(--subtext0); cursor: pointer;
156
+ font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px;
157
+ line-height: 1.4;
158
+ }
159
+ .copy-btn:hover { color: var(--text); background: var(--surface1); }
160
+ .copy-btn.copied { color: var(--green); }
161
+ .bubble-copy {
162
+ position: absolute; top: 4px; right: 4px; opacity: 0;
163
+ background: var(--surface1); transition: opacity 0.12s ease;
164
+ }
165
+ .bubble.assistant:hover .bubble-copy { opacity: 1; }
166
+ /* Touch devices have no hover — keep the button faintly visible. */
167
+ @media (hover: none) { .bubble-copy { opacity: 0.55; } }
89
168
  .tool-call {
90
169
  background: var(--crust); border-radius: 6px; align-self: flex-start;
91
170
  max-width: 90%; font-size: 12px; border: 1px solid var(--surface0);
@@ -127,10 +206,16 @@ export const STYLES = ` :root {
127
206
  border-radius: 6px; overflow: hidden; margin: 4px 0;
128
207
  align-self: stretch; max-width: 100%; font-size: 12px;
129
208
  }
209
+ /* Header row above a code block: language label on the left, copy button on
210
+ the right (the surface bar that used to live on .code-lang). */
211
+ .code-head {
212
+ display: flex; align-items: center; justify-content: space-between;
213
+ background: var(--surface0);
214
+ }
130
215
  .code-lang {
131
- background: var(--surface0); color: var(--subtext0);
132
- font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;
216
+ color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;
133
217
  }
218
+ .code-head .copy-btn { padding: 3px 10px; }
134
219
  .code-block code {
135
220
  display: block; padding: 10px 12px; overflow-x: auto;
136
221
  color: var(--text); white-space: pre; line-height: 1.55;
@@ -205,6 +290,13 @@ export const STYLES = ` :root {
205
290
  white-space: nowrap; align-self: flex-end;
206
291
  }
207
292
  #send:disabled, #input:disabled { opacity: 0.45; cursor: not-allowed; }
293
+ /* While the agent runs, Send becomes a red Stop; the armed (tap-to-confirm)
294
+ state brightens it and adds a halo, mirroring the prompt card's cancel. */
295
+ #send.stop { background: var(--red); color: var(--crust); }
296
+ #send.stop.armed {
297
+ filter: brightness(1.12);
298
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 45%, transparent);
299
+ }
208
300
  #reconnect-overlay {
209
301
  display: none; position: fixed; inset: 0;
210
302
  background: rgba(30,30,46,0.88); color: var(--subtext1);
@@ -220,6 +312,27 @@ export const STYLES = ` :root {
220
312
  }
221
313
  #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);
222
314
  color: var(--subtext1); white-space: pre-wrap; font-size: 13px; display: none; }
315
+ /* Structured task widget (progress bar + phase badge + elapsed). Replaces the
316
+ plain text lines when the server sends a structured data payload. */
317
+ #status-panel.structured { white-space: normal; display: block; }
318
+ .widget-top { display: flex; align-items: center; gap: 8px; }
319
+ .widget-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
320
+ white-space: nowrap; color: var(--text); font-size: 13px; }
321
+ .widget-phase { flex-shrink: 0; background: var(--surface0); color: var(--mauve);
322
+ border-radius: 4px; padding: 1px 7px; font-size: 10px; letter-spacing: 0.03em;
323
+ text-transform: uppercase; }
324
+ .widget-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px;
325
+ font-variant-numeric: tabular-nums; }
326
+ .widget-bar { height: 4px; background: var(--surface0); border-radius: 2px;
327
+ overflow: hidden; margin-top: 6px; }
328
+ .widget-bar-fill { height: 100%; background: var(--mauve); border-radius: 2px;
329
+ transition: width 0.3s ease; }
330
+ /* Dim per-turn timestamp shown under a committed turn's bubble. */
331
+ .turn-time { font-size: 10px; color: var(--subtext0); opacity: 0.55; padding: 0 4px;
332
+ margin-top: -2px; }
333
+ .turn-time.user { align-self: flex-end; }
334
+ .turn-time.assistant, .turn-time.system { align-self: flex-start; }
335
+ .turn-time.system { align-self: center; }
223
336
  #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);
224
337
  border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));
225
338
  display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }
package/dist/remote/ui.js CHANGED
@@ -38,9 +38,21 @@ ${STYLES}
38
38
  <div id="header">
39
39
  <span class="title">pi-task remote</span>
40
40
  <div class="hgroup">
41
- <button id="bell" aria-label="Toggle notifications" title="Notifications">&#x25EF;</button>
41
+ <span id="status-chip" title="Connection · model · context">
42
+ <span id="status-dot" class="disconnected"></span>
43
+ <span id="status-model"></span>
44
+ <span id="status-ctx"></span>
45
+ </span>
46
+ <button id="bell" aria-label="Notifications" title="Notifications">&#x25EF;</button>
42
47
  </div>
43
48
  </div>
49
+ <div id="notif-panel" aria-hidden="true">
50
+ <div id="notif-toggle-row">
51
+ <span id="notif-title">Notifications</span>
52
+ <button id="notif-toggle" type="button"></button>
53
+ </div>
54
+ <div id="notif-list"></div>
55
+ </div>
44
56
  <div id="chat-wrap">
45
57
  <div id="chat-log"></div>
46
58
  <button id="scroll-bottom" aria-label="Scroll to latest" title="Scroll to latest">&#x2193;</button>
@@ -20,7 +20,7 @@
20
20
  * Only one task runs at a time (single active task), so a single module-level slot
21
21
  * is sufficient.
22
22
  */
23
- import { WIDGET_KEY, WIDGET_REFRESH_MS, buildImplLines } from './widget.js';
23
+ import { WIDGET_KEY, WIDGET_REFRESH_MS, buildImplLines, buildImplData } from './widget.js';
24
24
  import { setTaskWidget } from '../remote/session-state.js';
25
25
  let armed = null;
26
26
  let lastLine;
@@ -52,7 +52,7 @@ function render() {
52
52
  catch {
53
53
  /* stale ctx */
54
54
  }
55
- setTaskWidget(buildImplLines(state, undefined)); // un-themed for the wire
55
+ setTaskWidget(buildImplLines(state, undefined), buildImplData(state)); // un-themed for the wire
56
56
  }
57
57
  function startTimer() {
58
58
  if (timer)
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
8
8
  import { type PhaseName, type TaskState } from './task-types.js';
9
+ import type { WidgetData } from '../remote/protocol.js';
9
10
  export interface WidgetState {
10
11
  taskId: string;
11
12
  title: string;
@@ -40,6 +41,8 @@ export declare function contextThresholdColor(theme: WidgetTheme, percent: numbe
40
41
  /** Render the `tokens/window [bar]` context suffix, or null when there's nothing to show. */
41
42
  export declare function formatContextDetail(usage: ContextSnapshot, theme?: WidgetTheme): string | null;
42
43
  export declare function buildWidgetLines(s: WidgetState, theme?: WidgetTheme): string[];
44
+ /** Structured mirror of buildWidgetLines for the browser's progress widget. */
45
+ export declare function buildWidgetData(s: WidgetState): WidgetData;
43
46
  export declare function startWidget(ctx: ExtensionCommandContext, getState: () => WidgetState | null): () => void;
44
47
  export interface AutoLoaderState {
45
48
  title: string;
@@ -58,6 +61,9 @@ export interface AutoLoaderState {
58
61
  kind?: 'planning' | 'enforce' | 'verify' | 'recommend' | 'lint-fix';
59
62
  }
60
63
  export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetTheme): string[];
64
+ /** Structured mirror of buildAutoLoaderLines. Only the numbered planning stage
65
+ * carries done/total; the enforce/verify/recommend/lint-fix passes are unnumbered. */
66
+ export declare function buildAutoLoaderData(s: AutoLoaderState): WidgetData;
61
67
  /**
62
68
  * Start the planning loader widget (same cadence/look as the phase widget).
63
69
  * Returns a disposer that stops the refresh and clears the widget. No-op
@@ -74,4 +80,7 @@ export interface ImplState {
74
80
  contextUsage?: ContextSnapshot;
75
81
  }
76
82
  export declare function buildImplLines(s: ImplState, theme?: WidgetTheme): string[];
83
+ /** Structured mirror of buildImplLines (the host implementation turn — no step
84
+ * numbering, so no progress bar; just the phase badge and elapsed clock). */
85
+ export declare function buildImplData(s: ImplState): WidgetData;
77
86
  export declare function flashTerminalWidget(ctx: ExtensionCommandContext, state: Exclude<TaskState, 'pending' | 'in_progress' | 'completed'>, taskId: string, reason: string | undefined): void;
@@ -88,6 +88,18 @@ export function buildWidgetLines(s, theme) {
88
88
  lines.push(trailer);
89
89
  return lines;
90
90
  }
91
+ /** Structured mirror of buildWidgetLines for the browser's progress widget. */
92
+ export function buildWidgetData(s) {
93
+ const idx = PHASE_INDEX[s.phase];
94
+ const total = PHASE_ORDER.length;
95
+ return {
96
+ title: `${s.taskId} · ${titleForDisplay(s)}`,
97
+ phase: s.phase,
98
+ done: Math.min(idx + 1, total),
99
+ total,
100
+ elapsed: formatDuration(Date.now() - s.startedAt)
101
+ };
102
+ }
91
103
  // ─── Widget lifecycle ────────────────────────────────────────────────────────
92
104
  export function startWidget(ctx, getState) {
93
105
  if (!ctx.hasUI)
@@ -102,7 +114,7 @@ export function startWidget(ctx, getState) {
102
114
  catch {
103
115
  /* stale ctx */
104
116
  }
105
- setTaskWidget(plain);
117
+ setTaskWidget(plain, s ? buildWidgetData(s) : null);
106
118
  };
107
119
  render();
108
120
  const timer = setInterval(render, WIDGET_REFRESH_MS);
@@ -137,6 +149,25 @@ export function buildAutoLoaderLines(s, theme) {
137
149
  lines.push(trailer);
138
150
  return lines;
139
151
  }
152
+ /** Structured mirror of buildAutoLoaderLines. Only the numbered planning stage
153
+ * carries done/total; the enforce/verify/recommend/lint-fix passes are unnumbered. */
154
+ export function buildAutoLoaderData(s) {
155
+ const phase = s.kind === 'enforce' ? 'enforcing guidelines'
156
+ : s.kind === 'verify' ? 'verifying work'
157
+ : s.kind === 'recommend' ? 'assessing the failure'
158
+ : s.kind === 'lint-fix' ? 'fixing static findings'
159
+ : s.step;
160
+ const d = {
161
+ title: `/task-auto · ${s.title}`,
162
+ phase,
163
+ elapsed: formatDuration(Date.now() - s.startedAt)
164
+ };
165
+ if (!s.kind || s.kind === 'planning') {
166
+ d.done = s.stepNum;
167
+ d.total = s.stepTotal;
168
+ }
169
+ return d;
170
+ }
140
171
  /**
141
172
  * Start the planning loader widget (same cadence/look as the phase widget).
142
173
  * Returns a disposer that stops the refresh and clears the widget. No-op
@@ -155,7 +186,7 @@ export function startAutoLoader(ctx, getState) {
155
186
  catch {
156
187
  /* stale ctx */
157
188
  }
158
- setTaskWidget(plain);
189
+ setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
159
190
  };
160
191
  render();
161
192
  const timer = setInterval(render, WIDGET_REFRESH_MS);
@@ -186,6 +217,15 @@ export function buildImplLines(s, theme) {
186
217
  lines.push(trailer);
187
218
  return lines;
188
219
  }
220
+ /** Structured mirror of buildImplLines (the host implementation turn — no step
221
+ * numbering, so no progress bar; just the phase badge and elapsed clock). */
222
+ export function buildImplData(s) {
223
+ return {
224
+ title: `${s.taskId} · ${titleForDisplay(s)}`,
225
+ phase: 'implementing',
226
+ elapsed: formatDuration(Date.now() - s.startedAt)
227
+ };
228
+ }
189
229
  export function flashTerminalWidget(ctx, state, taskId, reason) {
190
230
  if (!ctx.hasUI)
191
231
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.17.22",
3
+ "version": "0.17.23",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",