@mjasnikovs/pi-task 0.17.21 → 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 overflow-wrap: anywhere; word-break: break-word;\n }\n .tool-call summary::-webkit-details-marker { display: none; }\n .tool-call summary::before { content: \"\u25B6 \"; }\n .tool-call[open] > summary::before { content: \"\u25BC \"; }\n .tool-call.error > summary { color: var(--red); }\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); margin-left: 6px; font-family: ui-monospace, monospace; font-size: 13px; }\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 #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); }";
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);
@@ -93,28 +172,50 @@ export const STYLES = ` :root {
93
172
  .tool-call summary {
94
173
  padding: 6px 10px; color: var(--subtext1); cursor: pointer;
95
174
  user-select: none; list-style: none;
96
- overflow-wrap: anywhere; word-break: break-word;
175
+ display: flex; align-items: center; gap: 8px;
97
176
  }
98
177
  .tool-call summary::-webkit-details-marker { display: none; }
99
- .tool-call summary::before { content: "▶ "; }
100
- .tool-call[open] > summary::before { content: "▼ "; }
178
+ .tool-call summary::before { content: "▶"; flex-shrink: 0; font-size: 9px; color: var(--subtext0); }
179
+ .tool-call[open] > summary::before { content: "▼"; }
101
180
  .tool-call.error > summary { color: var(--red); }
181
+ /* The summary is a single line: the label ellipsizes (full text in the title
182
+ tooltip / on expand), badges and timing stay pinned to the right. */
183
+ .tool-label {
184
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
185
+ font-family: ui-monospace, monospace;
186
+ }
187
+ .tool-badge { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }
188
+ .tool-elapsed { flex-shrink: 0; color: var(--subtext0); font-size: 11px; }
102
189
  .tool-call pre {
103
190
  padding: 8px 12px; overflow-y: auto;
104
191
  color: var(--subtext1); font-size: 11px; max-height: 280px;
105
192
  border-top: 1px solid var(--surface0);
106
193
  white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;
107
194
  }
108
- .tool-spin { color: var(--mauve); margin-left: 6px; font-family: ui-monospace, monospace; font-size: 13px; }
195
+ .tool-spin { color: var(--mauve); flex-shrink: 0; font-family: ui-monospace, monospace; font-size: 13px; }
196
+ /* Line diff for edit/write tools (tints derived from --green/--red). */
197
+ .tool-diff { border-top: 1px solid var(--surface0); overflow-x: auto; }
198
+ .diff { font-family: ui-monospace, monospace; font-size: 11px; padding: 4px 0; }
199
+ .diff-line { white-space: pre; padding: 0 10px; }
200
+ .diff-sign { display: inline-block; width: 1ch; margin-right: 8px; color: var(--subtext0); }
201
+ .diff-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--green); }
202
+ .diff-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--red); }
203
+ .diff-ctx { color: var(--subtext1); }
109
204
  .code-block {
110
205
  background: var(--crust); border: 1px solid var(--surface0);
111
206
  border-radius: 6px; overflow: hidden; margin: 4px 0;
112
207
  align-self: stretch; max-width: 100%; font-size: 12px;
113
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
+ }
114
215
  .code-lang {
115
- background: var(--surface0); color: var(--subtext0);
116
- font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;
216
+ color: var(--subtext0); font-size: 10px; padding: 3px 10px; letter-spacing: 0.05em;
117
217
  }
218
+ .code-head .copy-btn { padding: 3px 10px; }
118
219
  .code-block code {
119
220
  display: block; padding: 10px 12px; overflow-x: auto;
120
221
  color: var(--text); white-space: pre; line-height: 1.55;
@@ -124,6 +225,35 @@ export const STYLES = ` :root {
124
225
  .hl-cmt { color: var(--subtext0); font-style: italic; }
125
226
  .hl-num { color: var(--blue); }
126
227
  .hl-fn { color: var(--yellow); }
228
+ /* Hand-rolled markdown, applied only to assistant bubbles + the recommendation
229
+ panel (both get the .md class from setContent). Block elements lay themselves
230
+ out, so switch off the container's pre-wrap for these. */
231
+ .md { white-space: normal; }
232
+ .md > :first-child { margin-top: 0; }
233
+ .md > :last-child { margin-bottom: 0; }
234
+ .md .md-h { color: var(--mauve); font-weight: 700; line-height: 1.3; margin: 10px 0 4px; }
235
+ .md .md-h1 { font-size: 1.35em; }
236
+ .md .md-h2 { font-size: 1.2em; }
237
+ .md .md-h3 { font-size: 1.08em; }
238
+ .md .md-h4, .md .md-h5, .md .md-h6 { font-size: 1em; }
239
+ .md .md-p { margin: 6px 0; }
240
+ .md .md-list { margin: 6px 0; padding-left: 22px; }
241
+ .md .md-list li { margin: 2px 0; }
242
+ .md .md-task { list-style: none; margin-left: -22px; }
243
+ .md .md-check { color: var(--green); }
244
+ .md .md-quote { border-left: 3px solid var(--surface2); margin: 6px 0; padding: 2px 0 2px 10px; color: var(--subtext1); }
245
+ .md .md-hr { border: none; border-top: 1px solid var(--surface1); margin: 10px 0; }
246
+ .md a { color: var(--blue); text-decoration: underline; }
247
+ .md strong { color: var(--text); font-weight: 700; }
248
+ .md em { font-style: italic; }
249
+ .md del { color: var(--subtext0); }
250
+ .md .md-code {
251
+ background: var(--crust); border: 1px solid var(--surface0);
252
+ border-radius: 4px; padding: 1px 4px; font-size: 0.92em;
253
+ }
254
+ .md .md-table { border-collapse: collapse; margin: 8px 0; font-size: 0.95em; display: block; overflow-x: auto; }
255
+ .md .md-table th, .md .md-table td { border: 1px solid var(--surface1); padding: 4px 8px; text-align: left; }
256
+ .md .md-table th { background: var(--surface0); color: var(--subtext1); }
127
257
  #input-bar {
128
258
  background: var(--mantle); padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
129
259
  display: flex; gap: 8px; flex-shrink: 0;
@@ -160,6 +290,13 @@ export const STYLES = ` :root {
160
290
  white-space: nowrap; align-self: flex-end;
161
291
  }
162
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
+ }
163
300
  #reconnect-overlay {
164
301
  display: none; position: fixed; inset: 0;
165
302
  background: rgba(30,30,46,0.88); color: var(--subtext1);
@@ -175,6 +312,27 @@ export const STYLES = ` :root {
175
312
  }
176
313
  #status-panel { padding: 6px 12px; border-bottom: 1px solid var(--surface1);
177
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; }
178
336
  #prompt-card { position: fixed; left: 0; right: 0; bottom: 0; background: var(--mantle);
179
337
  border-top: 2px solid var(--mauve); padding: 16px 14px calc(16px + env(safe-area-inset-bottom, 0px));
180
338
  display: none; z-index: 50; max-height: 80dvh; overflow-y: auto; }
@@ -217,4 +375,13 @@ export const STYLES = ` :root {
217
375
  #viewer { position: fixed; inset: 24px; background: var(--mantle); border: 1px solid var(--surface2);
218
376
  border-radius: 8px; padding: 16px; overflow: auto; white-space: pre-wrap;
219
377
  overflow-wrap: anywhere; word-break: break-word; display: none; z-index: 70; }
220
- #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }`;
378
+ #viewer .close { position: absolute; top: 8px; right: 12px; cursor: pointer; color: var(--subtext0); }
379
+ /* Desktop: center the transcript/status/input in a readable column instead of
380
+ hugging the left edge. The scrollbar stays at the true window edge; only the
381
+ content is inset. Mobile (below 960px) is unchanged. */
382
+ @media (min-width: 960px) {
383
+ #chat-log { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }
384
+ #status-panel { padding-left: calc((100% - 920px) / 2 + 12px); padding-right: calc((100% - 920px) / 2 + 12px); }
385
+ #input-bar { padding-left: calc((100% - 920px) / 2); padding-right: calc((100% - 920px) / 2); }
386
+ #cmd-suggestions { left: calc((100% - 920px) / 2 + 16px); right: calc((100% - 920px) / 2 + 16px); }
387
+ }`;
@@ -0,0 +1,2 @@
1
+ /** Emitted client JS (top-level function declarations). */
2
+ export declare function toolsModule(): string;
@@ -0,0 +1,113 @@
1
+ // Tool-call presentation helpers, shipped as a JS string and concatenated into
2
+ // the page by ui.ts. Turns raw {toolName, args} into a readable one-line summary,
3
+ // a +N −M diff badge, and an expandable line-diff body for edit/write tools —
4
+ // replacing the old "name + JSON.stringify(args) hard-sliced at 64 chars".
5
+ //
6
+ // Depends on escHtml (ui-render.ts) — both land in the same <script>.
7
+ /** Emitted client JS (top-level function declarations). */
8
+ export function toolsModule() {
9
+ return ` // args arrives as an object (live tool_start / snapshot part) or a JSON string.
10
+ function toolArgs(args) {
11
+ if (args && typeof args === 'object') return args;
12
+ if (typeof args === 'string') { try { return JSON.parse(args); } catch (e) { return null; } }
13
+ return null;
14
+ }
15
+
16
+ // A readable one-line summary. Falls back to the old name+JSON form (but with NO
17
+ // hard 64-char slice — the summary line ellipsizes via CSS, full text on hover).
18
+ function toolSummary(toolName, args) {
19
+ var name = (toolName || '').toLowerCase();
20
+ var a = toolArgs(args);
21
+ function s(v) { return v == null ? '' : String(v); }
22
+ if (a) {
23
+ var path = a.path != null ? a.path : a.file_path;
24
+ if (name.indexOf('bash') >= 0 && a.command != null) return '$ ' + s(a.command);
25
+ if (name === 'read' && path != null) return 'read ' + s(path);
26
+ if ((name === 'edit' || name === 'multiedit' || name === 'write' || name === 'str_replace') && path != null) return s(path);
27
+ if (name === 'grep' || name === 'find' || name === 'glob' || name === 'search') {
28
+ var pat = a.pattern != null ? a.pattern : (a.query != null ? a.query : a.glob);
29
+ var loc = a.path != null ? a.path : a.glob;
30
+ return name + ' ' + s(pat) + (loc && loc !== pat ? ' ' + s(loc) : '');
31
+ }
32
+ }
33
+ var argStr = typeof args === 'string' ? args : (args === undefined ? '' : JSON.stringify(args));
34
+ return toolName + (argStr ? ': ' + argStr : '');
35
+ }
36
+
37
+ // Line diff via LCS. Bounded: a huge pair falls back to "all removed / all added"
38
+ // rather than allocating an O(m*n) table that could hang the tab.
39
+ function lcsDiff(oldText, newText) {
40
+ // Treat '' / null as ZERO lines (not one empty line) so a new-file write shows
41
+ // as all-added with no phantom "removed blank line".
42
+ var a = (oldText == null || oldText === '') ? [] : String(oldText).split('\\n');
43
+ var b = (newText == null || newText === '') ? [] : String(newText).split('\\n');
44
+ var m = a.length, n = b.length;
45
+ var rows = [];
46
+ if (m * n > 250000) {
47
+ for (var x = 0; x < m; x++) rows.push({t: '-', s: a[x]});
48
+ for (var y = 0; y < n; y++) rows.push({t: '+', s: b[y]});
49
+ return rows;
50
+ }
51
+ var dp = [];
52
+ for (var i = 0; i <= m; i++) dp.push(new Array(n + 1).fill(0));
53
+ for (var i2 = m - 1; i2 >= 0; i2--)
54
+ for (var j2 = n - 1; j2 >= 0; j2--)
55
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
56
+ var i = 0, j = 0;
57
+ while (i < m && j < n) {
58
+ if (a[i] === b[j]) { rows.push({t: ' ', s: a[i]}); i++; j++; }
59
+ else if (dp[i + 1][j] >= dp[i][j + 1]) { rows.push({t: '-', s: a[i]}); i++; }
60
+ else { rows.push({t: '+', s: b[j]}); j++; }
61
+ }
62
+ while (i < m) { rows.push({t: '-', s: a[i++]}); }
63
+ while (j < n) { rows.push({t: '+', s: b[j++]}); }
64
+ return rows;
65
+ }
66
+
67
+ function diffStats(oldText, newText) {
68
+ var rows = lcsDiff(oldText, newText), add = 0, rem = 0;
69
+ for (var k = 0; k < rows.length; k++) { if (rows[k].t === '+') add++; else if (rows[k].t === '-') rem++; }
70
+ return {added: add, removed: rem};
71
+ }
72
+
73
+ function renderDiff(oldText, newText) {
74
+ var rows = lcsDiff(oldText, newText);
75
+ var html = '<div class="diff">';
76
+ for (var k = 0; k < rows.length; k++) {
77
+ var r = rows[k];
78
+ var cls = r.t === '+' ? 'diff-add' : (r.t === '-' ? 'diff-del' : 'diff-ctx');
79
+ html += '<div class="diff-line ' + cls + '"><span class="diff-sign">' + (r.t === ' ' ? ' ' : r.t) + '</span>' + escHtml(r.s) + '</div>';
80
+ }
81
+ return html + '</div>';
82
+ }
83
+
84
+ // The old/new text for an edit/write tool's diff, or null if not a diffable tool.
85
+ function toolDiffPair(toolName, args) {
86
+ var name = (toolName || '').toLowerCase();
87
+ var a = toolArgs(args);
88
+ if (!a) return null;
89
+ if (name === 'edit' || name === 'multiedit' || name === 'str_replace') {
90
+ if (a.old_string != null || a.new_string != null) return {old: a.old_string, neu: a.new_string};
91
+ }
92
+ if (name === 'write' && a.content != null) return {old: '', neu: a.content};
93
+ return null;
94
+ }
95
+
96
+ function toolBadge(toolName, args) {
97
+ var pair = toolDiffPair(toolName, args);
98
+ return pair ? diffStats(pair.old, pair.neu) : null;
99
+ }
100
+
101
+ function toolDiffHtml(toolName, args) {
102
+ var pair = toolDiffPair(toolName, args);
103
+ return pair ? renderDiff(pair.old, pair.neu) : null;
104
+ }
105
+
106
+ function fmtElapsed(ms) {
107
+ if (ms == null || isNaN(ms)) return '';
108
+ if (ms < 1000) return Math.round(ms) + 'ms';
109
+ var sec = ms / 1000;
110
+ return (sec < 10 ? sec.toFixed(1) : Math.round(sec)) + 's';
111
+ }
112
+ `;
113
+ }
package/dist/remote/ui.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { STYLES } from './ui-styles.js';
2
2
  import { clientScript } from './ui-script.js';
3
+ import { renderModule } from './ui-render.js';
4
+ import { highlightModule } from './ui-highlight.js';
5
+ import { toolsModule } from './ui-tools.js';
3
6
  export function html(wsUrl) {
4
7
  const iconSvg = encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">`
5
8
  + `<rect width="180" height="180" rx="38" fill="#1e1e2e"/>`
@@ -35,9 +38,21 @@ ${STYLES}
35
38
  <div id="header">
36
39
  <span class="title">pi-task remote</span>
37
40
  <div class="hgroup">
38
- <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>
39
47
  </div>
40
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>
41
56
  <div id="chat-wrap">
42
57
  <div id="chat-log"></div>
43
58
  <button id="scroll-bottom" aria-label="Scroll to latest" title="Scroll to latest">&#x2193;</button>
@@ -61,6 +76,9 @@ ${STYLES}
61
76
  </div>
62
77
  <div id="viewer"><span class="close" id="viewer-close">&#x2715;</span><div id="viewer-body"></div></div>
63
78
  <script>
79
+ ${renderModule()}
80
+ ${highlightModule()}
81
+ ${toolsModule()}
64
82
  ${clientScript(wsUrl)}
65
83
  </script>
66
84
  </body>
@@ -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.21",
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",