Valeriy Selitskiy commited on
Commit
6c62075
·
1 Parent(s): c3633b1

Release v1.2.1 adaptive chat workspace

Browse files
README.md CHANGED
@@ -40,9 +40,21 @@ Vite during its container build. The checked-in bundle is produced locally by
40
  `npm ci && npm run build`; source, tests, lockfile, and exact runtime assets
41
  remain in the same Space repository.
42
 
 
 
 
 
 
 
 
 
 
 
43
  ## Browser requirements
44
 
45
- Chrome or Edge with WebGPU is recommended. CPU-WASM fallback requires a
 
 
46
  cross-origin-isolated browser with `SharedArrayBuffer`; the Nginx configuration
47
  enables it. Model storage uses the browser cache/OPFS path exposed by wllama and
48
  requests persistent storage before the first model byte is downloaded. A browser
 
40
  `npm ci && npm run build`; source, tests, lockfile, and exact runtime assets
41
  remain in the same Space repository.
42
 
43
+ ## Chat workspace
44
+
45
+ Chat sessions and their system instructions are saved locally in IndexedDB.
46
+ The chat list can reopen, continue, or delete a session without touching the
47
+ model cache. New chats begin with the bundled private-assistant instructions;
48
+ an empty chat exposes them directly, while an active chat keeps them in Chat
49
+ settings. Appearance can follow the operating system or be pinned to the light
50
+ or dark theme. Both sidebars can collapse, and the transcript can switch
51
+ between centered and full-width layouts.
52
+
53
  ## Browser requirements
54
 
55
+ Chrome or Edge with WebGPU is recommended. Safari uses the stable compatibility
56
+ runtime but is substantially slower, so the UI calls this limitation out before
57
+ the model is loaded. CPU-WASM fallback requires a
58
  cross-origin-isolated browser with `SharedArrayBuffer`; the Nginx configuration
59
  enables it. Model storage uses the browser cache/OPFS path exposed by wllama and
60
  requests persistent storage before the first model byte is downloaded. A browser
dist/assets/index-BY7HNxCd.js DELETED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-BbReSmeP.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#141613;--ink-raised:#1b1e1a;--ink-soft:#252921;--paper:#f1f2ed;--paper-deep:#e4e6df;--paper-ghost:#f1f2eda3;--phosphor:#a8d574;--phosphor-dim:#a8d57424;--moss:#738066;--rust:#c27a56;--line-light:#f1f2ed21;--line-dark:#14161326;--display:"Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;--body:"Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;--mono:"Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;--type-utility:clamp(.75rem, .12vw + .72rem, .8125rem);--type-caption:clamp(.8125rem, .15vw + .77rem, .9375rem);--type-body:clamp(1.0625rem, .18vw + 1.02rem, 1.1875rem);--type-heading:clamp(1.55rem, .85vw + 1.35rem, 2.15rem);font-family:var(--body);font-synthesis:none;text-rendering:optimizelegibility}*{box-sizing:border-box}html{background:var(--ink)}body{background:var(--ink);min-width:320px;min-height:100vh;color:var(--paper);font-size:var(--type-body);margin:0}button,textarea,select{font:inherit}button{color:inherit}button:focus-visible,textarea:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--phosphor);outline-offset:3px}button:disabled{cursor:not-allowed}::selection{color:var(--ink);background:var(--phosphor)}.station-frame{--canvas-bg:#f1f2ed;--canvas-raised:#e4e6df;--canvas-field:#f8f9f5;--canvas-text:#141613;--canvas-muted:#14161385;--canvas-line:#14161326;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;isolation:isolate;min-height:100dvh;animation:.58s cubic-bezier(.16,1,.3,1) both station-in}.station-frame[data-theme=dark]{--canvas-bg:#1a1c18;--canvas-raised:#20231f;--canvas-field:#252823;--canvas-text:#edf0e8;--canvas-muted:#edf0e894;--canvas-line:#edf0e821;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}.station-frame:before{display:none}.station-header{background:var(--ink);border-bottom:1px solid var(--line-light);z-index:10;grid-template-columns:260px minmax(320px,1fr) auto;align-items:stretch;min-height:64px;display:grid;position:relative}.brand-block{border-right:1px solid var(--line-light);align-items:center;gap:10px;padding:10px 18px;display:flex}.brand-block h1{font:650 20px/1.1 var(--display);letter-spacing:-.02em;margin:0}.brand-block>div{gap:2px;display:grid}.brand-block>div>span{color:var(--paper-ghost);font-size:var(--type-caption)}.brand-glyph{width:34px;height:34px;color:var(--phosphor);font:650 16px/1 var(--mono);border:1px solid #a8d5746b;border-radius:8px;flex:0 0 34px;place-items:center;display:grid}.brand-mark:before,.brand-mark:after,.brand-mark span:before,.brand-mark span:after{content:"";background:var(--phosphor);transform-origin:bottom;width:1px;position:absolute}.brand-mark:before{height:25px;transform:translate(-1px,4px)rotate(0)}.brand-mark:after{height:13px;transform:translate(4px,-2px)rotate(43deg)}.brand-mark span:before{height:11px;transform:translate(-5px,-1px)rotate(-47deg)}.brand-mark span:after{height:8px;transform:translate(3px,-9px)rotate(72deg)}.eyebrow{color:#e8dfca85;font:600 var(--type-utility)/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;margin:0 0 5px}.eyebrow.ink{color:#11151285}.browser-notice{align-items:center;gap:10px;min-width:0;padding:0 20px;display:flex}.notice-mark{background:var(--phosphor);border-radius:2px;flex:0 0 6px;width:6px;height:6px}.browser-notice>span:last-child{gap:2px;display:grid}.browser-notice strong{color:var(--paper);font:600 var(--type-caption)/1.2 var(--body)}.browser-notice>span>span{color:var(--paper-ghost);font:var(--type-caption)/1.25 var(--body)}.browser-notice.is-warning .notice-mark{background:var(--rust)}.browser-notice.is-warning strong{color:#efb08f}.header-actions{border-left:1px solid var(--line-light);justify-self:end;align-items:center;gap:10px;padding:0 14px;display:flex}.layout-controls{gap:4px;display:flex}.layout-controls button,.icon-button{width:36px;height:36px;color:var(--paper-ghost);border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:7px;place-items:center;transition:color .15s,border-color .15s,background .15s;display:grid}.layout-controls button:hover,.icon-button:hover{color:var(--paper);background:#f1f2ed0d;border-color:#f1f2ed4d}.layout-controls button[aria-pressed=true]{color:var(--phosphor);background:var(--phosphor-dim);border-color:#a8d57457}.layout-controls svg,.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.station-clock{color:var(--paper-ghost);font-family:var(--mono);justify-items:end;padding:0 18px;display:grid}.station-clock small{font-size:var(--type-utility);letter-spacing:.18em}.station-clock span{font-size:var(--type-caption);letter-spacing:.09em;margin-top:4px}.icon-button{flex:0 0 36px}.icon-button:hover{color:var(--paper)}.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.workspace-grid{background:#10120f;grid-template-columns:260px minmax(0,1fr) 340px;grid-template-areas:"models chat inspector";min-height:calc(100dvh - 64px);display:grid}.workspace-grid.is-models-collapsed{grid-template-columns:0 minmax(0,1fr) 340px}.workspace-grid.is-inspector-collapsed{grid-template-columns:260px minmax(0,1fr) 0}.workspace-grid.is-models-collapsed.is-inspector-collapsed{grid-template-columns:0 minmax(0,1fr) 0}.workspace-grid.layout-centered .chat-surface{justify-self:center;width:min(100%,1120px)}.workspace-grid.layout-wide .chat-surface{width:100%}.model-rail[hidden],.inspector[hidden]{display:none!important}.model-rail,.inspector{background:var(--ink);min-width:0}.model-rail{border-right:1px solid var(--line-light);flex-direction:column;grid-area:models;padding:20px 14px 14px;display:flex;position:relative;overflow:hidden}.model-rail:before{display:none}.rail-heading,.section-heading{z-index:1;align-items:center;gap:10px;display:flex;position:relative}.rail-heading>span,.section-heading>span:first-child{color:var(--phosphor);font:500 var(--type-utility)/1 var(--mono);border-top:1px solid var(--phosphor);padding-top:5px}.rail-heading h2,.section-heading h2{font:600 20px/1.2 var(--display);letter-spacing:-.015em;margin:0}.rail-heading p{color:var(--paper-ghost);font-size:var(--type-caption);margin:4px 0 0;line-height:1.35}.model-list{z-index:1;gap:5px;margin-top:21px;display:grid;position:relative}.model-card{border:1px solid var(--line-light);border-radius:8px;transition:border-color .18s,background .18s}.model-card.is-active{background:var(--phosphor-dim);border-color:#a8d57494}.model-select{text-align:left;width:100%;min-height:72px;color:var(--paper);cursor:pointer;background:0 0;border:0;grid-template-columns:1fr auto;gap:8px;padding:13px 9px 12px;display:grid}.model-card:not(.is-active) .model-select:hover{background:#f1f2ed0b}.model-index{color:#e8dfca61;font:var(--type-utility)/1 var(--mono);padding-top:4px}.model-copy{gap:3px;min-width:0;display:grid}.model-title-line{justify-content:space-between;align-items:center;gap:7px;display:flex}.model-title-line strong{font:600 20px/1 var(--display)}.model-copy em{color:#e8dfca75;font:italic var(--type-caption)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.model-copy>span:last-child{color:var(--paper-ghost);font-size:var(--type-caption);margin-top:3px}.availability{font:600 var(--type-utility)/1 var(--mono);letter-spacing:0;border:1px solid var(--line-light);padding:2px 4px}.availability-loaded,.availability-loading,.availability-cached{color:var(--phosphor);border-color:#b9ff4552}.availability-remote,.availability-limited{color:var(--rust);border-color:#c96f4366}.model-footprint{color:#e8dfca63;font:var(--type-utility)/1 var(--mono);white-space:nowrap;align-self:end}.model-limit-reason{color:#e8dfca94;font:var(--type-utility)/1.35 var(--mono);margin:-4px 8px 9px;display:block}.rail-footnote{z-index:1;color:#e8dfca4d;font:var(--type-utility)/1.2 var(--mono);letter-spacing:.03em;justify-content:space-between;gap:8px;margin-top:auto;display:flex;position:relative}.chat-surface{min-width:0;height:calc(100dvh - 64px);min-height:calc(100dvh - 64px);color:var(--canvas-text);background:var(--canvas-bg);grid-area:chat;grid-template-rows:auto minmax(0,1fr) auto;display:grid;position:relative;overflow:hidden}.chat-header{border-bottom:1px solid var(--canvas-line);justify-content:space-between;align-items:center;gap:20px;min-height:72px;padding:14px clamp(20px,3vw,40px);display:flex}.chat-header h2{font:650 var(--type-heading)/1.1 var(--display);letter-spacing:-.025em;margin:0;display:inline}.chat-header-main{align-items:center;gap:8px;min-width:0;display:flex}.chat-list-button,.chat-settings-button{min-height:36px;color:var(--canvas-muted);border:1px solid var(--canvas-line);font:600 var(--type-utility)/1 var(--body);white-space:nowrap;cursor:pointer;background:0 0;border-radius:7px;align-items:center;gap:5px;padding:0 9px;display:inline-flex}.chat-list-button:hover,.chat-settings-button:hover{color:var(--canvas-text);border-color:var(--canvas-muted)}.chat-list-button svg{fill:none;stroke:currentColor;stroke-width:1.5px;width:16px}.chat-title{align-items:baseline;gap:10px;min-width:0;display:flex}.botanical-subtitle{color:#11151275;font:var(--type-caption)/1 var(--body)}.chat-header-meta{color:#1115128c;font:var(--type-utility)/1.2 var(--mono);letter-spacing:0;grid-template-columns:repeat(2,auto);gap:5px 13px;display:grid}.live-label{color:#46622f;grid-column:span 2;justify-self:end;align-items:center;gap:7px;display:flex}.live-label i{width:5px;height:5px;box-shadow:none;background:#668a45}.live-label.is-dormant{color:#1115127a}.live-label.is-dormant i{box-shadow:none;background:#1115125c}.runtime-error{z-index:4;color:var(--paper);border:1px solid var(--rust);background:#111512f5;justify-content:space-between;align-items:center;gap:16px;padding:11px 13px;display:flex;position:absolute;top:105px;left:clamp(21px,3vw,44px);right:clamp(21px,3vw,44px);box-shadow:4px 4px #c96f433d}.runtime-error div{gap:3px;display:grid}.runtime-error strong{color:#ff9a68;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.runtime-error span{font:var(--type-caption)/1.35 var(--mono);overflow-wrap:anywhere}.runtime-error button{min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:1px solid #e8dfca4d;flex:none;padding:5px 8px}.runtime-error button:disabled{cursor:wait;opacity:.5}.runtime-error.download-retry button{max-width:132px;line-height:1.25}.chat-ruler{color:#11151266;font:var(--type-utility)/1 var(--mono);background:repeating-linear-gradient(90deg,#0000 0 14px,#11151233 14px 15px) 0 100%/100% 4px no-repeat;border-bottom:1px solid #11151217;justify-content:space-between;padding-inline:clamp(21px,3vw,44px);display:flex}.transcript{scrollbar-color:#11151247 transparent;min-height:0;padding:20px clamp(21px,4vw,64px) 32px;overflow-y:auto}.transcript-date{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:0;align-items:center;gap:10px;margin-bottom:22px;display:flex}.transcript-date:before,.transcript-date:after{display:none}.transcript-empty{text-align:center;color:#11151280;max-width:430px;margin:12vh auto 0}.transcript-empty h3{color:var(--ink);font:650 22px/1.2 var(--display);margin:0 0 8px}.transcript-empty p{font-size:var(--type-body);margin:0;line-height:1.5}.system-instructions-button,.conversation-list-empty button,.new-chat-button{min-height:42px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:650 var(--type-caption)/1 var(--body);cursor:pointer;border-radius:7px;padding:0 13px}.system-instructions-button{margin-top:18px}.conversation-library{grid-template-rows:auto minmax(0,1fr)}.conversation-list-header>div{gap:4px;display:grid}.conversation-list-header p{color:var(--canvas-muted);font-size:var(--type-caption);margin:0}.conversation-list{scrollbar-color:var(--canvas-muted) transparent;min-height:0;padding:clamp(20px,4vw,48px);overflow-y:auto}.conversation-row{border:1px solid var(--canvas-line);background:var(--canvas-field);border-radius:9px;grid-template-columns:minmax(0,1fr) auto;align-items:stretch;max-width:850px;margin:0 auto 9px;display:grid}.conversation-row.is-current{border-color:#738066b8}.conversation-open{min-width:0;color:var(--canvas-text);text-align:left;cursor:pointer;background:0 0;border:0;gap:6px;padding:15px 16px;display:grid}.conversation-title-line{align-items:center;gap:9px;min-width:0;display:flex}.conversation-title-line strong{font:650 var(--type-body)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.conversation-title-line small{color:#52712f;font:600 var(--type-utility)/1 var(--body);flex:none}.conversation-open>span:nth-child(2){color:var(--canvas-muted);font-size:var(--type-caption);text-overflow:ellipsis;white-space:nowrap;line-height:1.35;overflow:hidden}.conversation-open>small{color:var(--canvas-muted);font:var(--type-utility)/1.2 var(--mono)}.conversation-actions{border-left:1px solid var(--canvas-line);align-content:center;gap:4px;padding:7px;display:grid}.conversation-actions button{min-height:34px;color:var(--canvas-muted);font:600 var(--type-utility)/1 var(--body);cursor:pointer;background:0 0;border:0;padding:0 8px}.conversation-actions button:hover{color:var(--canvas-text)}.conversation-actions .conversation-delete{color:var(--rust)}.conversation-list-empty{max-width:430px;color:var(--canvas-muted);text-align:center;justify-items:center;gap:10px;margin:15vh auto 0;display:grid}.conversation-list-empty h3,.conversation-list-empty p{margin:0}.conversation-list-empty h3{color:var(--canvas-text);font:650 22px/1.2 var(--display)}.conversation-list-empty button{margin-top:8px}.message{max-width:760px;margin-bottom:25px}.message>header{color:#11151278;font:var(--type-utility)/1.2 var(--mono);letter-spacing:0;align-items:center;gap:8px;margin-bottom:8px;display:flex}.message>header strong{color:var(--ink)}.message>header time{margin-left:auto}.message-ordinal{color:#4d6728;min-width:22px}.message-content{font:var(--type-body)/1.58 var(--body)}.message-content>p{margin:0}.markdown-content>:first-child{margin-top:0}.markdown-content>:last-child{margin-bottom:0}.markdown-content p,.markdown-content ul,.markdown-content ol,.markdown-content blockquote{margin:0 0 10px}.markdown-content ul,.markdown-content ol{padding-left:22px}.markdown-content h3,.markdown-content h4,.markdown-content h5{font-family:var(--display);margin:16px 0 7px;font-weight:500;line-height:1.1}.markdown-content blockquote{color:#111512ad;white-space:pre-wrap;border-left:2px solid #5073248c;padding-left:12px}.markdown-content code{font:.78em/1.4 var(--mono);background:#11151214;padding:1px 4px}.markdown-content pre{max-width:100%;color:var(--paper);background:var(--ink);border-left:2px solid var(--phosphor);margin:11px 0;padding:12px;position:relative;overflow:auto}.markdown-content pre code{white-space:pre;background:0 0;padding:0}.markdown-content a{color:#3f6417;text-underline-offset:2px;text-decoration-thickness:1px}.message-user .markdown-content code{background:#e8dfca1f}.reasoning-block{color:#1115129e;font-size:var(--type-caption);background:#11151208;border:1px solid #11151229;border-radius:8px;margin:0 0 11px;padding:8px 10px}.reasoning-block summary{color:#4d6728;font:600 var(--type-caption)/1.2 var(--body);cursor:pointer}.reasoning-block[open] summary{margin-bottom:8px}.generation-placeholder{color:#11151273;margin:0;font-style:italic}.message-error{color:#8d321b;font:var(--type-caption)/1.4 var(--mono);margin:9px 0 0}.message.has-error{border-color:var(--rust)}.message-assistant{border-left:2px solid #546f3c73;padding-left:22px}.message-assistant .message-content>p:first-letter{float:none;color:inherit;font:inherit;margin:0}.message-user{max-width:min(78%,610px);color:var(--paper);background:var(--ink);border-radius:10px;margin-left:auto;padding:12px 15px 13px}.message-user>header,.message-user>header strong{color:#e8dfca8f}.message-user .message-ordinal{color:var(--phosphor)}.message>footer{color:#1115126e;font:var(--type-utility)/1.2 var(--mono);border-top:1px solid #11151221;gap:12px;margin-top:9px;padding-top:7px;display:flex}.stream-cursor{vertical-align:-2px;background:#507324;width:7px;height:14px;margin-left:4px;animation:.86s step-end infinite cursor-blink;display:inline-block}.tool-call{background:#1115120b;border:1px solid #1115122e;margin-top:13px}.tool-call summary{cursor:pointer;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;padding:8px 10px;list-style:none;display:grid}.tool-call summary::-webkit-details-marker{display:none}.tool-call code{font:var(--type-caption)/1.4 var(--mono)}.tool-call summary>span:last-child{color:#1115127a;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.tool-state,.event-pip{background:var(--moss);border-radius:50%;width:6px;height:6px}.tool-state-complete,.event-complete{background:#55772c}.tool-state-running,.event-running{background:var(--phosphor);box-shadow:0 0 0 3px #5a782b26}.tool-state-error,.event-error{background:var(--rust)}.tool-call>div{border-top:1px solid #11151221;padding:9px 10px}.tool-call>div p{color:#11151294;font-size:var(--type-caption);margin:5px 0 0}.composer{border-top:1px solid var(--canvas-line);background:var(--canvas-raised);padding:12px clamp(21px,3vw,44px) 18px;position:relative}.composer:before{display:none}.composer-activity{color:var(--paper);background:var(--ink);border:1px solid #141613e6;border-radius:8px;gap:10px;margin-bottom:12px;padding:12px 14px;display:grid}.activity-status,.activity-status>div{align-items:center;display:flex}.activity-status{min-width:0;font:var(--type-caption)/1.2 var(--body);justify-content:space-between;gap:12px}.activity-status>div{gap:10px;min-width:0}.activity-status>div>span:last-child{gap:3px;min-width:0;display:grid}.activity-status strong,.activity-status small{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.activity-status strong{color:var(--paper);font-weight:600}.activity-status small{color:var(--paper-ghost);font-size:var(--type-utility)}.activity-status>b{color:var(--phosphor);font:600 var(--type-caption)/1 var(--mono);flex:none}.activity-pulse{background:var(--phosphor);border-radius:2px;flex:0 0 5px;width:5px;height:22px}.activity-track{background:#f1f2ed1f;border-radius:3px;height:6px;overflow:hidden}.activity-track>span{border-radius:inherit;background:var(--phosphor);height:100%;transition:width .12s linear;display:block}.composer-meta{color:#11151275;min-height:24px;font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px;display:flex}.tool-toggle{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);cursor:pointer;background:0 0;border:0;align-items:center;gap:5px;padding:0 2px;display:flex}.tool-toggle.is-on{color:#425f1f}.regenerate-button{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);cursor:pointer;background:0 0;border:0;margin-right:auto;padding:0 2px}.regenerate-button:disabled{cursor:default;opacity:.45}.tool-toggle svg{fill:none;stroke:currentColor;stroke-width:1.3px;width:12px}.composer-field{border:1px solid var(--canvas-text);background:var(--canvas-field);border-radius:10px;grid-template-columns:1fr auto;min-height:84px;display:grid;position:relative;overflow:hidden}.composer-field label{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:0;position:absolute;top:7px;left:11px}.composer-field textarea{resize:none;min-width:0;color:var(--canvas-text);font:var(--type-body)/1.45 var(--body);background:0 0;border:0;outline:none;padding:23px 13px 9px}.composer-field textarea::placeholder{color:#11151257}.send-button{min-width:112px;min-height:52px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:700 var(--type-caption)/1 var(--mono);cursor:pointer;border-radius:8px;justify-content:center;align-items:center;gap:7px;margin:7px;transition:background .15s,opacity .15s;display:flex}.send-button:hover:not(:disabled){background:#b6df87}.send-button:disabled{opacity:.34}.send-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:15px}.send-button.stop{color:var(--paper);background:var(--ink)}.send-button.stop span{background:var(--rust);width:7px;height:7px}.send-button.load{background:var(--phosphor)}.station-frame[data-theme=dark] .botanical-subtitle,.station-frame[data-theme=dark] .chat-header-meta,.station-frame[data-theme=dark] .transcript-date,.station-frame[data-theme=dark] .transcript-empty,.station-frame[data-theme=dark] .message>header,.station-frame[data-theme=dark] .message>footer,.station-frame[data-theme=dark] .composer-meta,.station-frame[data-theme=dark] .tool-toggle,.station-frame[data-theme=dark] .regenerate-button,.station-frame[data-theme=dark] .composer-field label{color:var(--canvas-muted)}.station-frame[data-theme=dark] .transcript-empty h3,.station-frame[data-theme=dark] .message>header strong{color:var(--canvas-text)}.station-frame[data-theme=dark] .live-label{color:#a8d574}.station-frame[data-theme=dark] .conversation-title-line small{color:var(--phosphor)}.station-frame[data-theme=dark] .live-label.is-dormant{color:var(--canvas-muted)}.station-frame[data-theme=dark] .live-label.is-dormant i{background:var(--canvas-muted)}.station-frame[data-theme=dark] .transcript{scrollbar-color:#edf0e83d transparent}.station-frame[data-theme=dark] .markdown-content blockquote,.station-frame[data-theme=dark] .reasoning-block,.station-frame[data-theme=dark] .tool-call>div p{color:#edf0e8b3}.station-frame[data-theme=dark] .markdown-content code{background:#edf0e817}.station-frame[data-theme=dark] .markdown-content pre{color:var(--paper);background:#0f110e}.station-frame[data-theme=dark] .markdown-content a,.station-frame[data-theme=dark] .reasoning-block summary{color:var(--phosphor)}.station-frame[data-theme=dark] .reasoning-block,.station-frame[data-theme=dark] .tool-call{border-color:var(--canvas-line);background:#edf0e809}.station-frame[data-theme=dark] .generation-placeholder,.station-frame[data-theme=dark] .tool-call summary>span:last-child,.station-frame[data-theme=dark] .composer-field textarea::placeholder{color:#edf0e866}.station-frame[data-theme=dark] .message-assistant{border-left-color:#a8d57470}.station-frame[data-theme=dark] .message-user{color:var(--canvas-text);border:1px solid var(--canvas-line);background:#11130f}.station-frame[data-theme=dark] .message>footer,.station-frame[data-theme=dark] .tool-call>div{border-color:var(--canvas-line)}.station-frame[data-theme=dark] .tool-toggle.is-on{color:var(--phosphor)}.station-frame[data-theme=dark] .composer-field textarea{color:var(--canvas-text)}.station-frame[data-theme=dark] .composer-field{border-color:#edf0e84d}.station-frame[data-theme=dark] .send-button{color:var(--ink);border-color:#0f110e}.station-frame[data-theme=dark] .send-button.stop{color:var(--canvas-text);border-color:var(--canvas-line);background:#11130f}.inspector{border-left:1px solid var(--line-light);scrollbar-color:#e8dfca2e transparent;grid-area:inspector;height:calc(100dvh - 64px);min-height:calc(100dvh - 64px);overflow-y:auto}.inspector-section{border-bottom:1px solid var(--line-light);padding:20px 18px}.section-heading{justify-content:space-between;align-items:center;margin-bottom:17px}.section-heading.compact{margin-bottom:14px}.section-heading>div{flex:1}.section-heading h2{font-size:20px}.sampling-label{color:var(--paper-ghost);border:1px solid var(--line-light);font:var(--type-utility)/1 var(--body);border-radius:6px;padding:3px 6px}.sampling-label.phase-prefill,.sampling-label.phase-decode{color:var(--phosphor);border-color:#a8d57459}.speed-gauge{grid-template-columns:116px 1fr;align-items:center;gap:15px;display:grid}.gauge-ring{background:radial-gradient(circle, var(--ink) 0 56%, transparent 57%), conic-gradient(var(--phosphor) 0 var(--gauge), #f1f2ed1c var(--gauge) 100%);border-radius:50%;place-items:center;width:116px;height:116px;display:grid;box-shadow:inset 0 0 0 1px #f1f2ed14}.gauge-ring:after{display:none}.gauge-ring>div{z-index:1;justify-items:center;min-width:0;display:grid;position:relative}.gauge-ring strong{color:var(--phosphor);font:600 clamp(24px, 2vw, 29px)/1 var(--display);white-space:nowrap}.gauge-ring small{color:var(--paper-ghost);font:600 10px/1 var(--mono);letter-spacing:.02em;text-transform:uppercase;justify-items:center;gap:2px;margin-top:3px;display:grid}.gauge-ring small span{white-space:nowrap}.speed-gauge dl{margin:0}.speed-gauge dl div{border-bottom:1px solid var(--line-light);font:var(--type-utility)/1 var(--mono);justify-content:space-between;padding:7px 0;display:flex}.speed-gauge dt{color:#e8dfca6e}.speed-gauge dd{color:var(--paper);margin:0}.telemetry-live-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:16px;display:grid}.telemetry-metric{border:1px solid var(--line-light);background:var(--ink-raised);border-radius:8px;gap:6px;min-width:0;padding:10px;display:grid}.telemetry-metric.is-active{border-color:#a8d57466}.telemetry-metric>div:first-child{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.telemetry-metric span,.telemetry-metric small{color:var(--paper-ghost);font-size:var(--type-utility)}.telemetry-metric strong{color:var(--paper);font:600 var(--type-caption)/1 var(--mono);white-space:nowrap}.telemetry-metric.is-active strong{color:var(--phosphor)}.telemetry-meter{background:#f1f2ed1a;border-radius:2px;height:4px;overflow:hidden}.telemetry-meter>span{border-radius:inherit;background:var(--phosphor);height:100%;transition:width .12s linear;display:block}.device-card{border:1px solid var(--line-light);background:var(--ink-raised);border-radius:8px;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;margin:19px 0 14px;padding:10px;display:grid}.device-icon{border:1px solid #b9ff4540;align-items:flex-end;gap:3px;width:28px;height:28px;padding:5px;display:flex}.device-icon i{background:var(--phosphor);width:4px}.device-icon i:first-child{opacity:.45;height:7px}.device-icon i:nth-child(2){height:15px}.device-icon i:nth-child(3){opacity:.7;height:11px}.device-card>div{gap:3px;display:grid}.device-card small,.memory-grid small,.meter small{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.device-card strong{font:500 var(--type-caption)/1 var(--mono)}.device-card>span:last-child{color:var(--phosphor);font:600 15px/1 var(--mono);justify-items:end;display:grid}.device-card>span:last-child small{color:#e8dfca5e;font-size:var(--type-utility);margin-top:3px}.meter{margin-top:13px}.meter>div:first-child{font:var(--type-utility)/1 var(--mono);justify-content:space-between;margin-bottom:6px;display:flex}.meter-track{border:1px solid #e8dfca36;height:5px;padding:1px}.meter-track>span{background:var(--phosphor);height:100%;display:block}.memory-grid{background:var(--line-light);grid-template-columns:1fr 1fr;gap:1px;margin-top:14px;display:grid}.memory-grid>div{background:var(--ink);gap:6px;padding:10px;display:grid}.memory-grid strong{font:500 var(--type-caption)/1 var(--mono)}.event-log{gap:0;margin:0;padding:0;list-style:none;display:grid;position:relative}.event-log:before{content:"";background:var(--line-light);width:1px;position:absolute;top:5px;bottom:5px;left:3px}.event-log li{grid-template-columns:8px 64px 1fr;align-items:start;gap:8px;padding:7px 0;display:grid}.event-log li.event-empty{color:#e8dfca5c;font:var(--type-utility)/1.4 var(--mono);padding:5px 0;display:block}.event-pip{z-index:1;border:1px solid var(--ink);margin-top:2px}.event-log time{color:#e8dfca57;font:var(--type-utility)/1.2 var(--mono)}.event-log div{gap:3px;display:grid}.event-log strong{font:500 var(--type-caption)/1 var(--mono)}.event-log small{color:#e8dfca6b;font-size:var(--type-utility)}.artifact-close{border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:6px;width:25px;height:25px}.artifact-empty{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--body);margin:0}.artifact-pane{border:1px solid var(--line-light);background:#f8f5eb}.artifact-tabs{color:var(--paper);border-bottom:1px solid var(--line-light);background:var(--ink-raised);grid-template-columns:auto auto 1fr;align-items:center;display:grid}.artifact-tabs button{border:0;border-right:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:7px 9px}.artifact-tabs button[aria-selected=true]{color:var(--ink);background:var(--phosphor)}.artifact-tabs span{color:#e8dfca73;font:var(--type-utility)/1 var(--mono);text-align:right;text-overflow:ellipsis;white-space:nowrap;padding:0 8px;overflow:hidden}.artifact-pane iframe{background:#fff;border:0;width:100%;height:260px;display:block}.artifact-running{position:relative}.artifact-running>button{z-index:2;color:var(--paper);border:1px solid var(--rust);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:#111512e6;padding:5px 7px;position:absolute;top:7px;right:7px}.artifact-launch{min-height:180px;color:var(--ink);text-align:center;align-content:center;justify-items:center;gap:13px;padding:20px;display:grid}.artifact-launch p{color:#1115129e;max-width:250px;font:var(--type-caption)/1.5 var(--mono);margin:0}.artifact-launch button{color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);min-height:44px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;padding:8px 10px}.artifact-code{height:260px;color:var(--paper);font:var(--type-utility)/1.5 var(--mono);white-space:pre-wrap;background:#0d100e;margin:0;padding:10px;overflow:auto}.settings-layer{z-index:50;background:#080b09a3;justify-content:flex-end;animation:.16s both station-in;display:flex;position:fixed;inset:0}.settings-sheet{width:min(430px,100%);min-height:100%;color:var(--paper);border-left:1px solid var(--phosphor);background:var(--ink);padding:30px 27px;animation:.3s cubic-bezier(.16,1,.3,1) both sheet-in;overflow-y:auto;box-shadow:-24px 0 70px #00000052}.settings-sheet>header{border-bottom:1px solid var(--line-light);justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:37px;padding-bottom:20px;display:flex}.settings-sheet h2{font:400 31px/1 var(--display);margin:0}.sheet-close{border:1px solid var(--line-light);width:35px;height:35px;font:300 22px/1 var(--body);cursor:pointer;background:0 0;border-radius:50%}.sheet-close:hover{color:var(--phosphor);border-color:var(--phosphor)}.setting-group{border-bottom:1px solid var(--line-light);grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;padding:15px 0;display:grid}.setting-group>div,.range-setting>span:first-child{gap:5px;display:grid}.setting-group small,.range-setting small{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.setting-group strong,.range-setting strong{font:500 var(--type-caption)/1 var(--mono)}.setting-locked{color:#e8dfca5c;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.setting-select select{max-width:112px;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);min-height:44px;font:var(--type-utility)/1 var(--mono);border-radius:0;padding:6px 7px}.range-setting{gap:13px;margin-top:29px;display:grid}.range-setting>span:first-child{justify-content:space-between;align-items:end;display:flex}.range-setting input{appearance:none;background:linear-gradient(90deg, var(--phosphor) 0 35%, #e8dfca1c 35%);border:1px solid #e8dfca3d;border-radius:0;width:100%;height:4px;margin:0}.range-setting:first-of-type input{background:var(--phosphor)}.range-setting input::-webkit-slider-thumb{appearance:none;border:2px solid var(--ink);background:var(--phosphor);width:12px;height:18px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab}.range-setting input::-moz-range-thumb{border:2px solid var(--ink);background:var(--phosphor);width:10px;height:16px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab;border-radius:0}.range-ticks{pointer-events:none;justify-content:space-between;margin-top:-13px;display:flex}.range-limits{color:#e8dfca59;font:var(--type-utility)/1 var(--mono);justify-content:space-between;display:flex}.range-limits i{font-style:normal}.system-prompt-setting{gap:9px;margin-top:29px;display:grid}.system-prompt-setting>span{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;display:flex}.system-prompt-setting textarea{resize:vertical;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);font:var(--type-caption)/1.5 var(--mono);padding:10px}.settings-actions{grid-template-columns:1fr 1fr;gap:7px;margin-top:29px;display:grid}.settings-actions button{color:var(--paper);border:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:9px}.settings-actions button:hover{color:var(--phosphor);border-color:var(--phosphor)}.settings-actions .danger-action{color:#e78a62;border-color:#c96f4373;grid-column:span 2}.boot-screen{min-height:100dvh;color:var(--paper);background:var(--ink);justify-content:center;align-items:center;gap:18px;padding:24px;display:flex}.boot-screen h1{color:var(--phosphor);font:400 34px/1 var(--display);margin:0}.boot-screen p:last-child{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--mono)}.range-ticks i{background:#e8dfca40;width:1px;height:5px}.settings-note{background:#b9ff450f;border:1px solid #b9ff4540;align-items:flex-start;gap:13px;margin-top:42px;padding:15px;display:flex}.settings-note .signal-dot{flex:0 0 7px;margin-top:6px}.settings-note p{color:#e8dfca94;font-size:var(--type-caption);margin:0;line-height:1.5}.settings-note strong{color:var(--phosphor)}@keyframes station-in{0%{opacity:0}to{opacity:1}}@keyframes rise-in{0%{opacity:0;transform:translateY(11px)}to{opacity:1;transform:translateY(0)}}@keyframes note-in{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes cursor-blink{0%,52%{opacity:1}53%,to{opacity:0}}@keyframes activity-pulse{0%,to{opacity:.55;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}@keyframes sheet-in{0%{opacity:0;transform:translate(34px)}to{opacity:1;transform:translate(0)}}@media (width<=1279px){.station-header{grid-template-columns:240px minmax(260px,1fr) auto}.browser-notice>span>span{display:none}.browser-notice.is-warning>span>span{display:block}.workspace-grid{grid-template-columns:240px minmax(0,1fr);grid-template-areas:"models chat""inspector inspector"}.workspace-grid.is-models-collapsed,.workspace-grid.is-models-collapsed.is-inspector-collapsed{grid-template-columns:0 minmax(0,1fr)}.workspace-grid.is-inspector-collapsed:not(.is-models-collapsed){grid-template-columns:240px minmax(0,1fr)}.model-rail,.chat-surface{height:calc(100dvh - 64px);min-height:calc(100dvh - 64px)}.inspector{border-top:1px solid var(--line-light);border-left:0;grid-template-columns:repeat(2,minmax(0,1fr));height:auto;min-height:auto;display:grid;overflow:visible}.inspector-section{border-right:1px solid var(--line-light);border-bottom:0}}@media (width<=820px){.station-header{grid-template-columns:minmax(0,1fr) auto;min-height:58px;display:grid}.brand-block{border-right:0;min-width:0;padding:10px 14px}.brand-glyph{flex-basis:32px;width:32px;height:32px}.browser-notice{border-top:1px solid var(--line-light);grid-area:2/1/auto/-1;min-height:38px;padding:7px 14px}.header-actions{border-left:0;grid-area:1/2;padding-inline:10px;position:static}.workspace-grid{display:block}.model-rail,.chat-surface,.inspector{height:auto;min-height:auto}.model-rail{border-right:0;border-bottom:1px solid var(--line-light);padding:18px 15px;display:block}.model-list{scroll-snap-type:x mandatory;gap:7px;padding-bottom:7px;display:flex;overflow-x:auto}.model-card{scroll-snap-align:start;min-width:205px}.rail-footnote{display:none}.chat-surface{grid-template-rows:auto minmax(240px,auto) auto;min-height:calc(100dvh - 96px);display:grid;overflow:visible;width:100%!important}.transcript{padding-inline:22px;overflow:visible}.chat-header{padding:17px 19px}.chat-header-meta span:not(.live-label){display:none}.live-label{grid-column:1}.composer{z-index:5;padding-inline:14px;position:relative}.transcript-empty{margin-top:4vh}.inspector{border-top:1px solid var(--line-light);display:block}.inspector-section{border-right:0;border-bottom:1px solid var(--line-light)}}@media (width<=520px){.brand-block h1{font-size:18px}.brand-block>div>span{display:none}.layout-controls{gap:2px}.layout-controls button,.icon-button{width:32px;height:32px}.chat-header{align-items:flex-end}.botanical-subtitle{display:none}.message-user{max-width:91%}.composer-meta>span{display:none}.composer-activity{padding-inline:10px}.activity-status small{max-width:210px}.composer-field{grid-template-columns:1fr;padding-bottom:62px}.send-button{height:52px;position:absolute;bottom:0;right:0}.speed-gauge{grid-template-columns:106px 1fr}.gauge-ring{width:102px;height:102px}.gauge-ring:after{display:none}.telemetry-live-grid{grid-template-columns:1fr}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.bench-page{min-height:100dvh;color:var(--ink);background:var(--paper)}.bench-masthead{min-height:86px;color:var(--paper);border-bottom:1px solid var(--phosphor);background:var(--ink);grid-template-columns:minmax(310px,1fr) minmax(360px,1.2fr) auto;align-items:stretch;display:grid}.bench-masthead .brand-block h1{color:var(--paper)}.bench-masthead-note{border-inline:1px solid var(--line-light);background:linear-gradient(110deg,#b9ff4517,#0000 72%);flex-direction:column;justify-content:center;gap:5px;padding:14px 28px;display:flex}.bench-masthead-note strong{color:var(--phosphor);font:700 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.bench-masthead-note span{color:#e8dfca8c;max-width:520px;font:10px/1.35 var(--body)}.bench-return{color:var(--paper);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;align-items:center;gap:8px;padding:0 25px;text-decoration:none;display:flex}.bench-return:hover{color:var(--phosphor)}.bench-layout{grid-template-columns:minmax(275px,330px) minmax(0,1fr);min-height:calc(100dvh - 86px);display:grid}.bench-controls{color:var(--paper);border-right:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at -12% 102%, transparent 0 24px, #b9ff450b 25px 26px), var(--ink);padding:28px 22px}.bench-controls h2{font:400 30px/1 var(--display);letter-spacing:-.03em;margin:0}.bench-control-intro{color:#e8dfca80;margin:9px 0 22px;font-size:11px;line-height:1.45}.bench-controls form{gap:15px;display:grid}.bench-field{gap:6px;min-width:0;display:grid}.bench-field>span{color:#e8dfcac7;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-field select,.bench-field input{width:100%;min-width:0;height:39px;color:var(--paper);background:var(--ink-raised);font:10px/1 var(--mono);border:1px solid #e8dfca38;border-radius:0;outline:0;padding:0 10px}.bench-field input::placeholder{color:#e8dfca40}.bench-field select:disabled,.bench-field input:disabled{opacity:.5}.bench-field small{color:#e8dfca59;font:8px/1.2 var(--mono)}.bench-field-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.bench-experimental{background:#c96f430d;border:1px dashed #c96f437a;padding:11px}.bench-experimental summary{color:#e19a76;font:700 8px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;cursor:pointer}.bench-experimental>p{color:#e8dfca75;margin:10px 0;font-size:9px;line-height:1.4}.bench-locks{color:#e8dfca70;font:7px/1.35 var(--mono);gap:5px;margin:10px 0 0;display:grid}.bench-locks div{justify-content:space-between;gap:8px;display:flex}.bench-locks dt,.bench-locks dd{margin:0}.bench-locks dt{color:#e8dfcab8}.bench-gate{border-left:2px solid var(--rust);background:#c96f4314;gap:5px;padding:10px;display:grid}.bench-gate.pass{border-color:var(--phosphor);background:var(--phosphor-dim)}.bench-gate strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase}.bench-gate.pass strong{color:var(--phosphor)}.bench-gate span{color:#e8dfca80;font:8px/1.4 var(--mono);overflow-wrap:anywhere}.bench-run-actions{grid-template-columns:1fr auto;gap:8px;display:grid}.bench-run-button,.bench-stop-button,.bench-section-heading button{border:1px solid var(--ink);background:var(--phosphor);min-height:38px;font:800 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;border-radius:0;padding:0 13px}.bench-run-button:disabled{opacity:.35}.bench-stop-button{color:#ffb08a;border-color:var(--rust);background:0 0}.bench-workbench{background:linear-gradient(90deg, #1115120b 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120a 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:0;padding:clamp(26px,4vw,58px)}.bench-hero{border-bottom:1px solid var(--line-dark);grid-template-columns:minmax(320px,1.3fr) minmax(260px,.7fr);align-items:end;gap:34px;padding-bottom:29px;display:grid}.bench-hero h2{max-width:690px;font:400 clamp(42px, 6vw, 82px)/.84 var(--display);letter-spacing:-.065em;margin:0}.bench-hero h2 em{color:#4f7125;font-weight:400}.bench-hero>p{color:#11151294;max-width:430px;margin:0;font-size:11px;line-height:1.55}.bench-progress{gap:8px;padding:16px 0;display:grid}.bench-progress>div{font:8px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;gap:20px;display:flex}.bench-progress>div span{color:#11151273}.bench-progress-track{background:#11151221;height:3px}.bench-progress-track i{background:#527426;height:100%;transition:width .18s;display:block}.bench-error{color:var(--paper);border-left:3px solid var(--rust);background:var(--ink);gap:5px;margin-bottom:18px;padding:12px 14px;display:grid}.bench-error strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-error span{font:9px/1.4 var(--mono);overflow-wrap:anywhere}.bench-core-section{margin-top:13px}.bench-section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:13px;display:flex}.bench-section-heading h3,.bench-output h3{font:500 24px/1 var(--display);margin:0}.bench-section-heading button{min-height:31px;color:var(--paper);border-color:var(--ink);background:var(--ink)}.bench-core-sample{border:1px solid var(--ink);background:var(--paper-deep);grid-template-columns:repeat(4,minmax(0,1fr));display:grid;position:relative;overflow:hidden}.bench-core-sample:before{content:"";pointer-events:none;opacity:.35;background:repeating-radial-gradient(#0000 0 34px,#1115122b 35px 36px);position:absolute;inset:-130% -5%}.bench-core-sample article{background:#e8dfca91;border-right:1px solid #11151242;flex-direction:column;justify-content:space-between;min-height:128px;padding:17px;display:flex;position:relative}.bench-core-sample article:nth-child(2n){background:#d8cdb499}.bench-core-sample article:last-child{color:var(--paper);background:#111512e8;border-right:0}.bench-core-sample article>span{font:800 8px/1 var(--mono);letter-spacing:.09em;text-transform:uppercase}.bench-core-sample article strong{font:400 clamp(28px, 3vw, 43px)/1 var(--display);letter-spacing:-.04em;margin-block:auto}.bench-core-sample article small{color:#11151280;font:7px/1.2 var(--mono);text-transform:uppercase}.bench-core-sample article:last-child small{color:#e8dfca7a}.bench-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px;display:grid}.bench-data-card{border:1px solid var(--line-dark);background:#e8dfcaad;padding:16px}.bench-runtime-card,.bench-tuning-card{grid-column:span 2}.bench-data-card dl{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 26px;margin:0;display:grid}.bench-runtime-card dl{grid-template-columns:minmax(180px,1.5fr) repeat(4,minmax(100px,.7fr))}.bench-tuning-card dl{grid-template-columns:repeat(4,minmax(0,1fr))}.bench-data-card dl div{border-top:1px solid #11151221;gap:4px;min-width:0;padding-top:8px;display:grid}.bench-data-card dt{color:#11151273;font:7px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase}.bench-data-card dd{min-width:0;font:500 16px/1.2 var(--body);text-overflow:ellipsis;white-space:nowrap;margin:0;overflow:hidden}.bench-output{border:1px dashed #1115124d;grid-template-columns:repeat(2,minmax(0,1fr));gap:34px;margin-top:12px;padding:21px;display:grid}.bench-output>div:last-child{border-left:1px solid var(--line-dark);padding-left:34px}.bench-output p:not(.eyebrow){color:#11151299;white-space:pre-wrap;margin:8px 0 0;font-size:11px;line-height:1.5}@media (width<=1050px){.bench-masthead{grid-template-columns:minmax(310px,1fr) auto}.bench-masthead-note{display:none}.bench-layout{grid-template-columns:285px minmax(0,1fr)}.bench-core-sample{grid-template-columns:repeat(2,minmax(0,1fr))}.bench-core-sample article:nth-child(2){border-right:0}.bench-core-sample article:nth-child(-n+2){border-bottom:1px solid #11151242}.bench-runtime-card dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.bench-masthead{align-items:center;display:flex}.bench-masthead .brand-block{flex:1;min-width:0}.bench-return{padding:0 15px;font-size:0}.bench-return span{font-size:17px}.bench-layout{display:block}.bench-controls{border-right:0;border-bottom:1px solid var(--phosphor)}.bench-workbench{padding:28px 16px}.bench-hero,.bench-output{grid-template-columns:1fr;gap:18px}.bench-hero h2{font-size:clamp(42px,14vw,68px)}.bench-detail-grid{display:block}.bench-data-card+.bench-data-card{margin-top:10px}.bench-output>div:last-child{border-top:1px solid var(--line-dark);border-left:0;padding-top:18px;padding-left:0}}@media (width<=480px){.bench-masthead .brand-mark{display:none}.bench-core-sample,.bench-data-card dl,.bench-runtime-card dl,.bench-tuning-card dl{grid-template-columns:1fr}.bench-core-sample article{border-bottom:1px solid #11151242;border-right:0;min-height:105px}.bench-core-sample article:last-child{border-bottom:0}}
dist/assets/index-DnMbdiUB.css DELETED
@@ -1 +0,0 @@
1
- :root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--ink:#111512;--ink-raised:#191e1a;--ink-soft:#252b25;--paper:#e8dfca;--paper-deep:#d8cdb4;--paper-ghost:#e8dfca9e;--phosphor:#b9ff45;--phosphor-dim:#b9ff4529;--moss:#6f8062;--rust:#c96f43;--line-light:#e8dfca24;--line-dark:#1115122e;--display:"Bodoni 72", Didot, "Iowan Old Style", Baskerville, serif;--body:"Avenir Next Condensed", "Gill Sans", "Trebuchet MS", sans-serif;--mono:"Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;--type-utility:clamp(.6875rem, .15vw + .65rem, .8125rem);--type-caption:clamp(.75rem, .18vw + .7rem, .875rem);--type-body:clamp(1rem, .22vw + .95rem, 1.125rem);--type-heading:clamp(1.75rem, 1.15vw + 1.45rem, 2.5rem);font-family:var(--body);font-synthesis:none;text-rendering:optimizelegibility}*{box-sizing:border-box}html{background:var(--ink)}body{background:linear-gradient(90deg, #1115120f 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120b 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:320px;min-height:100vh;color:var(--paper);font-size:var(--type-body);margin:0}button,textarea,select{font:inherit}button{color:inherit}button:focus-visible,textarea:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--phosphor);outline-offset:3px}button:disabled{cursor:not-allowed}::selection{color:var(--ink);background:var(--phosphor)}.station-frame{isolation:isolate;min-height:100dvh;animation:.58s cubic-bezier(.16,1,.3,1) both station-in}.station-frame:before{z-index:20;pointer-events:none;content:"";opacity:.14;mix-blend-mode:soft-light;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.3'/%3E%3C/svg%3E");position:fixed;inset:0}.station-header{background:var(--ink);border-bottom:1px solid var(--phosphor);z-index:10;grid-template-columns:minmax(330px,1fr) auto minmax(220px,1fr);align-items:stretch;min-height:76px;display:grid;position:relative;box-shadow:0 12px 38px #11151238}.brand-block{align-items:center;gap:14px;padding:12px 24px;display:flex}.brand-block h1{font:400 clamp(25px, 2vw, 34px) / .9 var(--display);letter-spacing:-.035em;margin:0}.brand-block h1 i{color:var(--phosphor);font-weight:400}.brand-mark{background:repeating-radial-gradient(at 52% 62%,#0000 0 4px,#b9ff456b 4.5px 5px);border:1px solid #b9ff457a;border-radius:50%;flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid;transform:rotate(-11deg)}.brand-mark:before,.brand-mark:after,.brand-mark span:before,.brand-mark span:after{content:"";background:var(--phosphor);transform-origin:bottom;width:1px;position:absolute}.brand-mark:before{height:25px;transform:translate(-1px,4px)rotate(0)}.brand-mark:after{height:13px;transform:translate(4px,-2px)rotate(43deg)}.brand-mark span:before{height:11px;transform:translate(-5px,-1px)rotate(-47deg)}.brand-mark span:after{height:8px;transform:translate(3px,-9px)rotate(72deg)}.eyebrow{color:#e8dfca85;font:600 var(--type-utility)/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;margin:0 0 5px}.eyebrow.ink{color:#11151285}.local-promise{border-inline:1px solid var(--line-light);background:linear-gradient(115deg,#b9ff4514,#0000 66%);align-items:center;gap:11px;min-width:360px;padding:0 28px;display:flex}.signal-dot,.live-label i,.sampling-label i{background:var(--phosphor);width:7px;height:7px;box-shadow:0 0 0 5px var(--phosphor-dim), 0 0 17px #b9ff459e;border-radius:50%;display:inline-block}.local-promise span:last-child{gap:3px;display:grid}.local-promise strong{color:var(--phosphor);font:600 var(--type-caption)/1 var(--mono);letter-spacing:.13em;text-transform:uppercase}.local-promise small{color:var(--paper-ghost);font:var(--type-caption)/1.25 var(--body)}.header-actions{justify-self:end;align-items:center;padding-right:18px;display:flex}.station-clock{color:var(--paper-ghost);font-family:var(--mono);justify-items:end;padding:0 18px;display:grid}.station-clock small{font-size:var(--type-utility);letter-spacing:.18em}.station-clock span{font-size:var(--type-caption);letter-spacing:.09em;margin-top:4px}.icon-button{cursor:pointer;background:0 0;border:1px solid #e8dfca3d;border-radius:50%;place-items:center;width:44px;height:44px;transition:color .16s,border-color .16s,transform .16s;display:grid}.icon-button:hover{color:var(--phosphor);border-color:var(--phosphor);transform:rotate(-7deg)}.icon-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:18px}.workspace-grid{grid-template-columns:clamp(280px,19vw,340px) minmax(560px,1fr) clamp(340px,23vw,410px);grid-template-areas:"models chat inspector";min-height:calc(100dvh - 76px);display:grid}.model-rail,.inspector{background:var(--ink);min-width:0}.model-rail{border-right:1px solid var(--line-light);flex-direction:column;grid-area:models;padding:24px 16px 14px;animation:.52s cubic-bezier(.16,1,.3,1) .1s both rise-in;display:flex;position:relative;overflow:hidden}.model-rail:before{z-index:0;content:"";opacity:.18;background:repeating-radial-gradient(ellipse at 51% 47%, transparent 0 7px, var(--moss) 7.5px 8.5px);border-radius:43% 57% 52% 48%;width:265px;height:265px;position:absolute;bottom:44px;left:-103px;transform:rotate(18deg)}.rail-heading,.section-heading{z-index:1;align-items:flex-start;gap:11px;display:flex;position:relative}.rail-heading>span,.section-heading>span:first-child{color:var(--phosphor);font:500 var(--type-utility)/1 var(--mono);border-top:1px solid var(--phosphor);padding-top:5px}.rail-heading h2,.section-heading h2{font:400 26px/1 var(--display);letter-spacing:-.025em;margin:0}.model-list{z-index:1;gap:5px;margin-top:21px;display:grid;position:relative}.model-card{border:1px solid #0000;transition:border-color .18s,background .18s}.model-card.is-active{border-color:var(--phosphor);background:var(--phosphor-dim);box-shadow:inset 3px 0 0 var(--phosphor)}.model-select{text-align:left;width:100%;min-height:76px;color:var(--paper);cursor:pointer;background:0 0;border:0;grid-template-columns:23px 1fr auto;gap:8px;padding:13px 9px 12px;display:grid}.model-card:not(.is-active) .model-select:hover{background:#e8dfca0e}.model-index{color:#e8dfca61;font:var(--type-utility)/1 var(--mono);padding-top:4px}.model-copy{gap:3px;min-width:0;display:grid}.model-title-line{justify-content:space-between;align-items:center;gap:7px;display:flex}.model-title-line strong{font:500 24px/1 var(--display)}.model-copy em{color:#e8dfca75;font:italic var(--type-caption)/1.2 var(--display);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.model-copy>span:last-child{color:var(--paper-ghost);font-size:var(--type-caption);margin-top:3px}.availability{font:600 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line-light);padding:2px 4px}.availability-loaded,.availability-loading,.availability-cached{color:var(--phosphor);border-color:#b9ff4552}.availability-remote,.availability-limited{color:var(--rust);border-color:#c96f4366}.model-footprint{color:#e8dfca63;font:var(--type-utility)/1 var(--mono);white-space:nowrap;align-self:end}.model-limit-reason{color:#e8dfca94;font:var(--type-utility)/1.35 var(--mono);margin:-4px 8px 9px 30px;display:block}.rail-footnote{z-index:1;color:#e8dfca4d;font:var(--type-utility)/1.2 var(--mono);letter-spacing:.03em;justify-content:space-between;gap:8px;margin-top:auto;display:flex;position:relative}.chat-surface{min-width:0;height:calc(100dvh - 76px);min-height:calc(100dvh - 76px);color:var(--ink);background:radial-gradient(ellipse at 82% 16%, transparent 0 77px, #1115120e 78px 79px, transparent 80px 89px, #1115120b 90px 91px, transparent 92px), linear-gradient(100deg, transparent 0 67%, #11151206 67.2% 67.35%, transparent 67.5%), var(--paper);grid-area:chat;grid-template-rows:auto 19px minmax(0,1fr) auto;animation:.56s cubic-bezier(.16,1,.3,1) .16s both rise-in;display:grid;position:relative;overflow:hidden}.chat-header{border-bottom:1px solid var(--line-dark);justify-content:space-between;align-items:center;gap:20px;min-height:82px;padding:17px clamp(21px,3vw,44px) 13px;display:flex}.chat-header h2{font:500 var(--type-heading)/1 var(--display);letter-spacing:-.045em;margin:0;display:inline}.botanical-subtitle{color:#11151275;font:italic var(--type-caption)/1 var(--display);margin-left:10px}.chat-header-meta{color:#1115128c;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;letter-spacing:.07em;grid-template-columns:repeat(2,auto);gap:5px 13px;display:grid}.live-label{color:#3d551d;grid-column:span 2;justify-self:end;align-items:center;gap:7px;display:flex}.live-label i{background:#5d7e2e;width:5px;height:5px;box-shadow:0 0 0 3px #5a782b21}.live-label.is-dormant{color:#1115127a}.live-label.is-dormant i{background:#1115125c;box-shadow:0 0 0 3px #11151217}.runtime-error{z-index:4;color:var(--paper);border:1px solid var(--rust);background:#111512f5;justify-content:space-between;align-items:center;gap:16px;padding:11px 13px;display:flex;position:absolute;top:105px;left:clamp(21px,3vw,44px);right:clamp(21px,3vw,44px);box-shadow:4px 4px #c96f433d}.runtime-error div{gap:3px;display:grid}.runtime-error strong{color:#ff9a68;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.runtime-error span{font:var(--type-caption)/1.35 var(--mono);overflow-wrap:anywhere}.runtime-error button{min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:1px solid #e8dfca4d;flex:none;padding:5px 8px}.runtime-error button:disabled{cursor:wait;opacity:.5}.runtime-error.download-retry button{max-width:132px;line-height:1.25}.chat-ruler{color:#11151266;font:var(--type-utility)/1 var(--mono);background:repeating-linear-gradient(90deg,#0000 0 14px,#11151233 14px 15px) 0 100%/100% 4px no-repeat;border-bottom:1px solid #11151217;justify-content:space-between;padding-inline:clamp(21px,3vw,44px);display:flex}.transcript{scrollbar-color:#11151247 transparent;min-height:0;padding:20px clamp(21px,4vw,64px) 32px;overflow-y:auto}.transcript-date{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;align-items:center;gap:10px;margin-bottom:22px;display:flex}.transcript-date:before,.transcript-date:after{content:"";background:#11151224;flex:1;height:1px}.transcript-empty{text-align:center;color:#11151280;max-width:430px;margin:12vh auto 0}.transcript-empty>span{color:#507324;font:42px/1 var(--display);display:block}.transcript-empty h3{color:var(--ink);font:400 25px/1 var(--display);margin:8px 0 6px}.transcript-empty p{font-size:var(--type-body);margin:0;line-height:1.5}.message{max-width:690px;margin-bottom:25px;animation:.43s cubic-bezier(.16,1,.3,1) both note-in}.message>header{color:#11151278;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;letter-spacing:.07em;align-items:center;gap:8px;margin-bottom:8px;display:flex}.message>header strong{color:var(--ink)}.message>header time{margin-left:auto}.message-ordinal{color:#4d6728;min-width:22px}.message-content{font:var(--type-body)/1.58 var(--body)}.message-content>p{margin:0}.markdown-content>:first-child{margin-top:0}.markdown-content>:last-child{margin-bottom:0}.markdown-content p,.markdown-content ul,.markdown-content ol,.markdown-content blockquote{margin:0 0 10px}.markdown-content ul,.markdown-content ol{padding-left:22px}.markdown-content h3,.markdown-content h4,.markdown-content h5{font-family:var(--display);margin:16px 0 7px;font-weight:500;line-height:1.1}.markdown-content blockquote{color:#111512ad;white-space:pre-wrap;border-left:2px solid #5073248c;padding-left:12px}.markdown-content code{font:.78em/1.4 var(--mono);background:#11151214;padding:1px 4px}.markdown-content pre{max-width:100%;color:var(--paper);background:var(--ink);border-left:2px solid var(--phosphor);margin:11px 0;padding:12px;position:relative;overflow:auto}.markdown-content pre code{white-space:pre;background:0 0;padding:0}.markdown-content a{color:#3f6417;text-underline-offset:2px;text-decoration-thickness:1px}.message-user .markdown-content code{background:#e8dfca1f}.reasoning-block{color:#1115129e;font-size:var(--type-caption);background:#11151209;border:1px dashed #1115123d;margin:0 0 11px;padding:8px 10px}.reasoning-block summary{color:#4d6728;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer}.reasoning-block[open] summary{margin-bottom:8px}.generation-placeholder{color:#11151273;margin:0;font-style:italic}.message-error{color:#8d321b;font:var(--type-caption)/1.4 var(--mono);margin:9px 0 0}.message.has-error{border-color:var(--rust)}.message-assistant{border-left:2px solid var(--ink);padding-left:22px}.message-assistant .message-content>p:first-letter{float:left;color:#38511d;font:42px/.75 var(--display);margin:1px 6px 0 0}.message-user{max-width:min(78%,610px);color:var(--paper);background:var(--ink);clip-path:polygon(0 0,calc(100% - 10px) 0,100% 10px,100% 100%,0 100%);margin-left:auto;padding:12px 15px 13px;box-shadow:5px 5px #1115121f}.message-user>header,.message-user>header strong{color:#e8dfca8f}.message-user .message-ordinal{color:var(--phosphor)}.message>footer{color:#1115126e;font:var(--type-utility)/1.2 var(--mono);border-top:1px dashed #11151230;gap:12px;margin-top:9px;padding-top:7px;display:flex}.stream-cursor{vertical-align:-2px;background:#507324;width:7px;height:14px;margin-left:4px;animation:.86s step-end infinite cursor-blink;display:inline-block}.tool-call{background:#1115120b;border:1px solid #1115122e;margin-top:13px}.tool-call summary{cursor:pointer;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;padding:8px 10px;list-style:none;display:grid}.tool-call summary::-webkit-details-marker{display:none}.tool-call code{font:var(--type-caption)/1.4 var(--mono)}.tool-call summary>span:last-child{color:#1115127a;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.tool-state,.event-pip{background:var(--moss);border-radius:50%;width:6px;height:6px}.tool-state-complete,.event-complete{background:#55772c}.tool-state-running,.event-running{background:var(--phosphor);box-shadow:0 0 0 3px #5a782b26}.tool-state-error,.event-error{background:var(--rust)}.tool-call>div{border-top:1px solid #11151221;padding:9px 10px}.tool-call>div p{color:#11151294;font-size:var(--type-caption);margin:5px 0 0}.composer{border-top:1px solid var(--line-dark);-webkit-backdrop-filter:blur(9px);backdrop-filter:blur(9px);background:#d8cdb4d1;padding:12px clamp(21px,3vw,44px) 18px;position:relative;box-shadow:0 -15px 38px #1115120f}.composer:before{content:"";background:repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 12px);opacity:.23;height:5px;position:absolute;top:-5px;left:0;right:0}.composer-activity{height:164px;color:var(--paper);background:linear-gradient(105deg, #b9ff4512, transparent 58%), var(--ink);border:1px solid #111512d1;grid-template-rows:24px 26px 40px 34px;gap:6px;margin-bottom:10px;padding:11px 12px;display:grid;overflow:hidden;box-shadow:4px 4px #1115121f}.activity-stage-line{grid-template-columns:repeat(5,minmax(0,1fr));align-items:start;gap:6px;display:grid}.activity-stage-line span{color:#e8dfca57;min-width:0;font:700 var(--type-utility)/1 var(--mono);letter-spacing:.04em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap;border-top:1px solid #e8dfca24;align-items:center;gap:5px;padding-top:5px;display:flex;overflow:hidden}.activity-stage-line i{border:1px solid;border-radius:50%;flex:0 0 6px;width:6px;height:6px}.activity-stage-line span.is-complete,.activity-stage-line span.is-current{color:var(--phosphor);border-color:#b9ff4585}.activity-stage-line span.is-complete i{background:var(--phosphor)}.activity-stage-line span.is-current i{box-shadow:0 0 0 4px var(--phosphor-dim)}.activity-status,.activity-status>div{align-items:center;display:flex}.activity-status{border-bottom:1px solid var(--line-light);min-width:0;font:var(--type-utility)/1 var(--mono);justify-content:space-between;gap:12px}.activity-status>div{gap:8px;min-width:0}.activity-status strong{color:var(--paper);letter-spacing:.04em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap;font-weight:600;overflow:hidden}.activity-status>span{color:#e8dfca80;flex:none}.activity-pulse{background:#e8dfca59;border-radius:50%;flex:0 0 7px;width:7px;height:7px}.composer-activity.is-loading .activity-pulse,.composer-activity.is-inference .activity-pulse{background:var(--phosphor);box-shadow:0 0 0 4px var(--phosphor-dim);animation:1.15s ease-in-out infinite activity-pulse}.load-live-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;align-items:end;gap:12px;display:grid}.activity-track-block,.live-metric{gap:6px;min-width:0;display:grid}.activity-track-block>div,.live-metric>div{font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:baseline;gap:8px;display:flex}.activity-track-block span,.live-metric span,.activity-memory span{color:#e8dfca75;text-transform:uppercase}.activity-track-block small,.live-metric small{color:#e8dfca80;font-size:inherit}.activity-memory{min-width:86px;font:var(--type-utility)/1 var(--mono);justify-items:end;gap:6px;display:grid}.activity-memory strong{color:var(--phosphor);font-size:var(--type-caption);font-weight:600}.activity-track{background:#e8dfca0e;border:1px solid #e8dfca2e;height:5px;overflow:hidden}.activity-track>span{background:repeating-linear-gradient(90deg, var(--phosphor) 0 7px, transparent 7px 9px);height:100%;transition:width .12s linear;display:block;box-shadow:0 0 10px #b9ff4557}.inference-live-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;display:grid}.live-metric{border:1px solid var(--line-light);opacity:.64;padding:5px 8px 6px}.live-metric.is-active{opacity:1;background:#b9ff450f;border-color:#b9ff457a}.live-metric strong{color:var(--phosphor);font-size:var(--type-caption);margin-left:auto;font-weight:600}.shard-strip{grid-template-columns:repeat(8,minmax(0,1fr));align-items:end;gap:5px;min-width:0;display:grid}.shard-strip.is-empty{border-top:1px dashed var(--line-light);align-items:center;display:flex}.shard-placeholder{color:#e8dfca52;font:var(--type-utility)/1.2 var(--mono)}.shard-cell{opacity:.55;gap:5px;min-width:0;display:grid}.shard-cell.state-downloading,.shard-cell.state-verifying,.shard-cell.state-complete,.shard-cell.state-cached{opacity:1}.shard-cell>span{color:#e8dfca7a;font:var(--type-utility)/1 var(--mono);justify-content:space-between;gap:3px;display:flex}.shard-cell b{color:var(--paper);font-weight:600}.shard-cell small{font-size:inherit;text-overflow:ellipsis;overflow:hidden}.composer-meta{color:#11151275;min-height:24px;font:var(--type-utility)/1 var(--mono);justify-content:space-between;align-items:center;gap:12px;margin-bottom:7px;display:flex}.tool-toggle{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:0;align-items:center;gap:5px;padding:0 2px;display:flex}.tool-toggle.is-on{color:#425f1f}.regenerate-button{color:#1115128a;min-height:32px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;border:0;margin-right:auto;padding:0 2px}.regenerate-button:disabled{cursor:default;opacity:.45}.tool-toggle svg{fill:none;stroke:currentColor;stroke-width:1.3px;width:12px}.composer-field{border:1px solid var(--ink);background:#e8dfcad4;grid-template-columns:1fr auto;min-height:84px;display:grid;position:relative;box-shadow:4px 4px #1115121a}.composer-field label{color:#11151273;font:var(--type-utility)/1 var(--mono);letter-spacing:.09em;text-transform:uppercase;position:absolute;top:7px;left:11px}.composer-field textarea{resize:none;min-width:0;color:var(--ink);font:var(--type-body)/1.45 var(--body);background:0 0;border:0;outline:none;padding:23px 13px 9px}.composer-field textarea::placeholder{color:#11151257}.send-button{min-width:112px;min-height:52px;color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);font:700 var(--type-caption)/1 var(--mono);text-transform:uppercase;cursor:pointer;justify-content:center;align-items:center;gap:7px;margin:7px;transition:transform .15s,box-shadow .15s,background .15s;display:flex}.send-button:hover:not(:disabled){box-shadow:3px 3px 0 var(--ink);transform:translate(-2px,-2px)}.send-button:disabled{opacity:.34}.send-button svg{fill:none;stroke:currentColor;stroke-width:1.4px;width:15px}.send-button.stop{color:var(--paper);background:var(--ink)}.send-button.stop span{background:var(--rust);width:7px;height:7px}.send-button.load{background:var(--phosphor)}.inspector{border-left:1px solid var(--line-light);scrollbar-color:#e8dfca2e transparent;grid-area:inspector;height:calc(100dvh - 76px);min-height:calc(100dvh - 76px);animation:.57s cubic-bezier(.16,1,.3,1) .22s both rise-in;overflow-y:auto}.inspector-section{border-bottom:1px solid var(--line-light);padding:20px 18px}.section-heading{align-items:center;margin-bottom:17px}.section-heading.compact{margin-bottom:14px}.section-heading>div{flex:1}.section-heading h2{font-size:24px}.sampling-label{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;letter-spacing:.06em;align-items:center;gap:6px;display:flex}.sampling-label i{width:4px;height:4px}.speed-gauge{grid-template-columns:116px 1fr;align-items:center;gap:15px;display:grid}.gauge-ring{background:radial-gradient(circle, var(--ink) 0 51%, transparent 52%), conic-gradient(var(--phosphor) 0 var(--gauge), #e8dfca1c var(--gauge) 100%);border-radius:49% 51% 47% 53%;place-items:center;width:116px;height:116px;display:grid;transform:rotate(-9deg);box-shadow:inset 0 0 0 1px #e8dfca1a,0 0 30px #b9ff450e}.gauge-ring:after{content:"";border:1px dashed #e8dfca21;border-radius:45% 55% 52% 48%;width:128px;height:128px;position:absolute}.gauge-ring>div{z-index:1;justify-items:center;display:grid;position:relative;transform:rotate(9deg)}.gauge-ring strong{color:var(--phosphor);font:400 29px/1 var(--display)}.gauge-ring small{color:var(--paper-ghost);font:var(--type-utility)/1 var(--mono);text-transform:uppercase;margin-top:3px}.speed-gauge dl{margin:0}.speed-gauge dl div{border-bottom:1px solid var(--line-light);font:var(--type-utility)/1 var(--mono);justify-content:space-between;padding:7px 0;display:flex}.speed-gauge dt{color:#e8dfca6e}.speed-gauge dd{color:var(--paper);margin:0}.device-card{background:linear-gradient(90deg,#b9ff4514,#0000);border:1px solid #b9ff453b;grid-template-columns:auto 1fr auto;align-items:center;gap:10px;margin:19px 0 14px;padding:10px;display:grid}.device-icon{border:1px solid #b9ff4540;align-items:flex-end;gap:3px;width:28px;height:28px;padding:5px;display:flex}.device-icon i{background:var(--phosphor);width:4px}.device-icon i:first-child{opacity:.45;height:7px}.device-icon i:nth-child(2){height:15px}.device-icon i:nth-child(3){opacity:.7;height:11px}.device-card>div{gap:3px;display:grid}.device-card small,.memory-grid small,.meter small{color:#e8dfca6b;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.device-card strong{font:500 var(--type-caption)/1 var(--mono)}.device-card>span:last-child{color:var(--phosphor);font:600 15px/1 var(--mono);justify-items:end;display:grid}.device-card>span:last-child small{color:#e8dfca5e;font-size:var(--type-utility);margin-top:3px}.meter{margin-top:13px}.meter>div:first-child{font:var(--type-utility)/1 var(--mono);justify-content:space-between;margin-bottom:6px;display:flex}.meter-track{border:1px solid #e8dfca36;height:5px;padding:1px}.meter-track>span{background:repeating-linear-gradient(90deg, var(--phosphor) 0 5px, transparent 5px 7px);height:100%;display:block}.memory-grid{background:var(--line-light);grid-template-columns:1fr 1fr;gap:1px;margin-top:14px;display:grid}.memory-grid>div{background:var(--ink);gap:6px;padding:10px;display:grid}.memory-grid strong{font:500 var(--type-caption)/1 var(--mono)}.event-log{gap:0;margin:0;padding:0;list-style:none;display:grid;position:relative}.event-log:before{content:"";background:var(--line-light);width:1px;position:absolute;top:5px;bottom:5px;left:3px}.event-log li{grid-template-columns:8px 64px 1fr;align-items:start;gap:8px;padding:7px 0;display:grid}.event-log li.event-empty{color:#e8dfca5c;font:var(--type-utility)/1.4 var(--mono);padding:5px 0;display:block}.event-pip{z-index:1;border:1px solid var(--ink);margin-top:2px}.event-log time{color:#e8dfca57;font:var(--type-utility)/1.2 var(--mono)}.event-log div{gap:3px;display:grid}.event-log strong{font:500 var(--type-caption)/1 var(--mono)}.event-log small{color:#e8dfca6b;font-size:var(--type-utility)}.artifact-close{border:1px solid var(--line-light);cursor:pointer;background:0 0;border-radius:50%;width:25px;height:25px}.artifact-empty{color:#e8dfca66;font:var(--type-utility)/1.5 var(--mono);margin:0}.artifact-pane{border:1px solid var(--line-light);background:#f8f5eb}.artifact-tabs{color:var(--paper);border-bottom:1px solid var(--line-light);background:var(--ink-raised);grid-template-columns:auto auto 1fr;align-items:center;display:grid}.artifact-tabs button{border:0;border-right:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:7px 9px}.artifact-tabs button[aria-selected=true]{color:var(--ink);background:var(--phosphor)}.artifact-tabs span{color:#e8dfca73;font:var(--type-utility)/1 var(--mono);text-align:right;text-overflow:ellipsis;white-space:nowrap;padding:0 8px;overflow:hidden}.artifact-pane iframe{background:#fff;border:0;width:100%;height:260px;display:block}.artifact-running{position:relative}.artifact-running>button{z-index:2;color:var(--paper);border:1px solid var(--rust);min-height:44px;font:var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;background:#111512e6;padding:5px 7px;position:absolute;top:7px;right:7px}.artifact-launch{min-height:180px;color:var(--ink);text-align:center;align-content:center;justify-items:center;gap:13px;padding:20px;display:grid}.artifact-launch p{color:#1115129e;max-width:250px;font:var(--type-caption)/1.5 var(--mono);margin:0}.artifact-launch button{color:var(--ink);border:1px solid var(--ink);background:var(--phosphor);min-height:44px;font:700 var(--type-utility)/1 var(--mono);text-transform:uppercase;cursor:pointer;padding:8px 10px}.artifact-code{height:260px;color:var(--paper);font:var(--type-utility)/1.5 var(--mono);white-space:pre-wrap;background:#0d100e;margin:0;padding:10px;overflow:auto}.settings-layer{z-index:50;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#080b09a3;justify-content:flex-end;animation:.16s both station-in;display:flex;position:fixed;inset:0}.settings-sheet{width:min(430px,100%);min-height:100%;color:var(--paper);border-left:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at 104% 101%, transparent 0 13px, #b9ff450e 13.5px 14.5px), var(--ink);padding:30px 27px;animation:.3s cubic-bezier(.16,1,.3,1) both sheet-in;overflow-y:auto;box-shadow:-24px 0 70px #00000052}.settings-sheet>header{border-bottom:1px solid var(--line-light);justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:37px;padding-bottom:20px;display:flex}.settings-sheet h2{font:400 31px/1 var(--display);margin:0}.sheet-close{border:1px solid var(--line-light);width:35px;height:35px;font:300 22px/1 var(--body);cursor:pointer;background:0 0;border-radius:50%}.sheet-close:hover{color:var(--phosphor);border-color:var(--phosphor)}.setting-group{border-bottom:1px solid var(--line-light);grid-template-columns:24px 1fr auto;align-items:center;gap:12px;padding:15px 0;display:grid}.setting-number{color:var(--phosphor);font:var(--type-utility)/1 var(--mono)}.setting-group>div,.range-setting>span:first-child{gap:5px;display:grid}.setting-group small,.range-setting small{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.setting-group strong,.range-setting strong{font:500 var(--type-caption)/1 var(--mono)}.setting-locked{color:#e8dfca5c;font:var(--type-utility)/1 var(--mono);text-transform:uppercase}.setting-select select{max-width:112px;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);min-height:44px;font:var(--type-utility)/1 var(--mono);border-radius:0;padding:6px 7px}.range-setting{gap:13px;margin-top:29px;display:grid}.range-setting>span:first-child{justify-content:space-between;align-items:end;display:flex}.range-setting input{appearance:none;background:linear-gradient(90deg, var(--phosphor) 0 35%, #e8dfca1c 35%);border:1px solid #e8dfca3d;border-radius:0;width:100%;height:4px;margin:0}.range-setting:first-of-type input{background:var(--phosphor)}.range-setting input::-webkit-slider-thumb{appearance:none;border:2px solid var(--ink);background:var(--phosphor);width:12px;height:18px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab}.range-setting input::-moz-range-thumb{border:2px solid var(--ink);background:var(--phosphor);width:10px;height:16px;box-shadow:0 0 0 1px var(--phosphor);cursor:grab;border-radius:0}.range-ticks{pointer-events:none;justify-content:space-between;margin-top:-13px;display:flex}.range-limits{color:#e8dfca59;font:var(--type-utility)/1 var(--mono);justify-content:space-between;display:flex}.range-limits i{font-style:normal}.system-prompt-setting{gap:9px;margin-top:29px;display:grid}.system-prompt-setting>span{color:#e8dfca6e;font:var(--type-utility)/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;display:flex}.system-prompt-setting textarea{resize:vertical;color:var(--paper);border:1px solid var(--line-light);background:var(--ink-raised);font:var(--type-caption)/1.5 var(--mono);padding:10px}.settings-actions{grid-template-columns:1fr 1fr;gap:7px;margin-top:29px;display:grid}.settings-actions button{color:var(--paper);border:1px solid var(--line-light);min-height:44px;font:var(--type-utility)/1.2 var(--mono);text-transform:uppercase;cursor:pointer;background:0 0;padding:9px}.settings-actions button:hover{color:var(--phosphor);border-color:var(--phosphor)}.settings-actions .danger-action{color:#e78a62;border-color:#c96f4373;grid-column:span 2}.boot-screen{min-height:100dvh;color:var(--paper);background:var(--ink);justify-content:center;align-items:center;gap:18px;padding:24px;display:flex}.boot-screen h1{color:var(--phosphor);font:400 34px/1 var(--display);margin:0}.boot-screen p:last-child{color:var(--paper-ghost);font:var(--type-caption)/1.5 var(--mono)}.range-ticks i{background:#e8dfca40;width:1px;height:5px}.settings-note{background:#b9ff450f;border:1px solid #b9ff4540;align-items:flex-start;gap:13px;margin-top:42px;padding:15px;display:flex}.settings-note .signal-dot{flex:0 0 7px;margin-top:6px}.settings-note p{color:#e8dfca94;font-size:var(--type-caption);margin:0;line-height:1.5}.settings-note strong{color:var(--phosphor)}@keyframes station-in{0%{opacity:0}to{opacity:1}}@keyframes rise-in{0%{opacity:0;transform:translateY(11px)}to{opacity:1;transform:translateY(0)}}@keyframes note-in{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes cursor-blink{0%,52%{opacity:1}53%,to{opacity:0}}@keyframes activity-pulse{0%,to{opacity:.55;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}@keyframes sheet-in{0%{opacity:0;transform:translate(34px)}to{opacity:1;transform:translate(0)}}@media (width<=1279px){.station-header{grid-template-columns:minmax(320px,1fr) auto auto}.local-promise{min-width:auto}.local-promise small{display:none}.workspace-grid{grid-template-columns:280px minmax(500px,1fr);grid-template-areas:"models chat""inspector inspector"}.model-rail,.chat-surface{height:calc(100dvh - 76px);min-height:calc(100dvh - 76px)}.inspector{border-top:1px solid var(--phosphor);border-left:0;grid-template-columns:repeat(2,minmax(0,1fr));height:auto;min-height:auto;display:grid;overflow:visible}.inspector-section{border-right:1px solid var(--line-light);border-bottom:0}}@media (width<=820px){.station-header{min-height:70px;display:block}.brand-block{padding:11px 70px 11px 15px}.brand-mark{flex-basis:36px;width:36px;height:36px}.local-promise{border:0;border-top:1px solid var(--line-light);min-height:31px;padding:0 16px}.header-actions{position:absolute;top:10px;right:0}.station-clock{display:none}.workspace-grid{display:block}.model-rail,.chat-surface,.inspector{height:auto;min-height:auto}.model-rail{border-right:0;border-bottom:1px solid var(--phosphor);padding:18px 15px;display:block}.model-list{scroll-snap-type:x mandatory;gap:7px;padding-bottom:7px;display:flex;overflow-x:auto}.model-card{scroll-snap-align:start;min-width:205px}.rail-footnote{display:none}.chat-surface{grid-template-rows:auto 19px minmax(240px,auto) auto;display:grid;overflow:visible}.transcript{padding-inline:22px;overflow:visible}.chat-header{padding:17px 19px}.chat-header-meta span:not(.live-label){display:none}.live-label{grid-column:1}.composer{z-index:5;padding-inline:14px;position:relative}.transcript-empty{margin-top:4vh}.inspector{border-top:1px solid var(--phosphor);display:block}.inspector-section{border-right:0;border-bottom:1px solid var(--line-light)}}@media (width<=520px){.brand-block h1{font-size:24px}.brand-block .eyebrow,.local-promise strong{font-size:var(--type-utility)}.chat-header{align-items:flex-end}.botanical-subtitle{margin:4px 0 0;display:block}.message-user{max-width:91%}.composer-meta>span{display:none}.composer-activity{grid-template-rows:24px 30px 44px 36px;height:176px;padding-inline:9px}.activity-stage-line{gap:3px}.activity-stage-line span{letter-spacing:0;gap:3px}.activity-stage-line i{display:none}.load-live-grid{gap:7px}.activity-memory{min-width:68px}.shard-strip{gap:3px}.shard-cell small{display:none}.composer-field{grid-template-columns:1fr;padding-bottom:62px}.send-button{height:52px;position:absolute;bottom:0;right:0}.speed-gauge{grid-template-columns:106px 1fr}.gauge-ring{width:102px;height:102px}.gauge-ring:after{width:113px;height:113px}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.bench-page{min-height:100dvh;color:var(--ink);background:var(--paper)}.bench-masthead{min-height:86px;color:var(--paper);border-bottom:1px solid var(--phosphor);background:var(--ink);grid-template-columns:minmax(310px,1fr) minmax(360px,1.2fr) auto;align-items:stretch;display:grid}.bench-masthead .brand-block h1{color:var(--paper)}.bench-masthead-note{border-inline:1px solid var(--line-light);background:linear-gradient(110deg,#b9ff4517,#0000 72%);flex-direction:column;justify-content:center;gap:5px;padding:14px 28px;display:flex}.bench-masthead-note strong{color:var(--phosphor);font:700 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase}.bench-masthead-note span{color:#e8dfca8c;max-width:520px;font:10px/1.35 var(--body)}.bench-return{color:var(--paper);font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;align-items:center;gap:8px;padding:0 25px;text-decoration:none;display:flex}.bench-return:hover{color:var(--phosphor)}.bench-layout{grid-template-columns:minmax(275px,330px) minmax(0,1fr);min-height:calc(100dvh - 86px);display:grid}.bench-controls{color:var(--paper);border-right:1px solid var(--phosphor);background:repeating-radial-gradient(ellipse at -12% 102%, transparent 0 24px, #b9ff450b 25px 26px), var(--ink);padding:28px 22px}.bench-controls h2{font:400 30px/1 var(--display);letter-spacing:-.03em;margin:0}.bench-control-intro{color:#e8dfca80;margin:9px 0 22px;font-size:11px;line-height:1.45}.bench-controls form{gap:15px;display:grid}.bench-field{gap:6px;min-width:0;display:grid}.bench-field>span{color:#e8dfcac7;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-field select,.bench-field input{width:100%;min-width:0;height:39px;color:var(--paper);background:var(--ink-raised);font:10px/1 var(--mono);border:1px solid #e8dfca38;border-radius:0;outline:0;padding:0 10px}.bench-field input::placeholder{color:#e8dfca40}.bench-field select:disabled,.bench-field input:disabled{opacity:.5}.bench-field small{color:#e8dfca59;font:8px/1.2 var(--mono)}.bench-field-pair{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.bench-experimental{background:#c96f430d;border:1px dashed #c96f437a;padding:11px}.bench-experimental summary{color:#e19a76;font:700 8px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase;cursor:pointer}.bench-experimental>p{color:#e8dfca75;margin:10px 0;font-size:9px;line-height:1.4}.bench-locks{color:#e8dfca70;font:7px/1.35 var(--mono);gap:5px;margin:10px 0 0;display:grid}.bench-locks div{justify-content:space-between;gap:8px;display:flex}.bench-locks dt,.bench-locks dd{margin:0}.bench-locks dt{color:#e8dfcab8}.bench-gate{border-left:2px solid var(--rust);background:#c96f4314;gap:5px;padding:10px;display:grid}.bench-gate.pass{border-color:var(--phosphor);background:var(--phosphor-dim)}.bench-gate strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase}.bench-gate.pass strong{color:var(--phosphor)}.bench-gate span{color:#e8dfca80;font:8px/1.4 var(--mono);overflow-wrap:anywhere}.bench-run-actions{grid-template-columns:1fr auto;gap:8px;display:grid}.bench-run-button,.bench-stop-button,.bench-section-heading button{border:1px solid var(--ink);background:var(--phosphor);min-height:38px;font:800 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;border-radius:0;padding:0 13px}.bench-run-button:disabled{opacity:.35}.bench-stop-button{color:#ffb08a;border-color:var(--rust);background:0 0}.bench-workbench{background:linear-gradient(90deg, #1115120b 1px, transparent 1px) 0 0 / 22px 22px, linear-gradient(#1115120a 1px, transparent 1px) 0 0 / 22px 22px, var(--paper);min-width:0;padding:clamp(26px,4vw,58px)}.bench-hero{border-bottom:1px solid var(--line-dark);grid-template-columns:minmax(320px,1.3fr) minmax(260px,.7fr);align-items:end;gap:34px;padding-bottom:29px;display:grid}.bench-hero h2{max-width:690px;font:400 clamp(42px, 6vw, 82px)/.84 var(--display);letter-spacing:-.065em;margin:0}.bench-hero h2 em{color:#4f7125;font-weight:400}.bench-hero>p{color:#11151294;max-width:430px;margin:0;font-size:11px;line-height:1.55}.bench-progress{gap:8px;padding:16px 0;display:grid}.bench-progress>div{font:8px/1 var(--mono);letter-spacing:.05em;text-transform:uppercase;justify-content:space-between;gap:20px;display:flex}.bench-progress>div span{color:#11151273}.bench-progress-track{background:#11151221;height:3px}.bench-progress-track i{background:#527426;height:100%;transition:width .18s;display:block}.bench-error{color:var(--paper);border-left:3px solid var(--rust);background:var(--ink);gap:5px;margin-bottom:18px;padding:12px 14px;display:grid}.bench-error strong{color:#ff9a68;font:700 8px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase}.bench-error span{font:9px/1.4 var(--mono);overflow-wrap:anywhere}.bench-core-section{margin-top:13px}.bench-section-heading{justify-content:space-between;align-items:end;gap:20px;margin-bottom:13px;display:flex}.bench-section-heading h3,.bench-output h3{font:500 24px/1 var(--display);margin:0}.bench-section-heading button{min-height:31px;color:var(--paper);border-color:var(--ink);background:var(--ink)}.bench-core-sample{border:1px solid var(--ink);background:var(--paper-deep);grid-template-columns:repeat(4,minmax(0,1fr));display:grid;position:relative;overflow:hidden}.bench-core-sample:before{content:"";pointer-events:none;opacity:.35;background:repeating-radial-gradient(#0000 0 34px,#1115122b 35px 36px);position:absolute;inset:-130% -5%}.bench-core-sample article{background:#e8dfca91;border-right:1px solid #11151242;flex-direction:column;justify-content:space-between;min-height:128px;padding:17px;display:flex;position:relative}.bench-core-sample article:nth-child(2n){background:#d8cdb499}.bench-core-sample article:last-child{color:var(--paper);background:#111512e8;border-right:0}.bench-core-sample article>span{font:800 8px/1 var(--mono);letter-spacing:.09em;text-transform:uppercase}.bench-core-sample article strong{font:400 clamp(28px, 3vw, 43px)/1 var(--display);letter-spacing:-.04em;margin-block:auto}.bench-core-sample article small{color:#11151280;font:7px/1.2 var(--mono);text-transform:uppercase}.bench-core-sample article:last-child small{color:#e8dfca7a}.bench-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px;display:grid}.bench-data-card{border:1px solid var(--line-dark);background:#e8dfcaad;padding:16px}.bench-runtime-card,.bench-tuning-card{grid-column:span 2}.bench-data-card dl{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 26px;margin:0;display:grid}.bench-runtime-card dl{grid-template-columns:minmax(180px,1.5fr) repeat(4,minmax(100px,.7fr))}.bench-tuning-card dl{grid-template-columns:repeat(4,minmax(0,1fr))}.bench-data-card dl div{border-top:1px solid #11151221;gap:4px;min-width:0;padding-top:8px;display:grid}.bench-data-card dt{color:#11151273;font:7px/1 var(--mono);letter-spacing:.07em;text-transform:uppercase}.bench-data-card dd{min-width:0;font:500 16px/1.2 var(--body);text-overflow:ellipsis;white-space:nowrap;margin:0;overflow:hidden}.bench-output{border:1px dashed #1115124d;grid-template-columns:repeat(2,minmax(0,1fr));gap:34px;margin-top:12px;padding:21px;display:grid}.bench-output>div:last-child{border-left:1px solid var(--line-dark);padding-left:34px}.bench-output p:not(.eyebrow){color:#11151299;white-space:pre-wrap;margin:8px 0 0;font-size:11px;line-height:1.5}@media (width<=1050px){.bench-masthead{grid-template-columns:minmax(310px,1fr) auto}.bench-masthead-note{display:none}.bench-layout{grid-template-columns:285px minmax(0,1fr)}.bench-core-sample{grid-template-columns:repeat(2,minmax(0,1fr))}.bench-core-sample article:nth-child(2){border-right:0}.bench-core-sample article:nth-child(-n+2){border-bottom:1px solid #11151242}.bench-runtime-card dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=760px){.bench-masthead{align-items:center;display:flex}.bench-masthead .brand-block{flex:1;min-width:0}.bench-return{padding:0 15px;font-size:0}.bench-return span{font-size:17px}.bench-layout{display:block}.bench-controls{border-right:0;border-bottom:1px solid var(--phosphor)}.bench-workbench{padding:28px 16px}.bench-hero,.bench-output{grid-template-columns:1fr;gap:18px}.bench-hero h2{font-size:clamp(42px,14vw,68px)}.bench-detail-grid{display:block}.bench-data-card+.bench-data-card{margin-top:10px}.bench-output>div:last-child{border-top:1px solid var(--line-dark);border-left:0;padding-top:18px;padding-left:0}}@media (width<=480px){.bench-masthead .brand-mark{display:none}.bench-core-sample,.bench-data-card dl,.bench-runtime-card dl,.bench-tuning-card dl{grid-template-columns:1fr}.bench-core-sample article{border-bottom:1px solid #11151242;border-right:0;min-height:105px}.bench-core-sample article:last-child{border-bottom:0}}
 
 
dist/assets/index-DsVgIwSl.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/index.html CHANGED
@@ -10,8 +10,8 @@
10
  content="Bonsai local inference field station — private WebGPU language models in your browser."
11
  />
12
  <title>Bonsai Field Station</title>
13
- <script type="module" crossorigin src="/assets/index-BY7HNxCd.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-DnMbdiUB.css">
15
  </head>
16
  <body>
17
  <div id="root"></div>
 
10
  content="Bonsai local inference field station — private WebGPU language models in your browser."
11
  />
12
  <title>Bonsai Field Station</title>
13
+ <script type="module" crossorigin src="/assets/index-DsVgIwSl.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BbReSmeP.css">
15
  </head>
16
  <body>
17
  <div id="root"></div>
src/app/App.tsx CHANGED
@@ -18,6 +18,7 @@ import {
18
  import { AGENT_TOOLS, executeAgentTool, type ArtifactDocument } from '../lib/agent-tools';
19
  import type {
20
  ChatMessage,
 
21
  ComposerSubmission,
22
  ModelLoadProgress,
23
  ModelId,
@@ -31,13 +32,13 @@ import type {
31
  ToolRun,
32
  } from '../lib/contracts';
33
  import { formatBytes } from '../lib/format';
34
- import { listConversations, saveConversation } from '../lib/idb';
 
35
  import { parseThoughtStream } from '../lib/think-parser';
36
  import { parseToolCalls } from '../lib/tool-protocol';
37
  import { BonsaiShell } from './components/BonsaiShell';
38
 
39
  const MANIFEST_PATH = 'manifest/models.json';
40
- const CONVERSATION_ID = 'primary-field-log';
41
  const MAX_TOOL_ROUNDS = 5;
42
  const MAX_TOOL_CALLS_PER_ROUND = 4;
43
  const MAX_TOOL_CALLS_TOTAL = 12;
@@ -150,6 +151,10 @@ function eventClock(): string {
150
  return new Date().toISOString().slice(11, 23);
151
  }
152
 
 
 
 
 
153
  function errorMessage(error: unknown): string {
154
  if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
155
  return error instanceof Error ? error.message : String(error);
@@ -275,6 +280,9 @@ export function App() {
275
  const [loadedModelId, setLoadedModelId] = useState<ModelId | null>(null);
276
  const [loadingModelId, setLoadingModelId] = useState<ModelId | null>(null);
277
  const [messages, setMessages] = useState<ChatMessage[]>([]);
 
 
 
278
  const [streamState, setStreamState] = useState<StreamState>('idle');
279
  const [modelLoadProgress, setModelLoadProgress] = useState<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
280
  const [toolsEnabled, setToolsEnabled] = useState(false);
@@ -415,9 +423,23 @@ export function App() {
415
  if (!cancelled) setError(`Capability inspection failed: ${errorMessage(capabilityError)}`);
416
  }
417
  try {
418
- const conversations = await listConversations<ChatMessage>();
419
- const latest = conversations.find((conversation) => conversation.id === CONVERSATION_ID);
420
- if (!cancelled && latest) setMessages(latest.messages);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  } catch {
422
  // Private browsing may disable IndexedDB. Chat remains usable for the tab.
423
  if (!cancelled) setBootStatus((current) => `${current} · history is session-only`);
@@ -432,19 +454,24 @@ export function App() {
432
 
433
  useEffect(() => {
434
  if (!historyReadyRef.current) return;
 
 
 
 
 
 
 
 
 
 
435
  const timer = window.setTimeout(() => {
436
- void saveConversation({
437
- id: CONVERSATION_ID,
438
- title: messages.find((message) => message.role === 'user')?.content.slice(0, 80) || 'Bonsai field log',
439
- messages,
440
- modelId: activeModelId,
441
- updatedAt: Date.now(),
442
- }).catch(() => setBootStatus((current) => current.includes('history is session-only')
443
  ? current
444
  : `${current} · history is session-only`));
445
  }, 250);
446
  return () => window.clearTimeout(timer);
447
- }, [activeModelId, messages]);
448
 
449
  const models = useMemo<ModelOption[]>(() => {
450
  if (!manifest) return [];
@@ -1082,10 +1109,84 @@ export function App() {
1082
 
1083
  const handleClearConversation = () => {
1084
  if (operationRef.current) return;
 
 
 
1085
  setMessages([]);
1086
  setToolEvents([]);
1087
  setArtifact(null);
1088
  setBootStatus('Conversation cleared · model remains local');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  };
1090
 
1091
  const effectiveLoadedModelId = (() => {
@@ -1117,6 +1218,9 @@ export function App() {
1117
  activeModelId={activeModelId}
1118
  loadedModelId={effectiveLoadedModelId}
1119
  messages={messages}
 
 
 
1120
  streamState={streamState}
1121
  modelLoadProgress={modelLoadProgress}
1122
  telemetry={telemetry}
@@ -1141,6 +1245,11 @@ export function App() {
1141
  onPersistStorage={handlePersistStorage}
1142
  onClearStorage={handleClearStorage}
1143
  onClearConversation={handleClearConversation}
 
 
 
 
 
1144
  onDismissError={() => setError(null)}
1145
  onRetryShard={handleLoadModel}
1146
  onCloseArtifact={() => setArtifact(null)}
 
18
  import { AGENT_TOOLS, executeAgentTool, type ArtifactDocument } from '../lib/agent-tools';
19
  import type {
20
  ChatMessage,
21
+ ChatSession,
22
  ComposerSubmission,
23
  ModelLoadProgress,
24
  ModelId,
 
32
  ToolRun,
33
  } from '../lib/contracts';
34
  import { formatBytes } from '../lib/format';
35
+ import { conversationTitle, normalizeConversationModelId, upsertConversation } from '../lib/conversations';
36
+ import { deleteConversation, listConversations, saveConversation } from '../lib/idb';
37
  import { parseThoughtStream } from '../lib/think-parser';
38
  import { parseToolCalls } from '../lib/tool-protocol';
39
  import { BonsaiShell } from './components/BonsaiShell';
40
 
41
  const MANIFEST_PATH = 'manifest/models.json';
 
42
  const MAX_TOOL_ROUNDS = 5;
43
  const MAX_TOOL_CALLS_PER_ROUND = 4;
44
  const MAX_TOOL_CALLS_TOTAL = 12;
 
151
  return new Date().toISOString().slice(11, 23);
152
  }
153
 
154
+ function createConversationId(): string {
155
+ return crypto.randomUUID();
156
+ }
157
+
158
  function errorMessage(error: unknown): string {
159
  if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
160
  return error instanceof Error ? error.message : String(error);
 
280
  const [loadedModelId, setLoadedModelId] = useState<ModelId | null>(null);
281
  const [loadingModelId, setLoadingModelId] = useState<ModelId | null>(null);
282
  const [messages, setMessages] = useState<ChatMessage[]>([]);
283
+ const [conversations, setConversations] = useState<ChatSession[]>([]);
284
+ const [activeConversationId, setActiveConversationId] = useState(createConversationId);
285
+ const [conversationListOpen, setConversationListOpen] = useState(false);
286
  const [streamState, setStreamState] = useState<StreamState>('idle');
287
  const [modelLoadProgress, setModelLoadProgress] = useState<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
288
  const [toolsEnabled, setToolsEnabled] = useState(false);
 
423
  if (!cancelled) setError(`Capability inspection failed: ${errorMessage(capabilityError)}`);
424
  }
425
  try {
426
+ const storedConversations = (await listConversations<ChatMessage>()).map((conversation) => ({
427
+ ...conversation,
428
+ modelId: normalizeConversationModelId(conversation.modelId),
429
+ systemPrompt: typeof conversation.systemPrompt === 'string'
430
+ ? conversation.systemPrompt
431
+ : INITIAL_SETTINGS.systemPrompt,
432
+ }));
433
+ const latest = storedConversations[0];
434
+ if (!cancelled) {
435
+ setConversations(storedConversations);
436
+ if (latest) {
437
+ setActiveConversationId(latest.id);
438
+ setActiveModelId(latest.modelId);
439
+ setMessages(latest.messages);
440
+ setSettings((current) => ({ ...current, systemPrompt: latest.systemPrompt }));
441
+ }
442
+ }
443
  } catch {
444
  // Private browsing may disable IndexedDB. Chat remains usable for the tab.
445
  if (!cancelled) setBootStatus((current) => `${current} · history is session-only`);
 
454
 
455
  useEffect(() => {
456
  if (!historyReadyRef.current) return;
457
+ const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
458
+ if (messages.length === 0 && settings.systemPrompt === INITIAL_SETTINGS.systemPrompt && !alreadySaved) return;
459
+ const nextConversation: ChatSession = {
460
+ id: activeConversationId,
461
+ title: conversationTitle(messages),
462
+ messages,
463
+ modelId: activeModelId,
464
+ systemPrompt: settings.systemPrompt,
465
+ updatedAt: Date.now(),
466
+ };
467
  const timer = window.setTimeout(() => {
468
+ setConversations((current) => upsertConversation(current, nextConversation));
469
+ void saveConversation(nextConversation).catch(() => setBootStatus((current) => current.includes('history is session-only')
 
 
 
 
 
470
  ? current
471
  : `${current} · history is session-only`));
472
  }, 250);
473
  return () => window.clearTimeout(timer);
474
+ }, [activeConversationId, activeModelId, messages, settings.systemPrompt]);
475
 
476
  const models = useMemo<ModelOption[]>(() => {
477
  if (!manifest) return [];
 
1109
 
1110
  const handleClearConversation = () => {
1111
  if (operationRef.current) return;
1112
+ const clearedId = activeConversationId;
1113
+ setConversations((current) => current.filter((conversation) => conversation.id !== clearedId));
1114
+ setActiveConversationId(createConversationId());
1115
  setMessages([]);
1116
  setToolEvents([]);
1117
  setArtifact(null);
1118
  setBootStatus('Conversation cleared · model remains local');
1119
+ void deleteConversation(clearedId).catch(() => setBootStatus('Conversation cleared for this tab · local history is unavailable'));
1120
+ };
1121
+
1122
+ const handleOpenConversationList = () => {
1123
+ if (operationRef.current) return;
1124
+ const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
1125
+ if (messages.length > 0 || settings.systemPrompt !== INITIAL_SETTINGS.systemPrompt || alreadySaved) {
1126
+ const nextConversation: ChatSession = {
1127
+ id: activeConversationId,
1128
+ title: conversationTitle(messages),
1129
+ messages,
1130
+ modelId: activeModelId,
1131
+ systemPrompt: settings.systemPrompt,
1132
+ updatedAt: Date.now(),
1133
+ };
1134
+ setConversations((current) => upsertConversation(current, nextConversation));
1135
+ void saveConversation(nextConversation).catch(() => undefined);
1136
+ }
1137
+ setConversationListOpen(true);
1138
+ };
1139
+
1140
+ const handleNewConversation = () => {
1141
+ if (operationRef.current) return;
1142
+ setActiveConversationId(createConversationId());
1143
+ setMessages([]);
1144
+ setToolEvents([]);
1145
+ setArtifact(null);
1146
+ setError(null);
1147
+ setStreamState('idle');
1148
+ setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
1149
+ setConversationListOpen(false);
1150
+ setBootStatus('New local chat · model remains available');
1151
+ };
1152
+
1153
+ const handleOpenConversation = (conversationId: string) => {
1154
+ if (operationRef.current) return;
1155
+ const conversation = conversations.find((candidate) => candidate.id === conversationId);
1156
+ if (!conversation) return;
1157
+ setActiveConversationId(conversation.id);
1158
+ setMessages(conversation.messages);
1159
+ setToolEvents([]);
1160
+ setArtifact(null);
1161
+ setError(null);
1162
+ setStreamState('idle');
1163
+ setSettings((current) => ({ ...current, systemPrompt: conversation.systemPrompt }));
1164
+ handleModelSelect(conversation.modelId);
1165
+ setConversationListOpen(false);
1166
+ setBootStatus(`Opened local chat · ${conversation.title}`);
1167
+ };
1168
+
1169
+ const handleOpenConversationSettings = (conversationId: string) => {
1170
+ if (operationRef.current) return;
1171
+ handleOpenConversation(conversationId);
1172
+ setSettingsOpen(true);
1173
+ };
1174
+
1175
+ const handleDeleteConversation = (conversationId: string) => {
1176
+ if (operationRef.current) return;
1177
+ const conversation = conversations.find((candidate) => candidate.id === conversationId);
1178
+ if (!conversation || !window.confirm(`Delete “${conversation.title}” from this browser?`)) return;
1179
+ setConversations((current) => current.filter((candidate) => candidate.id !== conversationId));
1180
+ if (conversationId === activeConversationId) {
1181
+ setActiveConversationId(createConversationId());
1182
+ setMessages([]);
1183
+ setToolEvents([]);
1184
+ setArtifact(null);
1185
+ setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
1186
+ }
1187
+ void deleteConversation(conversationId)
1188
+ .then(() => setBootStatus('Local chat deleted'))
1189
+ .catch(() => setBootStatus('Chat removed for this tab · local history is unavailable'));
1190
  };
1191
 
1192
  const effectiveLoadedModelId = (() => {
 
1218
  activeModelId={activeModelId}
1219
  loadedModelId={effectiveLoadedModelId}
1220
  messages={messages}
1221
+ conversations={conversations}
1222
+ activeConversationId={activeConversationId}
1223
+ conversationListOpen={conversationListOpen}
1224
  streamState={streamState}
1225
  modelLoadProgress={modelLoadProgress}
1226
  telemetry={telemetry}
 
1245
  onPersistStorage={handlePersistStorage}
1246
  onClearStorage={handleClearStorage}
1247
  onClearConversation={handleClearConversation}
1248
+ onOpenConversationList={handleOpenConversationList}
1249
+ onNewConversation={handleNewConversation}
1250
+ onOpenConversation={handleOpenConversation}
1251
+ onOpenConversationSettings={handleOpenConversationSettings}
1252
+ onDeleteConversation={handleDeleteConversation}
1253
  onDismissError={() => setError(null)}
1254
  onRetryShard={handleLoadModel}
1255
  onCloseArtifact={() => setArtifact(null)}
src/app/components/ArtifactPane.tsx CHANGED
@@ -25,11 +25,7 @@ export function ArtifactPane({ artifact, onClose }: ArtifactPaneProps) {
25
  return (
26
  <section className="inspector-section artifact-section" aria-labelledby="artifact-title">
27
  <div className="section-heading compact">
28
- <span>04</span>
29
- <div>
30
- <p className="eyebrow">Isolated terrarium</p>
31
- <h2 id="artifact-title">Artifact</h2>
32
- </div>
33
  {artifact && <button className="artifact-close" type="button" onClick={onClose} aria-label="Close artifact">×</button>}
34
  </div>
35
  {!artifact ? (
 
25
  return (
26
  <section className="inspector-section artifact-section" aria-labelledby="artifact-title">
27
  <div className="section-heading compact">
28
+ <h2 id="artifact-title">Artifact</h2>
 
 
 
 
29
  {artifact && <button className="artifact-close" type="button" onClick={onClose} aria-label="Close artifact">×</button>}
30
  </div>
31
  {!artifact ? (
src/app/components/BonsaiShell.tsx CHANGED
@@ -1,35 +1,83 @@
 
1
  import type { BonsaiShellProps } from '../../lib/contracts';
 
2
  import { ChatSurface } from './ChatSurface';
3
- import { Header } from './Header';
 
4
  import { InspectorPanel } from './InspectorPanel';
5
  import { ModelRail } from './ModelRail';
6
  import { SettingsSheet } from './SettingsSheet';
7
 
8
  export function BonsaiShell(props: BonsaiShellProps) {
 
 
 
 
 
 
 
 
 
 
 
9
  const activeModel = props.models.find((model) => model.id === props.activeModelId);
10
  const busy = props.streamState === 'loading' || props.streamState === 'streaming';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  if (!activeModel) {
13
  throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`);
14
  }
15
 
16
  return (
17
- <div className="station-frame">
18
- <Header onOpenSettings={props.onOpenSettings} disabled={busy} />
19
- <div className="workspace-grid">
 
 
 
 
 
 
 
 
 
20
  <ModelRail
 
21
  models={props.models}
22
  activeModelId={props.activeModelId}
23
  busy={busy}
24
  onModelSelect={props.onModelSelect}
25
  />
26
- <ChatSurface
 
 
 
 
 
 
 
27
  activeModel={activeModel}
28
  loadedModelId={props.loadedModelId}
29
  messages={props.messages}
30
  streamState={props.streamState}
31
  modelLoadProgress={props.modelLoadProgress}
32
- telemetry={props.telemetry}
33
  toolsEnabled={props.toolsEnabled}
34
  runtimeStatus={props.runtimeStatus}
35
  error={props.error}
@@ -41,8 +89,11 @@ export function BonsaiShell(props: BonsaiShellProps) {
41
  onRegenerate={props.onRegenerate}
42
  onCancel={props.onCancel}
43
  onToolsEnabledChange={props.onToolsEnabledChange}
44
- />
 
 
45
  <InspectorPanel
 
46
  telemetry={props.telemetry}
47
  toolEvents={props.toolEvents}
48
  artifact={props.artifact}
@@ -51,6 +102,8 @@ export function BonsaiShell(props: BonsaiShellProps) {
51
  </div>
52
  <SettingsSheet
53
  open={props.settingsOpen}
 
 
54
  activeModel={activeModel}
55
  telemetry={props.telemetry}
56
  settings={props.settings}
 
1
+ import { useEffect, useState } from 'react';
2
  import type { BonsaiShellProps } from '../../lib/contracts';
3
+ import { normalizeThemeMode, resolveThemeMode, THEME_STORAGE_KEY, type ThemeMode } from '../theme';
4
  import { ChatSurface } from './ChatSurface';
5
+ import { ConversationList } from './ConversationList';
6
+ import { Header, type ChatWidth } from './Header';
7
  import { InspectorPanel } from './InspectorPanel';
8
  import { ModelRail } from './ModelRail';
9
  import { SettingsSheet } from './SettingsSheet';
10
 
11
  export function BonsaiShell(props: BonsaiShellProps) {
12
+ const [leftSidebarOpen, setLeftSidebarOpen] = useState(true);
13
+ const [rightSidebarOpen, setRightSidebarOpen] = useState(true);
14
+ const [chatWidth, setChatWidth] = useState<ChatWidth>('centered');
15
+ const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
16
+ try {
17
+ return normalizeThemeMode(globalThis.localStorage?.getItem(THEME_STORAGE_KEY));
18
+ } catch {
19
+ return 'system';
20
+ }
21
+ });
22
+ const [systemDark, setSystemDark] = useState(() => globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false);
23
  const activeModel = props.models.find((model) => model.id === props.activeModelId);
24
  const busy = props.streamState === 'loading' || props.streamState === 'streaming';
25
+ const resolvedTheme = resolveThemeMode(themeMode, systemDark);
26
+
27
+ useEffect(() => {
28
+ const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
29
+ if (!media) return;
30
+ const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
31
+ setSystemDark(media.matches);
32
+ media.addEventListener('change', handleChange);
33
+ return () => media.removeEventListener('change', handleChange);
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ try {
38
+ globalThis.localStorage?.setItem(THEME_STORAGE_KEY, themeMode);
39
+ } catch {
40
+ // Theme still works for the current tab when storage is unavailable.
41
+ }
42
+ }, [themeMode]);
43
 
44
  if (!activeModel) {
45
  throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`);
46
  }
47
 
48
  return (
49
+ <div className="station-frame" data-theme={resolvedTheme} data-theme-mode={themeMode}>
50
+ <Header
51
+ onOpenSettings={props.onOpenSettings}
52
+ disabled={busy}
53
+ leftSidebarOpen={leftSidebarOpen}
54
+ rightSidebarOpen={rightSidebarOpen}
55
+ chatWidth={chatWidth}
56
+ onToggleLeftSidebar={() => setLeftSidebarOpen((open) => !open)}
57
+ onToggleRightSidebar={() => setRightSidebarOpen((open) => !open)}
58
+ onToggleChatWidth={() => setChatWidth((width) => width === 'centered' ? 'wide' : 'centered')}
59
+ />
60
+ <div className={`workspace-grid layout-${chatWidth}${leftSidebarOpen ? '' : ' is-models-collapsed'}${rightSidebarOpen ? '' : ' is-inspector-collapsed'}`}>
61
  <ModelRail
62
+ hidden={!leftSidebarOpen}
63
  models={props.models}
64
  activeModelId={props.activeModelId}
65
  busy={busy}
66
  onModelSelect={props.onModelSelect}
67
  />
68
+ {props.conversationListOpen ? <ConversationList
69
+ conversations={props.conversations}
70
+ activeConversationId={props.activeConversationId}
71
+ onNewConversation={props.onNewConversation}
72
+ onOpenConversation={props.onOpenConversation}
73
+ onOpenConversationSettings={props.onOpenConversationSettings}
74
+ onDeleteConversation={props.onDeleteConversation}
75
+ /> : <ChatSurface
76
  activeModel={activeModel}
77
  loadedModelId={props.loadedModelId}
78
  messages={props.messages}
79
  streamState={props.streamState}
80
  modelLoadProgress={props.modelLoadProgress}
 
81
  toolsEnabled={props.toolsEnabled}
82
  runtimeStatus={props.runtimeStatus}
83
  error={props.error}
 
89
  onRegenerate={props.onRegenerate}
90
  onCancel={props.onCancel}
91
  onToolsEnabledChange={props.onToolsEnabledChange}
92
+ onOpenConversationList={props.onOpenConversationList}
93
+ onOpenSettings={props.onOpenSettings}
94
+ />}
95
  <InspectorPanel
96
+ hidden={!rightSidebarOpen}
97
  telemetry={props.telemetry}
98
  toolEvents={props.toolEvents}
99
  artifact={props.artifact}
 
102
  </div>
103
  <SettingsSheet
104
  open={props.settingsOpen}
105
+ themeMode={themeMode}
106
+ onThemeModeChange={setThemeMode}
107
  activeModel={activeModel}
108
  telemetry={props.telemetry}
109
  settings={props.settings}
src/app/components/ChatSurface.tsx CHANGED
@@ -7,7 +7,6 @@ import type {
7
  ModelOption,
8
  ShardRetryNotice,
9
  StreamState,
10
- TelemetrySnapshot,
11
  ToolRun,
12
  } from '../../lib/contracts';
13
  import { ComposerActivity } from './ComposerActivity';
@@ -19,7 +18,6 @@ interface ChatSurfaceProps {
19
  messages: ChatMessage[];
20
  streamState: StreamState;
21
  modelLoadProgress: ModelLoadProgress;
22
- telemetry: TelemetrySnapshot;
23
  toolsEnabled: boolean;
24
  runtimeStatus: string;
25
  error: string | null;
@@ -31,6 +29,8 @@ interface ChatSurfaceProps {
31
  onRegenerate: () => void;
32
  onCancel: () => void;
33
  onToolsEnabledChange: (enabled: boolean) => void;
 
 
34
  }
35
 
36
  export function ChatSurface({
@@ -39,7 +39,6 @@ export function ChatSurface({
39
  messages,
40
  streamState,
41
  modelLoadProgress,
42
- telemetry,
43
  toolsEnabled,
44
  runtimeStatus,
45
  error,
@@ -51,6 +50,8 @@ export function ChatSurface({
51
  onRegenerate,
52
  onCancel,
53
  onToolsEnabledChange,
 
 
54
  }: ChatSurfaceProps) {
55
  const [prompt, setPrompt] = useState('');
56
  const isLoading = streamState === 'loading';
@@ -83,22 +84,23 @@ export function ChatSurface({
83
  return (
84
  <main className="chat-surface">
85
  <header className="chat-header">
86
- <div>
87
- <p className="eyebrow ink">Active specimen</p>
88
- <h2>{activeModel.label}</h2>
89
- <span className="botanical-subtitle">{activeModel.botanicalName}</span>
 
 
 
 
 
90
  </div>
91
  <div className="chat-header-meta">
92
- <span>{activeModel.capability} canopy</span>
93
  <span>{Math.round(activeModel.contextLimit / 1024)}K context</span>
94
  <span className={`live-label${isLoaded ? '' : ' is-dormant'}`}><i /> {isLoaded ? 'local graph' : 'not loaded'}</span>
95
  </div>
96
  </header>
97
 
98
- <div className="chat-ruler" aria-hidden="true">
99
- <span>00</span><span>¼</span><span>½</span><span>¾</span><span>{Math.round(activeModel.contextLimit / 1024)}K</span>
100
- </div>
101
-
102
  {shardRetry && (
103
  <div className="runtime-error download-retry" role="alert">
104
  <div>
@@ -119,26 +121,24 @@ export function ChatSurface({
119
  )}
120
 
121
  <section className="transcript" aria-label="Conversation transcript" aria-live="polite">
122
- <div className="transcript-date"><span>Field log · {logDate}</span></div>
123
  {messages.length === 0 && (
124
  <div className="transcript-empty">
125
- <span aria-hidden="true">⌁</span>
126
  <h3>No observations yet</h3>
127
  <p>Choose a model, keep the backend on Auto, and send a prompt. The model is downloaded once and runs inside this browser.</p>
 
128
  </div>
129
  )}
130
- {messages.map((message, index) => (
131
- <Message key={message.id} message={message} ordinal={index + 1} />
132
  ))}
133
  </section>
134
 
135
  <form className="composer" onSubmit={handleSubmit}>
136
  <ComposerActivity
137
  modelId={activeModel.id}
138
- isLoaded={isLoaded}
139
  streamState={streamState}
140
  progress={modelLoadProgress}
141
- telemetry={telemetry}
142
  />
143
  <div className="composer-meta">
144
  <button
@@ -157,9 +157,10 @@ export function ChatSurface({
157
  <span>{runtimeStatus}</span>
158
  </div>
159
  <div className="composer-field">
160
- <label htmlFor="bonsai-prompt">Ask the local specimen</label>
161
  <textarea
162
  id="bonsai-prompt"
 
163
  value={prompt}
164
  onChange={(event) => setPrompt(event.target.value)}
165
  onKeyDown={handleKeyDown}
@@ -186,13 +187,12 @@ export function ChatSurface({
186
  );
187
  }
188
 
189
- function Message({ message, ordinal }: { message: ChatMessage; ordinal: number }) {
190
  const streaming = message.state === 'streaming';
191
  return (
192
  <article className={`message message-${message.role}${streaming ? ' is-streaming' : ''}${message.error ? ' has-error' : ''}`}>
193
  <header>
194
- <span className="message-ordinal">{String(ordinal).padStart(2, '0')}</span>
195
- <strong>{message.role === 'assistant' ? 'Bonsai observation' : message.role === 'system' ? 'Station protocol' : 'Field operator'}</strong>
196
  <time>{message.timestamp}</time>
197
  </header>
198
  <div className="message-content">
@@ -246,6 +246,10 @@ function ToolIcon() {
246
  );
247
  }
248
 
 
 
 
 
249
  function ArrowIcon() {
250
  return (
251
  <svg viewBox="0 0 20 20" aria-hidden="true">
 
7
  ModelOption,
8
  ShardRetryNotice,
9
  StreamState,
 
10
  ToolRun,
11
  } from '../../lib/contracts';
12
  import { ComposerActivity } from './ComposerActivity';
 
18
  messages: ChatMessage[];
19
  streamState: StreamState;
20
  modelLoadProgress: ModelLoadProgress;
 
21
  toolsEnabled: boolean;
22
  runtimeStatus: string;
23
  error: string | null;
 
29
  onRegenerate: () => void;
30
  onCancel: () => void;
31
  onToolsEnabledChange: (enabled: boolean) => void;
32
+ onOpenConversationList: () => void;
33
+ onOpenSettings: () => void;
34
  }
35
 
36
  export function ChatSurface({
 
39
  messages,
40
  streamState,
41
  modelLoadProgress,
 
42
  toolsEnabled,
43
  runtimeStatus,
44
  error,
 
50
  onRegenerate,
51
  onCancel,
52
  onToolsEnabledChange,
53
+ onOpenConversationList,
54
+ onOpenSettings,
55
  }: ChatSurfaceProps) {
56
  const [prompt, setPrompt] = useState('');
57
  const isLoading = streamState === 'loading';
 
84
  return (
85
  <main className="chat-surface">
86
  <header className="chat-header">
87
+ <div className="chat-header-main">
88
+ <button className="chat-list-button" type="button" disabled={isBusy} onClick={onOpenConversationList} aria-label="Open chat list">
89
+ <BackIcon /> Chats
90
+ </button>
91
+ <button className="chat-settings-button" type="button" disabled={isBusy} onClick={onOpenSettings}>Chat settings</button>
92
+ <div className="chat-title">
93
+ <h2>{activeModel.label}</h2>
94
+ <span className="botanical-subtitle">{activeModel.botanicalName}</span>
95
+ </div>
96
  </div>
97
  <div className="chat-header-meta">
98
+ <span>{activeModel.footprint} download</span>
99
  <span>{Math.round(activeModel.contextLimit / 1024)}K context</span>
100
  <span className={`live-label${isLoaded ? '' : ' is-dormant'}`}><i /> {isLoaded ? 'local graph' : 'not loaded'}</span>
101
  </div>
102
  </header>
103
 
 
 
 
 
104
  {shardRetry && (
105
  <div className="runtime-error download-retry" role="alert">
106
  <div>
 
121
  )}
122
 
123
  <section className="transcript" aria-label="Conversation transcript" aria-live="polite">
124
+ <div className="transcript-date"><span>{logDate}</span></div>
125
  {messages.length === 0 && (
126
  <div className="transcript-empty">
 
127
  <h3>No observations yet</h3>
128
  <p>Choose a model, keep the backend on Auto, and send a prompt. The model is downloaded once and runs inside this browser.</p>
129
+ <button className="system-instructions-button" type="button" onClick={onOpenSettings}>Set system instructions</button>
130
  </div>
131
  )}
132
+ {messages.map((message) => (
133
+ <Message key={message.id} message={message} />
134
  ))}
135
  </section>
136
 
137
  <form className="composer" onSubmit={handleSubmit}>
138
  <ComposerActivity
139
  modelId={activeModel.id}
 
140
  streamState={streamState}
141
  progress={modelLoadProgress}
 
142
  />
143
  <div className="composer-meta">
144
  <button
 
157
  <span>{runtimeStatus}</span>
158
  </div>
159
  <div className="composer-field">
160
+ <label htmlFor="bonsai-prompt">Prompt</label>
161
  <textarea
162
  id="bonsai-prompt"
163
+ aria-label="Ask the local specimen"
164
  value={prompt}
165
  onChange={(event) => setPrompt(event.target.value)}
166
  onKeyDown={handleKeyDown}
 
187
  );
188
  }
189
 
190
+ function Message({ message }: { message: ChatMessage }) {
191
  const streaming = message.state === 'streaming';
192
  return (
193
  <article className={`message message-${message.role}${streaming ? ' is-streaming' : ''}${message.error ? ' has-error' : ''}`}>
194
  <header>
195
+ <strong>{message.role === 'assistant' ? 'Bonsai' : message.role === 'system' ? 'System' : 'You'}</strong>
 
196
  <time>{message.timestamp}</time>
197
  </header>
198
  <div className="message-content">
 
246
  );
247
  }
248
 
249
+ function BackIcon() {
250
+ return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="m11.5 5-5 5 5 5M7 10h7" /></svg>;
251
+ }
252
+
253
  function ArrowIcon() {
254
  return (
255
  <svg viewBox="0 0 20 20" aria-hidden="true">
src/app/components/ComposerActivity.test.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { renderToStaticMarkup } from 'react-dom/server';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { ModelLoadProgress, ModelShardProgress } from '../../lib/contracts';
4
+ import { ComposerActivity, describeLoadProgress, overallLoadProgress } from './ComposerActivity';
5
+
6
+ function shard(index: number, state: ModelShardProgress['state']): ModelShardProgress {
7
+ return {
8
+ index,
9
+ path: `shard-${index + 1}.gguf`,
10
+ loadedBytes: state === 'queued' ? 0 : 100,
11
+ verifiedBytes: state === 'complete' || state === 'cached' ? 100 : 0,
12
+ totalBytes: 100,
13
+ state,
14
+ };
15
+ }
16
+
17
+ function progress(overrides: Partial<ModelLoadProgress> = {}): ModelLoadProgress {
18
+ return {
19
+ modelId: 'bonsai-27b',
20
+ modelLabel: 'Bonsai 27B',
21
+ stage: 'download',
22
+ stageProgress: 0.5,
23
+ downloadedBytes: 400,
24
+ totalBytes: 800,
25
+ residentBytes: 0,
26
+ nativeStage: null,
27
+ shards: [
28
+ shard(0, 'complete'),
29
+ shard(1, 'downloading'),
30
+ ...Array.from({ length: 6 }, (_, index) => shard(index + 2, 'queued')),
31
+ ],
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ describe('ComposerActivity', () => {
37
+ it('combines the load pipeline into one weighted overall progress value', () => {
38
+ expect(overallLoadProgress(progress())).toBeCloseTo(0.39625);
39
+ expect(overallLoadProgress(progress({ stage: 'cache', stageProgress: 0.2 }))).toBeCloseTo(0.39625);
40
+ expect(overallLoadProgress(progress({ stage: 'load', stageProgress: 0.5 }))).toBeCloseTo(0.95);
41
+ expect(overallLoadProgress(progress({ stage: 'ready' }))).toBe(1);
42
+ });
43
+
44
+ it('annotates the active shard and total ready shard count', () => {
45
+ expect(describeLoadProgress(progress())).toEqual({
46
+ label: 'Downloading shard 2 of 8',
47
+ detail: '1 of 8 shards ready · 400 B / 800 B',
48
+ });
49
+ });
50
+
51
+ it('renders exactly one progressbar during load and disappears afterwards', () => {
52
+ const loading = renderToStaticMarkup(
53
+ <ComposerActivity modelId="bonsai-27b" streamState="loading" progress={progress()} />,
54
+ );
55
+ expect(loading.match(/role="progressbar"/g)).toHaveLength(1);
56
+ expect(loading).not.toContain('Model transfer');
57
+ expect(loading).not.toContain('Load pipeline');
58
+
59
+ const ready = renderToStaticMarkup(
60
+ <ComposerActivity modelId="bonsai-27b" streamState="complete" progress={progress({ stage: 'ready' })} />,
61
+ );
62
+ expect(ready).toBe('');
63
+ });
64
+ });
src/app/components/ComposerActivity.tsx CHANGED
@@ -1,29 +1,17 @@
 
1
  import type {
2
  ModelId,
3
  ModelLoadProgress,
4
  StreamState,
5
- TelemetrySnapshot,
6
  } from '../../lib/contracts';
7
  import { formatBytes, formatPercent } from '../../lib/format';
8
 
9
  interface ComposerActivityProps {
10
  modelId: ModelId;
11
- isLoaded: boolean;
12
  streamState: StreamState;
13
  progress: ModelLoadProgress;
14
- telemetry: TelemetrySnapshot;
15
  }
16
 
17
- const LOAD_STAGES = [
18
- ['manifest', 'Manifest'],
19
- ['cache', 'Cache'],
20
- ['download', 'Download'],
21
- ['verify', 'Verify'],
22
- ['load', 'Allocate'],
23
- ] as const;
24
-
25
- type LoadStage = typeof LOAD_STAGES[number][0];
26
-
27
  function ratio(value: number, total: number): number {
28
  return total > 0 ? Math.min(1, Math.max(0, value / total)) : 0;
29
  }
@@ -33,173 +21,111 @@ function humanizeStage(value: string | null): string {
33
  return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
34
  }
35
 
36
- export function ComposerActivity({
37
- modelId,
38
- isLoaded,
39
- streamState,
40
- progress,
41
- telemetry,
42
- }: ComposerActivityProps) {
43
- const tracksModel = progress.modelId === modelId;
44
- const isLoading = streamState === 'loading' && tracksModel;
45
- const isInferring = streamState === 'streaming'
46
- || telemetry.inferencePhase === 'prefill'
47
- || telemetry.inferencePhase === 'decode';
48
- const activeStage = tracksModel && LOAD_STAGES.some(([stage]) => stage === progress.stage)
49
- ? progress.stage as LoadStage
50
- : null;
51
- const downloadRatio = tracksModel ? ratio(progress.downloadedBytes, progress.totalBytes) : 0;
52
- const verifiedBytes = displayShardsTotal(progress.shards, 'verifiedBytes');
53
- const verifiedRatio = tracksModel ? ratio(verifiedBytes, progress.totalBytes) : 0;
54
- const prefillRatio = ratio(telemetry.promptProcessed, telemetry.promptTotal);
55
- const displayShards = tracksModel ? progress.shards : [];
56
- const pipelineProgress = !tracksModel
57
- ? 0
58
- : progress.stage === 'ready'
59
- ? 1
60
- : progress.stage === 'load'
61
- ? 0.85 + Math.min(1, Math.max(0, progress.stageProgress)) * 0.15
62
- : progress.stage === 'manifest'
63
- ? Math.min(1, Math.max(0, progress.stageProgress)) * 0.05
64
- : Math.min(0.85, 0.05 + downloadRatio * 0.55 + verifiedRatio * 0.25);
 
 
 
65
 
66
- const status = isLoading
67
- ? progress.nativeStage
68
- ? humanizeStage(progress.nativeStage)
69
- : LOAD_STAGES.find(([stage]) => stage === activeStage)?.[1] ?? 'Preparing model'
70
- : isInferring
71
- ? telemetry.inferencePhase === 'prefill' ? 'Prefilling prompt' : 'Generating response'
72
- : progress.stage === 'error' && tracksModel
73
- ? 'Model load stopped'
74
- : isLoaded ? 'Model ready in this tab' : 'Model is not loaded';
75
 
76
- return (
77
- <section className={`composer-activity${isLoading ? ' is-loading' : ''}${isInferring ? ' is-inference' : ''}`} aria-label="Runtime activity">
78
- <div className="activity-stage-line" aria-hidden="true">
79
- {LOAD_STAGES.map(([stage, label]) => {
80
- const complete = isLoaded || progress.stage === 'ready' || (
81
- stage === 'manifest' ? progress.stage !== 'idle' && progress.stage !== 'manifest'
82
- : stage === 'cache' ? progress.shards.length > 0 && progress.stage !== 'manifest'
83
- : stage === 'download' ? downloadRatio >= 1
84
- : stage === 'verify' ? progress.shards.length > 0 && verifiedRatio >= 1
85
- : false
86
- );
87
- const current = isLoading && activeStage === stage;
88
- return (
89
- <span className={`${complete ? 'is-complete' : ''}${current ? ' is-current' : ''}`} key={stage}>
90
- <i /> {label}
91
- </span>
92
- );
93
- })}
94
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
 
 
96
  <div className="activity-status" aria-live="polite" aria-atomic="true">
97
  <div>
98
  <span className="activity-pulse" aria-hidden="true" />
99
- <strong>{status}</strong>
 
 
 
100
  </div>
101
- {isLoading && (
102
- <span>{formatPercent(pipelineProgress)} · {formatBytes(progress.downloadedBytes)} / {formatBytes(progress.totalBytes)}</span>
103
- )}
104
- {isInferring && telemetry.inferencePhase === 'prefill' && (
105
- <span>{telemetry.promptProcessed.toLocaleString()} / {telemetry.promptTotal.toLocaleString()} tokens</span>
106
- )}
107
- {isInferring && telemetry.inferencePhase === 'decode' && (
108
- <span>{telemetry.completionTokens.toLocaleString()} generated tokens</span>
109
- )}
110
- {!isLoading && !isInferring && isLoaded && progress.residentBytes > 0 && (
111
- <span>{formatBytes(progress.residentBytes)} allocated</span>
112
- )}
113
  </div>
114
-
115
- {isInferring ? (
116
- <div className="inference-live-grid">
117
- <LiveMetric
118
- label="Prefill"
119
- value={`${telemetry.prefillTokensPerSecond.toFixed(1)} tok/s`}
120
- detail={telemetry.promptTotal > 0 ? formatPercent(prefillRatio) : 'waiting'}
121
- progress={prefillRatio}
122
- active={telemetry.inferencePhase === 'prefill'}
123
- />
124
- <LiveMetric
125
- label="Decode"
126
- value={`${telemetry.decodeTokensPerSecond.toFixed(1)} tok/s`}
127
- detail={`${telemetry.completionTokens.toLocaleString()} tok`}
128
- progress={Math.min(1, telemetry.decodeTokensPerSecond / 20)}
129
- active={telemetry.inferencePhase === 'decode'}
130
- />
131
- </div>
132
- ) : (
133
- <div className="load-live-grid">
134
- <div className="activity-track-block">
135
- <div><span>Model transfer</span><small>{formatPercent(downloadRatio)}</small></div>
136
- <ProgressTrack label="Model transfer" value={downloadRatio} />
137
- </div>
138
- <div className="activity-track-block">
139
- <div><span>Load pipeline</span><small>{formatPercent(pipelineProgress)}</small></div>
140
- <ProgressTrack label="Overall model load pipeline" value={pipelineProgress} />
141
- </div>
142
- <div className="activity-memory">
143
- <span>Resident</span>
144
- <strong>{formatBytes(tracksModel ? progress.residentBytes : 0)}</strong>
145
- </div>
146
- </div>
147
- )}
148
-
149
- <div className={`shard-strip${displayShards.length === 0 ? ' is-empty' : ''}`} aria-label="Model shard progress">
150
- {displayShards.length === 0
151
- ? <span className="shard-placeholder">Shard detail appears during cache inspection and transfer.</span>
152
- : displayShards.map((shard) => {
153
- const shardRatio = shard.state === 'complete' || shard.state === 'cached'
154
- ? 1
155
- : shard.state === 'verifying'
156
- ? ratio(shard.verifiedBytes, shard.totalBytes)
157
- : ratio(shard.loadedBytes, shard.totalBytes);
158
- return (
159
- <div className={`shard-cell state-${shard.state}`} key={`${shard.index}-${shard.path}`} title={shard.path}>
160
- <span><b>S{shard.index + 1}</b><small>{formatPercent(shardRatio)}</small></span>
161
- <ProgressTrack label={`Shard ${shard.index + 1} ${shard.state}`} value={shardRatio} />
162
- </div>
163
- );
164
- })}
165
  </div>
166
  </section>
167
  );
168
  }
169
-
170
- function displayShardsTotal(
171
- shards: ModelLoadProgress['shards'],
172
- field: 'loadedBytes' | 'verifiedBytes',
173
- ): number {
174
- return shards.reduce((total, shard) => total + shard[field], 0);
175
- }
176
-
177
- function LiveMetric({
178
- label,
179
- value,
180
- detail,
181
- progress,
182
- active,
183
- }: {
184
- label: string;
185
- value: string;
186
- detail: string;
187
- progress: number;
188
- active: boolean;
189
- }) {
190
- return (
191
- <div className={`live-metric${active ? ' is-active' : ''}`}>
192
- <div><span>{label}</span><strong>{value}</strong><small>{detail}</small></div>
193
- <ProgressTrack label={`${label} live progress`} value={progress} />
194
- </div>
195
- );
196
- }
197
-
198
- function ProgressTrack({ label, value }: { label: string; value: number }) {
199
- const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
200
- return (
201
- <div className="activity-track" role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
202
- <span style={{ width: `${percentage}%` }} />
203
- </div>
204
- );
205
- }
 
1
+ import { useRef } from 'react';
2
  import type {
3
  ModelId,
4
  ModelLoadProgress,
5
  StreamState,
 
6
  } from '../../lib/contracts';
7
  import { formatBytes, formatPercent } from '../../lib/format';
8
 
9
  interface ComposerActivityProps {
10
  modelId: ModelId;
 
11
  streamState: StreamState;
12
  progress: ModelLoadProgress;
 
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
15
  function ratio(value: number, total: number): number {
16
  return total > 0 ? Math.min(1, Math.max(0, value / total)) : 0;
17
  }
 
21
  return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
22
  }
23
 
24
+ export function overallLoadProgress(progress: ModelLoadProgress): number {
25
+ const stageProgress = Math.min(1, Math.max(0, progress.stageProgress));
26
+ const loadedShardBytes = progress.shards.reduce((total, shard) => (
27
+ total + (shard.state === 'cached' || shard.state === 'complete'
28
+ ? shard.totalBytes
29
+ : Math.min(shard.loadedBytes, shard.totalBytes))
30
+ ), 0);
31
+ const verifiedBytes = progress.shards.reduce((total, shard) => (
32
+ total + (shard.state === 'cached' || shard.state === 'complete'
33
+ ? shard.totalBytes
34
+ : Math.min(shard.verifiedBytes, shard.totalBytes))
35
+ ), 0);
36
+ const downloadRatio = ratio(Math.max(progress.downloadedBytes, loadedShardBytes), progress.totalBytes);
37
+ const verifiedRatio = ratio(verifiedBytes, progress.totalBytes);
38
+ const transferProgress = Math.min(0.9, 0.08 + downloadRatio * 0.57 + verifiedRatio * 0.25);
39
+
40
+ switch (progress.stage) {
41
+ case 'manifest':
42
+ return stageProgress * 0.03;
43
+ case 'cache':
44
+ return Math.max(0.03 + stageProgress * 0.05, transferProgress);
45
+ case 'download':
46
+ case 'verify':
47
+ return transferProgress;
48
+ case 'load':
49
+ return 0.9 + stageProgress * 0.1;
50
+ case 'ready':
51
+ return 1;
52
+ default:
53
+ return 0;
54
+ }
55
+ }
56
 
57
+ export function describeLoadProgress(progress: ModelLoadProgress): { label: string; detail: string } {
58
+ const shardCount = progress.shards.length;
59
+ const readyShards = progress.shards.filter((shard) => shard.state === 'cached' || shard.state === 'complete').length;
60
+ const activeShard = progress.shards.find((shard) => shard.state === 'downloading' || shard.state === 'verifying');
61
+ const shardDetail = shardCount > 0 ? `${readyShards} of ${shardCount} shards ready` : 'Preparing shard map';
62
+ const byteDetail = progress.totalBytes > 0
63
+ ? `${formatBytes(progress.downloadedBytes)} / ${formatBytes(progress.totalBytes)}`
64
+ : '';
 
65
 
66
+ switch (progress.stage) {
67
+ case 'manifest':
68
+ return { label: 'Reading model manifest', detail: progress.modelLabel || 'Pinned release metadata' };
69
+ case 'cache':
70
+ return { label: 'Checking local cache', detail: shardCount > 0 ? shardDetail : 'Looking for existing shards' };
71
+ case 'download':
72
+ return {
73
+ label: activeShard ? `Downloading shard ${activeShard.index + 1} of ${shardCount}` : 'Downloading model shards',
74
+ detail: [shardDetail, byteDetail].filter(Boolean).join(' · '),
75
+ };
76
+ case 'verify':
77
+ return {
78
+ label: activeShard ? `Verifying shard ${activeShard.index + 1} of ${shardCount}` : 'Verifying model shards',
79
+ detail: [shardDetail, byteDetail].filter(Boolean).join(' · '),
80
+ };
81
+ case 'load':
82
+ return {
83
+ label: progress.nativeStage ? humanizeStage(progress.nativeStage) : 'Allocating model',
84
+ detail: progress.residentBytes > 0 ? `${formatBytes(progress.residentBytes)} resident` : 'Building the local graph',
85
+ };
86
+ default:
87
+ return { label: 'Preparing model', detail: progress.modelLabel };
88
+ }
89
+ }
90
+
91
+ export function ComposerActivity({ modelId, streamState, progress }: ComposerActivityProps) {
92
+ const displayedProgress = useRef<{ modelId: ModelId | null; value: number }>({ modelId: null, value: 0 });
93
+ const isLoading = streamState === 'loading' && progress.modelId === modelId;
94
+ if (!isLoading) {
95
+ displayedProgress.current = { modelId: null, value: 0 };
96
+ return null;
97
+ }
98
+
99
+ const measuredProgress = overallLoadProgress(progress);
100
+ const overallProgress = displayedProgress.current.modelId === modelId
101
+ ? Math.max(displayedProgress.current.value, measuredProgress)
102
+ : measuredProgress;
103
+ displayedProgress.current = { modelId, value: overallProgress };
104
+ const status = describeLoadProgress(progress);
105
+ const percentage = Math.round(overallProgress * 100);
106
 
107
+ return (
108
+ <section className="composer-activity" aria-label="Model load progress">
109
  <div className="activity-status" aria-live="polite" aria-atomic="true">
110
  <div>
111
  <span className="activity-pulse" aria-hidden="true" />
112
+ <span>
113
+ <strong>{status.label}</strong>
114
+ <small>{status.detail}</small>
115
+ </span>
116
  </div>
117
+ <b>{formatPercent(overallProgress)}</b>
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
+ <div
120
+ className="activity-track"
121
+ role="progressbar"
122
+ aria-label={`${status.label}: ${status.detail}`}
123
+ aria-valuemin={0}
124
+ aria-valuemax={100}
125
+ aria-valuenow={percentage}
126
+ >
127
+ <span style={{ width: `${percentage}%` }} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  </div>
129
  </section>
130
  );
131
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/components/ConversationList.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ChatSession } from '../../lib/contracts';
2
+ import { conversationPreview } from '../../lib/conversations';
3
+
4
+ interface ConversationListProps {
5
+ conversations: ChatSession[];
6
+ activeConversationId: string;
7
+ onNewConversation: () => void;
8
+ onOpenConversation: (conversationId: string) => void;
9
+ onOpenConversationSettings: (conversationId: string) => void;
10
+ onDeleteConversation: (conversationId: string) => void;
11
+ }
12
+
13
+ export function ConversationList({
14
+ conversations,
15
+ activeConversationId,
16
+ onNewConversation,
17
+ onOpenConversation,
18
+ onOpenConversationSettings,
19
+ onDeleteConversation,
20
+ }: ConversationListProps) {
21
+ return (
22
+ <main className="chat-surface conversation-library">
23
+ <header className="chat-header conversation-list-header">
24
+ <div>
25
+ <h2>Chats</h2>
26
+ <p>Saved only in this browser</p>
27
+ </div>
28
+ <button className="new-chat-button" type="button" onClick={onNewConversation}>New chat</button>
29
+ </header>
30
+ <section className="conversation-list" aria-label="Saved chats">
31
+ {conversations.length === 0 ? (
32
+ <div className="conversation-list-empty">
33
+ <h3>No saved chats</h3>
34
+ <p>Your first prompt creates a local session automatically.</p>
35
+ <button type="button" onClick={onNewConversation}>Start a chat</button>
36
+ </div>
37
+ ) : conversations.map((conversation) => (
38
+ <article className={`conversation-row${conversation.id === activeConversationId ? ' is-current' : ''}`} key={conversation.id}>
39
+ <button className="conversation-open" type="button" onClick={() => onOpenConversation(conversation.id)}>
40
+ <span className="conversation-title-line">
41
+ <strong>{conversation.title}</strong>
42
+ {conversation.id === activeConversationId && <small>Current</small>}
43
+ </span>
44
+ <span>{conversationPreview(conversation.messages)}</span>
45
+ <small>{conversation.modelId.replace('bonsai-', 'Bonsai ')} · {formatConversationTime(conversation.updatedAt)}</small>
46
+ </button>
47
+ <div className="conversation-actions">
48
+ <button type="button" onClick={() => onOpenConversationSettings(conversation.id)}>Settings</button>
49
+ <button
50
+ className="conversation-delete"
51
+ type="button"
52
+ aria-label={`Delete chat: ${conversation.title}`}
53
+ onClick={() => onDeleteConversation(conversation.id)}
54
+ >
55
+ Delete
56
+ </button>
57
+ </div>
58
+ </article>
59
+ ))}
60
+ </section>
61
+ </main>
62
+ );
63
+ }
64
+
65
+ function formatConversationTime(timestamp: number): string {
66
+ return new Intl.DateTimeFormat('en', {
67
+ month: 'short',
68
+ day: 'numeric',
69
+ hour: '2-digit',
70
+ minute: '2-digit',
71
+ }).format(new Date(timestamp));
72
+ }
src/app/components/Header.test.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isSafariUserAgent } from './Header';
3
+
4
+ describe('Safari performance notice routing', () => {
5
+ it('detects desktop Safari without matching Chromium user agents', () => {
6
+ expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/605.1.15 Version/26.5 Safari/605.1.15')).toBe(true);
7
+ expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 Chrome/150.0.0.0 Safari/537.36')).toBe(false);
8
+ expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 HeadlessChrome/150.0.0.0 Safari/537.36')).toBe(false);
9
+ expect(isSafariUserAgent('Mozilla/5.0 AppleWebKit/537.36 Edg/150.0.0.0 Safari/537.36')).toBe(false);
10
+ });
11
+ });
src/app/components/Header.tsx CHANGED
@@ -1,33 +1,79 @@
 
 
1
  interface HeaderProps {
2
  onOpenSettings: () => void;
 
 
 
 
 
 
3
  disabled?: boolean;
4
  }
5
 
6
- export function Header({ onOpenSettings, disabled = false }: HeaderProps) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  return (
8
  <header className="station-header">
9
- <div className="brand-block" aria-label="Bonsai field station">
10
- <span className="brand-mark" aria-hidden="true">
11
- <span />
12
- </span>
13
  <div>
14
- <p className="eyebrow">Local inference specimen no. 01</p>
15
- <h1>Bonsai <i>field station</i></h1>
16
  </div>
17
  </div>
18
 
19
- <div className="local-promise">
20
- <span className="signal-dot" aria-hidden="true" />
21
  <span>
22
- <strong>Private by construction</strong>
23
- <small>No prompts, tokens, or tools leave this device</small>
24
  </span>
25
  </div>
26
 
27
  <div className="header-actions">
28
- <div className="station-clock" aria-label="Station coordinates">
29
- <small>STATION</small>
30
- <span>GPU·LOCAL / 07</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
  <button className="icon-button" type="button" disabled={disabled} onClick={onOpenSettings} aria-label="Open runtime settings">
33
  <SettingsIcon />
@@ -37,6 +83,18 @@ export function Header({ onOpenSettings, disabled = false }: HeaderProps) {
37
  );
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  function SettingsIcon() {
41
  return (
42
  <svg viewBox="0 0 24 24" aria-hidden="true">
 
1
+ export type ChatWidth = 'centered' | 'wide';
2
+
3
  interface HeaderProps {
4
  onOpenSettings: () => void;
5
+ leftSidebarOpen: boolean;
6
+ rightSidebarOpen: boolean;
7
+ chatWidth: ChatWidth;
8
+ onToggleLeftSidebar: () => void;
9
+ onToggleRightSidebar: () => void;
10
+ onToggleChatWidth: () => void;
11
  disabled?: boolean;
12
  }
13
 
14
+ export function isSafariUserAgent(userAgent: string): boolean {
15
+ return /\bSafari\//.test(userAgent) && !/(?:HeadlessChrome|\bChrome|\bChromium|\bCriOS|\bEdg|\bOPR)\//.test(userAgent);
16
+ }
17
+
18
+ export function Header({
19
+ onOpenSettings,
20
+ leftSidebarOpen,
21
+ rightSidebarOpen,
22
+ chatWidth,
23
+ onToggleLeftSidebar,
24
+ onToggleRightSidebar,
25
+ onToggleChatWidth,
26
+ disabled = false,
27
+ }: HeaderProps) {
28
+ const safari = isSafariUserAgent(globalThis.navigator?.userAgent ?? '');
29
+
30
  return (
31
  <header className="station-header">
32
+ <div className="brand-block" aria-label="Bonsai local WebGPU chat">
33
+ <span className="brand-glyph" aria-hidden="true">B</span>
 
 
34
  <div>
35
+ <h1>Bonsai</h1>
36
+ <span>Local WebGPU chat</span>
37
  </div>
38
  </div>
39
 
40
+ <div className={`browser-notice${safari ? ' is-warning' : ''}`} role={safari ? 'status' : undefined}>
41
+ <span className="notice-mark" aria-hidden="true" />
42
  <span>
43
+ <strong>{safari ? 'Safari compatibility mode' : 'Private local inference'}</strong>
44
+ <span>{safari ? 'Use Chrome or Edge for substantially faster generation.' : 'Prompts and model output stay in this browser.'}</span>
45
  </span>
46
  </div>
47
 
48
  <div className="header-actions">
49
+ <div className="layout-controls" role="group" aria-label="Workspace layout">
50
+ <button
51
+ type="button"
52
+ aria-label={leftSidebarOpen ? 'Hide model sidebar' : 'Show model sidebar'}
53
+ aria-pressed={leftSidebarOpen}
54
+ onClick={onToggleLeftSidebar}
55
+ title={leftSidebarOpen ? 'Hide models' : 'Show models'}
56
+ >
57
+ <PanelLeftIcon />
58
+ </button>
59
+ <button
60
+ type="button"
61
+ aria-label={chatWidth === 'wide' ? 'Center chat' : 'Use wide chat'}
62
+ aria-pressed={chatWidth === 'wide'}
63
+ onClick={onToggleChatWidth}
64
+ title={chatWidth === 'wide' ? 'Centered chat' : 'Wide chat'}
65
+ >
66
+ <WidthIcon />
67
+ </button>
68
+ <button
69
+ type="button"
70
+ aria-label={rightSidebarOpen ? 'Hide telemetry sidebar' : 'Show telemetry sidebar'}
71
+ aria-pressed={rightSidebarOpen}
72
+ onClick={onToggleRightSidebar}
73
+ title={rightSidebarOpen ? 'Hide telemetry' : 'Show telemetry'}
74
+ >
75
+ <PanelRightIcon />
76
+ </button>
77
  </div>
78
  <button className="icon-button" type="button" disabled={disabled} onClick={onOpenSettings} aria-label="Open runtime settings">
79
  <SettingsIcon />
 
83
  );
84
  }
85
 
86
+ function PanelLeftIcon() {
87
+ return <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.5" y="3" width="15" height="14" rx="1.5" /><path d="M7.5 3v14" /></svg>;
88
+ }
89
+
90
+ function PanelRightIcon() {
91
+ return <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2.5" y="3" width="15" height="14" rx="1.5" /><path d="M12.5 3v14" /></svg>;
92
+ }
93
+
94
+ function WidthIcon() {
95
+ return <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 6v8M17 6v8M7 10h6M8.5 8.5 7 10l1.5 1.5M11.5 8.5 13 10l-1.5 1.5" /></svg>;
96
+ }
97
+
98
  function SettingsIcon() {
99
  return (
100
  <svg viewBox="0 0 24 24" aria-hidden="true">
src/app/components/InspectorPanel.tsx CHANGED
@@ -1,59 +1,75 @@
1
  import type { ArtifactDocument } from '../../lib/agent-tools';
2
  import type { TelemetrySnapshot, ToolEvent } from '../../lib/contracts';
3
- import { formatBytes } from '../../lib/format';
4
  import { ArtifactPane } from './ArtifactPane';
5
 
6
  interface InspectorPanelProps {
 
7
  telemetry: TelemetrySnapshot;
8
  toolEvents: ToolEvent[];
9
  artifact: ArtifactDocument | null;
10
  onCloseArtifact: () => void;
11
  }
12
 
13
- export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifact }: InspectorPanelProps) {
14
  const contextRatio = telemetry.contextLimit > 0 ? telemetry.contextUsed / telemetry.contextLimit : 0;
 
15
  const activeRate = telemetry.inferencePhase === 'prefill'
16
  ? telemetry.prefillTokensPerSecond
17
  : telemetry.decodeTokensPerSecond || telemetry.tokensPerSecond;
18
  const speedRatio = Math.min(1, Math.max(0, activeRate / 50));
19
  const gaugeLabel = telemetry.inferencePhase === 'prefill'
20
- ? 'prefill tok / sec'
21
  : telemetry.inferencePhase === 'decode'
22
- ? 'decode tok / sec'
23
- : 'average tok / sec';
 
 
 
24
 
25
  return (
26
- <aside className="inspector" aria-label="Runtime inspector">
27
  <section className="inspector-section telemetry-section" aria-labelledby="telemetry-title">
28
  <div className="section-heading">
29
- <span>02</span>
30
- <div>
31
- <p className="eyebrow">Live cambium</p>
32
- <h2 id="telemetry-title">Telemetry</h2>
33
- </div>
34
- <span className={`sampling-label phase-${telemetry.inferencePhase}`}><i /> {
35
- telemetry.inferencePhase === 'idle'
36
- ? 'ready'
37
- : telemetry.inferencePhase === 'complete' ? 'average' : 'live'
38
- }</span>
39
  </div>
40
 
41
  <div className="speed-gauge">
42
  <div className="gauge-ring" style={{ '--gauge': `${Math.round(speedRatio * 100)}%` } as React.CSSProperties}>
43
  <div>
44
  <strong>{activeRate.toFixed(1)}</strong>
45
- <small>{gaugeLabel}</small>
46
  </div>
47
  </div>
48
  <dl>
49
  <div><dt>Backend</dt><dd>{telemetry.backend}</dd></div>
50
- <div><dt>Prefill</dt><dd>{telemetry.prefillTokensPerSecond.toFixed(1)} tok/s</dd></div>
51
- <div><dt>Decode</dt><dd>{telemetry.decodeTokensPerSecond.toFixed(1)} tok/s</dd></div>
52
  <div><dt>First token</dt><dd>{telemetry.timeToFirstTokenMs} ms</dd></div>
53
  <div><dt>Graph splits</dt><dd>{telemetry.graphSplits ?? '—'}</dd></div>
54
  </dl>
55
  </div>
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  <div className="device-card">
58
  <span className="device-icon" aria-hidden="true"><i /><i /><i /></span>
59
  <div>
@@ -63,7 +79,7 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
63
  <span>{telemetry.gpuLayers}<small>layers</small></span>
64
  </div>
65
 
66
- <Meter label="Context rings" value={contextRatio} detail={`${telemetry.contextUsed.toLocaleString()} / ${telemetry.contextLimit.toLocaleString()}`} />
67
  <div className="memory-grid">
68
  <div><small>Model allocation</small><strong>{formatBytes(telemetry.modelMemoryBytes)}</strong></div>
69
  <div><small>KV allocation</small><strong>{formatBytes(telemetry.kvMemoryBytes)}</strong></div>
@@ -72,11 +88,7 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
72
 
73
  <section className="inspector-section tool-log-section" aria-labelledby="tool-log-title">
74
  <div className="section-heading compact">
75
- <span>03</span>
76
- <div>
77
- <p className="eyebrow">Instrument trace</p>
78
- <h2 id="tool-log-title">Tool log</h2>
79
- </div>
80
  </div>
81
  <ol className="event-log">
82
  {toolEvents.length === 0 && <li className="event-empty">No tool calls in this session.</li>}
@@ -95,6 +107,33 @@ export function InspectorPanel({ telemetry, toolEvents, artifact, onCloseArtifac
95
  );
96
  }
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
99
  const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
100
  return (
 
1
  import type { ArtifactDocument } from '../../lib/agent-tools';
2
  import type { TelemetrySnapshot, ToolEvent } from '../../lib/contracts';
3
+ import { formatBytes, formatPercent } from '../../lib/format';
4
  import { ArtifactPane } from './ArtifactPane';
5
 
6
  interface InspectorPanelProps {
7
+ hidden?: boolean;
8
  telemetry: TelemetrySnapshot;
9
  toolEvents: ToolEvent[];
10
  artifact: ArtifactDocument | null;
11
  onCloseArtifact: () => void;
12
  }
13
 
14
+ export function InspectorPanel({ hidden = false, telemetry, toolEvents, artifact, onCloseArtifact }: InspectorPanelProps) {
15
  const contextRatio = telemetry.contextLimit > 0 ? telemetry.contextUsed / telemetry.contextLimit : 0;
16
+ const prefillRatio = telemetry.promptTotal > 0 ? telemetry.promptProcessed / telemetry.promptTotal : 0;
17
  const activeRate = telemetry.inferencePhase === 'prefill'
18
  ? telemetry.prefillTokensPerSecond
19
  : telemetry.decodeTokensPerSecond || telemetry.tokensPerSecond;
20
  const speedRatio = Math.min(1, Math.max(0, activeRate / 50));
21
  const gaugeLabel = telemetry.inferencePhase === 'prefill'
22
+ ? 'Prefill'
23
  : telemetry.inferencePhase === 'decode'
24
+ ? 'Decode'
25
+ : 'Average';
26
+ const samplingLabel = telemetry.inferencePhase === 'idle'
27
+ ? 'Ready'
28
+ : telemetry.inferencePhase === 'complete' ? 'Average' : 'Live';
29
 
30
  return (
31
+ <aside className="inspector" aria-label="Runtime inspector" hidden={hidden}>
32
  <section className="inspector-section telemetry-section" aria-labelledby="telemetry-title">
33
  <div className="section-heading">
34
+ <h2 id="telemetry-title">Telemetry</h2>
35
+ <span className={`sampling-label phase-${telemetry.inferencePhase}`}>{samplingLabel}</span>
 
 
 
 
 
 
 
 
36
  </div>
37
 
38
  <div className="speed-gauge">
39
  <div className="gauge-ring" style={{ '--gauge': `${Math.round(speedRatio * 100)}%` } as React.CSSProperties}>
40
  <div>
41
  <strong>{activeRate.toFixed(1)}</strong>
42
+ <small><span>{gaugeLabel}</span><span>tok/s</span></small>
43
  </div>
44
  </div>
45
  <dl>
46
  <div><dt>Backend</dt><dd>{telemetry.backend}</dd></div>
 
 
47
  <div><dt>First token</dt><dd>{telemetry.timeToFirstTokenMs} ms</dd></div>
48
  <div><dt>Graph splits</dt><dd>{telemetry.graphSplits ?? '—'}</dd></div>
49
  </dl>
50
  </div>
51
 
52
+ <div className="telemetry-live-grid" aria-label="Live inference rates">
53
+ <TelemetryMetric
54
+ label="Prefill"
55
+ value={`${telemetry.prefillTokensPerSecond.toFixed(1)} tok/s`}
56
+ detail={telemetry.promptTotal > 0
57
+ ? `${telemetry.promptProcessed.toLocaleString()} / ${telemetry.promptTotal.toLocaleString()} tokens`
58
+ : 'No prompt yet'}
59
+ ratio={prefillRatio}
60
+ active={telemetry.inferencePhase === 'prefill'}
61
+ progressLabel={telemetry.promptTotal > 0 ? formatPercent(prefillRatio) : 'Waiting'}
62
+ />
63
+ <TelemetryMetric
64
+ label="Decode"
65
+ value={`${telemetry.decodeTokensPerSecond.toFixed(1)} tok/s`}
66
+ detail={`${telemetry.completionTokens.toLocaleString()} generated tokens`}
67
+ ratio={telemetry.decodeTokensPerSecond / 50}
68
+ active={telemetry.inferencePhase === 'decode'}
69
+ progressLabel="Rate / 50 tok/s"
70
+ />
71
+ </div>
72
+
73
  <div className="device-card">
74
  <span className="device-icon" aria-hidden="true"><i /><i /><i /></span>
75
  <div>
 
79
  <span>{telemetry.gpuLayers}<small>layers</small></span>
80
  </div>
81
 
82
+ <Meter label="Context" value={contextRatio} detail={`${telemetry.contextUsed.toLocaleString()} / ${telemetry.contextLimit.toLocaleString()}`} />
83
  <div className="memory-grid">
84
  <div><small>Model allocation</small><strong>{formatBytes(telemetry.modelMemoryBytes)}</strong></div>
85
  <div><small>KV allocation</small><strong>{formatBytes(telemetry.kvMemoryBytes)}</strong></div>
 
88
 
89
  <section className="inspector-section tool-log-section" aria-labelledby="tool-log-title">
90
  <div className="section-heading compact">
91
+ <h2 id="tool-log-title">Tool log</h2>
 
 
 
 
92
  </div>
93
  <ol className="event-log">
94
  {toolEvents.length === 0 && <li className="event-empty">No tool calls in this session.</li>}
 
107
  );
108
  }
109
 
110
+ function TelemetryMetric({
111
+ label,
112
+ value,
113
+ detail,
114
+ ratio,
115
+ active,
116
+ progressLabel,
117
+ }: {
118
+ label: string;
119
+ value: string;
120
+ detail: string;
121
+ ratio: number;
122
+ active: boolean;
123
+ progressLabel: string;
124
+ }) {
125
+ const percentage = Math.round(Math.min(1, Math.max(0, ratio)) * 100);
126
+ return (
127
+ <div className={`telemetry-metric${active ? ' is-active' : ''}`}>
128
+ <div><span>{label}</span><strong>{value}</strong></div>
129
+ <small>{detail}</small>
130
+ <div className="telemetry-meter" role="meter" aria-label={`${label}: ${progressLabel}`} aria-valuemin={0} aria-valuemax={100} aria-valuenow={percentage}>
131
+ <span style={{ width: `${percentage}%` }} />
132
+ </div>
133
+ </div>
134
+ );
135
+ }
136
+
137
  function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
138
  const percentage = Math.round(Math.min(1, Math.max(0, value)) * 100);
139
  return (
src/app/components/ModelRail.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import type { ModelId, ModelOption } from '../../lib/contracts';
2
 
3
  interface ModelRailProps {
 
4
  models: ModelOption[];
5
  activeModelId: ModelId;
6
  busy: boolean;
@@ -16,23 +17,23 @@ const availabilityLabels: Record<ModelOption['availability'], string> = {
16
  };
17
 
18
  export function ModelRail({
 
19
  models,
20
  activeModelId,
21
  busy,
22
  onModelSelect,
23
  }: ModelRailProps) {
24
  return (
25
- <aside className="model-rail" aria-labelledby="model-rail-title">
26
  <div className="rail-heading">
27
- <span>01</span>
28
  <div>
29
- <p className="eyebrow">Specimen drawer</p>
30
- <h2 id="model-rail-title">Choose canopy</h2>
31
  </div>
32
  </div>
33
 
34
  <div className="model-list" role="list" aria-label="Available Bonsai models">
35
- {models.map((model, index) => {
36
  const active = model.id === activeModelId;
37
  return (
38
  <article className={`model-card${active ? ' is-active' : ''}`} key={model.id} role="listitem">
@@ -43,7 +44,6 @@ export function ModelRail({
43
  onClick={() => onModelSelect(model.id)}
44
  aria-pressed={active}
45
  >
46
- <span className="model-index">{String(index + 1).padStart(2, '0')}</span>
47
  <span className="model-copy">
48
  <span className="model-title-line">
49
  <strong>{model.parameters}</strong>
@@ -64,7 +64,7 @@ export function ModelRail({
64
 
65
  <footer className="rail-footnote">
66
  <span>GGUF · Q1_0</span>
67
- <span>Manifest v2 · commit pinned</span>
68
  </footer>
69
  </aside>
70
  );
 
1
  import type { ModelId, ModelOption } from '../../lib/contracts';
2
 
3
  interface ModelRailProps {
4
+ hidden?: boolean;
5
  models: ModelOption[];
6
  activeModelId: ModelId;
7
  busy: boolean;
 
17
  };
18
 
19
  export function ModelRail({
20
+ hidden = false,
21
  models,
22
  activeModelId,
23
  busy,
24
  onModelSelect,
25
  }: ModelRailProps) {
26
  return (
27
+ <aside className="model-rail" aria-labelledby="model-rail-title" hidden={hidden}>
28
  <div className="rail-heading">
 
29
  <div>
30
+ <h2 id="model-rail-title">Models</h2>
31
+ <p>Choose what runs in this browser.</p>
32
  </div>
33
  </div>
34
 
35
  <div className="model-list" role="list" aria-label="Available Bonsai models">
36
+ {models.map((model) => {
37
  const active = model.id === activeModelId;
38
  return (
39
  <article className={`model-card${active ? ' is-active' : ''}`} key={model.id} role="listitem">
 
44
  onClick={() => onModelSelect(model.id)}
45
  aria-pressed={active}
46
  >
 
47
  <span className="model-copy">
48
  <span className="model-title-line">
49
  <strong>{model.parameters}</strong>
 
64
 
65
  <footer className="rail-footnote">
66
  <span>GGUF · Q1_0</span>
67
+ <span>Commit pinned</span>
68
  </footer>
69
  </aside>
70
  );
src/app/components/SettingsSheet.tsx CHANGED
@@ -1,8 +1,11 @@
1
  import { useEffect, useRef } from 'react';
2
  import type { ModelOption, RuntimeSettings, TelemetrySnapshot } from '../../lib/contracts';
 
3
 
4
  interface SettingsSheetProps {
5
  open: boolean;
 
 
6
  activeModel: ModelOption;
7
  telemetry: TelemetrySnapshot;
8
  settings: RuntimeSettings;
@@ -15,6 +18,8 @@ interface SettingsSheetProps {
15
 
16
  export function SettingsSheet({
17
  open,
 
 
18
  activeModel,
19
  telemetry,
20
  settings,
@@ -52,15 +57,19 @@ export function SettingsSheet({
52
  }}>
53
  <section className="settings-sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
54
  <header>
55
- <div>
56
- <p className="eyebrow">Instrument calibration</p>
57
- <h2 id="settings-title">Runtime settings</h2>
58
- </div>
59
  <button ref={closeButtonRef} className="sheet-close" type="button" onClick={onClose} aria-label="Close settings">×</button>
60
  </header>
61
 
62
  <label className="setting-group setting-select">
63
- <span className="setting-number">01</span>
 
 
 
 
 
 
 
64
  <div><small>Compute backend</small><strong>Auto prefers WebGPU</strong></div>
65
  <select value={settings.backend} onChange={(event) => update('backend', event.target.value as RuntimeSettings['backend'])}>
66
  <option value="auto">Auto</option>
@@ -69,7 +78,6 @@ export function SettingsSheet({
69
  </select>
70
  </label>
71
  <div className="setting-group">
72
- <span className="setting-number">02</span>
73
  <div><small>Selected specimen</small><strong>{activeModel.label}</strong></div>
74
  <span className="setting-locked">{activeModel.footprint}</span>
75
  </div>
@@ -100,7 +108,7 @@ export function SettingsSheet({
100
  </label>
101
 
102
  <label className="system-prompt-setting">
103
- <span><small>System prompt</small><strong>Stored only in this browser</strong></span>
104
  <textarea rows={5} value={settings.systemPrompt} onChange={(event) => update('systemPrompt', event.target.value.slice(0, 8192))} />
105
  </label>
106
 
 
1
  import { useEffect, useRef } from 'react';
2
  import type { ModelOption, RuntimeSettings, TelemetrySnapshot } from '../../lib/contracts';
3
+ import type { ThemeMode } from '../theme';
4
 
5
  interface SettingsSheetProps {
6
  open: boolean;
7
+ themeMode: ThemeMode;
8
+ onThemeModeChange: (theme: ThemeMode) => void;
9
  activeModel: ModelOption;
10
  telemetry: TelemetrySnapshot;
11
  settings: RuntimeSettings;
 
18
 
19
  export function SettingsSheet({
20
  open,
21
+ themeMode,
22
+ onThemeModeChange,
23
  activeModel,
24
  telemetry,
25
  settings,
 
57
  }}>
58
  <section className="settings-sheet" role="dialog" aria-modal="true" aria-labelledby="settings-title">
59
  <header>
60
+ <h2 id="settings-title">Chat settings</h2>
 
 
 
61
  <button ref={closeButtonRef} className="sheet-close" type="button" onClick={onClose} aria-label="Close settings">×</button>
62
  </header>
63
 
64
  <label className="setting-group setting-select">
65
+ <div><small>Appearance</small><strong>Follow the system or choose a theme</strong></div>
66
+ <select aria-label="Color theme" value={themeMode} onChange={(event) => onThemeModeChange(event.target.value as ThemeMode)}>
67
+ <option value="system">System</option>
68
+ <option value="light">Light</option>
69
+ <option value="dark">Dark</option>
70
+ </select>
71
+ </label>
72
+ <label className="setting-group setting-select">
73
  <div><small>Compute backend</small><strong>Auto prefers WebGPU</strong></div>
74
  <select value={settings.backend} onChange={(event) => update('backend', event.target.value as RuntimeSettings['backend'])}>
75
  <option value="auto">Auto</option>
 
78
  </select>
79
  </label>
80
  <div className="setting-group">
 
81
  <div><small>Selected specimen</small><strong>{activeModel.label}</strong></div>
82
  <span className="setting-locked">{activeModel.footprint}</span>
83
  </div>
 
108
  </label>
109
 
110
  <label className="system-prompt-setting">
111
+ <span><small>System instructions</small><strong>Saved with this chat</strong></span>
112
  <textarea rows={5} value={settings.systemPrompt} onChange={(event) => update('systemPrompt', event.target.value.slice(0, 8192))} />
113
  </label>
114
 
src/app/styles.css CHANGED
@@ -1,24 +1,24 @@
1
  :root {
2
  color-scheme: dark;
3
- --ink: #111512;
4
- --ink-raised: #191e1a;
5
- --ink-soft: #252b25;
6
- --paper: #e8dfca;
7
- --paper-deep: #d8cdb4;
8
- --paper-ghost: rgba(232, 223, 202, 0.62);
9
- --phosphor: #b9ff45;
10
- --phosphor-dim: rgba(185, 255, 69, 0.16);
11
- --moss: #6f8062;
12
- --rust: #c96f43;
13
- --line-light: rgba(232, 223, 202, 0.14);
14
- --line-dark: rgba(17, 21, 18, 0.18);
15
- --display: "Bodoni 72", Didot, "Iowan Old Style", Baskerville, serif;
16
- --body: "Avenir Next Condensed", "Gill Sans", "Trebuchet MS", sans-serif;
17
  --mono: "Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;
18
- --type-utility: clamp(0.6875rem, 0.15vw + 0.65rem, 0.8125rem);
19
- --type-caption: clamp(0.75rem, 0.18vw + 0.7rem, 0.875rem);
20
- --type-body: clamp(1rem, 0.22vw + 0.95rem, 1.125rem);
21
- --type-heading: clamp(1.75rem, 1.15vw + 1.45rem, 2.5rem);
22
  font-family: var(--body);
23
  font-synthesis: none;
24
  text-rendering: optimizeLegibility;
@@ -36,10 +36,7 @@ body {
36
  margin: 0;
37
  min-width: 320px;
38
  min-height: 100vh;
39
- background:
40
- linear-gradient(90deg, rgba(17, 21, 18, 0.06) 1px, transparent 1px) 0 0 / 22px 22px,
41
- linear-gradient(rgba(17, 21, 18, 0.045) 1px, transparent 1px) 0 0 / 22px 22px,
42
- var(--paper);
43
  color: var(--paper);
44
  font-size: var(--type-body);
45
  }
@@ -72,62 +69,77 @@ button:disabled {
72
  }
73
 
74
  .station-frame {
 
 
 
 
 
 
75
  min-height: 100dvh;
 
76
  isolation: isolate;
77
  animation: station-in 580ms cubic-bezier(.16, 1, .3, 1) both;
78
  }
79
 
 
 
 
 
 
 
 
 
 
 
80
  .station-frame::before {
81
- position: fixed;
82
- z-index: 20;
83
- inset: 0;
84
- pointer-events: none;
85
- content: "";
86
- opacity: 0.14;
87
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.3'/%3E%3C/svg%3E");
88
- mix-blend-mode: soft-light;
89
  }
90
 
91
  .station-header {
92
- min-height: 76px;
93
  display: grid;
94
- grid-template-columns: minmax(330px, 1fr) auto minmax(220px, 1fr);
95
  align-items: stretch;
96
  background: var(--ink);
97
- border-bottom: 1px solid var(--phosphor);
98
- box-shadow: 0 12px 38px rgba(17, 21, 18, 0.22);
99
  position: relative;
100
  z-index: 10;
101
  }
102
 
103
  .brand-block {
104
  display: flex;
105
- gap: 14px;
106
  align-items: center;
107
- padding: 12px 24px;
 
108
  }
109
 
110
  .brand-block h1 {
111
  margin: 0;
112
- font: 400 clamp(25px, 2vw, 34px) / 0.9 var(--display);
113
- letter-spacing: -0.035em;
114
  }
115
 
116
- .brand-block h1 i {
117
- color: var(--phosphor);
118
- font-weight: 400;
119
  }
120
 
121
- .brand-mark {
122
- width: 42px;
123
- height: 42px;
124
- flex: 0 0 42px;
 
 
 
 
 
125
  display: grid;
126
  place-items: center;
127
- border: 1px solid rgba(185, 255, 69, 0.48);
128
- border-radius: 50%;
129
- background: repeating-radial-gradient(ellipse at 52% 62%, transparent 0 4px, rgba(185, 255, 69, 0.42) 4.5px 5px);
130
- transform: rotate(-11deg);
131
  }
132
 
133
  .brand-mark::before,
@@ -173,49 +185,92 @@ button:disabled {
173
  color: rgba(17, 21, 18, 0.52);
174
  }
175
 
176
- .local-promise {
177
  display: flex;
178
  align-items: center;
179
- gap: 11px;
180
- min-width: 360px;
181
- padding: 0 28px;
182
- border-inline: 1px solid var(--line-light);
183
- background: linear-gradient(115deg, rgba(185, 255, 69, 0.08), transparent 66%);
184
  }
185
 
186
- .signal-dot,
187
- .live-label i,
188
- .sampling-label i {
189
- display: inline-block;
190
- width: 7px;
191
- height: 7px;
192
- border-radius: 50%;
193
  background: var(--phosphor);
194
- box-shadow: 0 0 0 5px var(--phosphor-dim), 0 0 17px rgba(185, 255, 69, 0.62);
195
  }
196
 
197
- .local-promise span:last-child {
198
  display: grid;
199
- gap: 3px;
200
  }
201
 
202
- .local-promise strong {
203
- color: var(--phosphor);
204
- font: 600 var(--type-caption)/1 var(--mono);
205
- letter-spacing: 0.13em;
206
- text-transform: uppercase;
207
  }
208
 
209
- .local-promise small {
210
  color: var(--paper-ghost);
211
  font: var(--type-caption)/1.25 var(--body);
212
  }
213
 
 
 
 
 
 
 
 
 
214
  .header-actions {
215
  justify-self: end;
216
  display: flex;
217
  align-items: center;
218
- padding-right: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
 
221
  .station-clock {
@@ -238,21 +293,11 @@ button:disabled {
238
  }
239
 
240
  .icon-button {
241
- width: 44px;
242
- height: 44px;
243
- display: grid;
244
- place-items: center;
245
- border: 1px solid rgba(232, 223, 202, 0.24);
246
- border-radius: 50%;
247
- background: transparent;
248
- cursor: pointer;
249
- transition: color 160ms ease, border-color 160ms ease, transform 160ms ease;
250
  }
251
 
252
  .icon-button:hover {
253
- color: var(--phosphor);
254
- border-color: var(--phosphor);
255
- transform: rotate(-7deg);
256
  }
257
 
258
  .icon-button svg {
@@ -263,10 +308,37 @@ button:disabled {
263
  }
264
 
265
  .workspace-grid {
266
- min-height: calc(100dvh - 76px);
267
  display: grid;
268
- grid-template-columns: clamp(280px, 19vw, 340px) minmax(560px, 1fr) clamp(340px, 23vw, 410px);
269
  grid-template-areas: "models chat inspector";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
 
272
  .model-rail,
@@ -282,22 +354,11 @@ button:disabled {
282
  display: flex;
283
  flex-direction: column;
284
  border-right: 1px solid var(--line-light);
285
- padding: 24px 16px 14px;
286
- animation: rise-in 520ms 100ms cubic-bezier(.16, 1, .3, 1) both;
287
  }
288
 
289
  .model-rail::before {
290
- position: absolute;
291
- z-index: 0;
292
- left: -103px;
293
- bottom: 44px;
294
- width: 265px;
295
- height: 265px;
296
- border-radius: 43% 57% 52% 48%;
297
- content: "";
298
- opacity: 0.18;
299
- background: repeating-radial-gradient(ellipse at 51% 47%, transparent 0 7px, var(--moss) 7.5px 8.5px);
300
- transform: rotate(18deg);
301
  }
302
 
303
  .rail-heading,
@@ -305,8 +366,8 @@ button:disabled {
305
  position: relative;
306
  z-index: 1;
307
  display: flex;
308
- align-items: flex-start;
309
- gap: 11px;
310
  }
311
 
312
  .rail-heading > span,
@@ -320,8 +381,15 @@ button:disabled {
320
  .rail-heading h2,
321
  .section-heading h2 {
322
  margin: 0;
323
- font: 400 26px/1 var(--display);
324
- letter-spacing: -0.025em;
 
 
 
 
 
 
 
325
  }
326
 
327
  .model-list {
@@ -333,22 +401,22 @@ button:disabled {
333
  }
334
 
335
  .model-card {
336
- border: 1px solid transparent;
 
337
  transition: border-color 180ms ease, background 180ms ease;
338
  }
339
 
340
  .model-card.is-active {
341
- border-color: var(--phosphor);
342
  background: var(--phosphor-dim);
343
- box-shadow: inset 3px 0 0 var(--phosphor);
344
  }
345
 
346
  .model-select {
347
  width: 100%;
348
  display: grid;
349
- grid-template-columns: 23px 1fr auto;
350
  gap: 8px;
351
- min-height: 76px;
352
  padding: 13px 9px 12px;
353
  text-align: left;
354
  color: var(--paper);
@@ -358,7 +426,7 @@ button:disabled {
358
  }
359
 
360
  .model-card:not(.is-active) .model-select:hover {
361
- background: rgba(232, 223, 202, 0.055);
362
  }
363
 
364
  .model-index {
@@ -381,7 +449,7 @@ button:disabled {
381
  }
382
 
383
  .model-title-line strong {
384
- font: 500 24px/1 var(--display);
385
  }
386
 
387
  .model-copy em {
@@ -401,8 +469,7 @@ button:disabled {
401
  .availability {
402
  padding: 2px 4px;
403
  font: 600 var(--type-utility)/1 var(--mono);
404
- letter-spacing: 0.08em;
405
- text-transform: uppercase;
406
  border: 1px solid var(--line-light);
407
  }
408
 
@@ -428,7 +495,7 @@ button:disabled {
428
 
429
  .model-limit-reason {
430
  display: block;
431
- margin: -4px 8px 9px 30px;
432
  color: rgba(232, 223, 202, 0.58);
433
  font: var(--type-utility)/1.35 var(--mono);
434
  }
@@ -448,17 +515,13 @@ button:disabled {
448
  .chat-surface {
449
  grid-area: chat;
450
  min-width: 0;
451
- min-height: calc(100dvh - 76px);
452
- height: calc(100dvh - 76px);
453
  display: grid;
454
- grid-template-rows: auto 19px minmax(0, 1fr) auto;
455
  overflow: hidden;
456
- color: var(--ink);
457
- background:
458
- radial-gradient(ellipse at 82% 16%, transparent 0 77px, rgba(17, 21, 18, 0.055) 78px 79px, transparent 80px 89px, rgba(17, 21, 18, 0.045) 90px 91px, transparent 92px),
459
- linear-gradient(100deg, transparent 0 67%, rgba(17, 21, 18, 0.025) 67.2% 67.35%, transparent 67.5%),
460
- var(--paper);
461
- animation: rise-in 560ms 160ms cubic-bezier(.16, 1, .3, 1) both;
462
  position: relative;
463
  }
464
 
@@ -467,22 +530,64 @@ button:disabled {
467
  align-items: center;
468
  justify-content: space-between;
469
  gap: 20px;
470
- min-height: 82px;
471
- padding: 17px clamp(21px, 3vw, 44px) 13px;
472
- border-bottom: 1px solid var(--line-dark);
473
  }
474
 
475
  .chat-header h2 {
476
  display: inline;
477
  margin: 0;
478
- font: 500 var(--type-heading)/1 var(--display);
479
- letter-spacing: -0.045em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  }
481
 
482
  .botanical-subtitle {
483
- margin-left: 10px;
484
  color: rgba(17, 21, 18, 0.46);
485
- font: italic var(--type-caption)/1 var(--display);
486
  }
487
 
488
  .chat-header-meta {
@@ -491,8 +596,7 @@ button:disabled {
491
  gap: 5px 13px;
492
  color: rgba(17, 21, 18, 0.55);
493
  font: var(--type-utility)/1.2 var(--mono);
494
- text-transform: uppercase;
495
- letter-spacing: 0.07em;
496
  }
497
 
498
  .live-label {
@@ -501,14 +605,14 @@ button:disabled {
501
  display: flex;
502
  align-items: center;
503
  gap: 7px;
504
- color: #3d551d;
505
  }
506
 
507
  .live-label i {
508
  width: 5px;
509
  height: 5px;
510
- box-shadow: 0 0 0 3px rgba(90, 120, 43, 0.13);
511
- background: #5d7e2e;
512
  }
513
 
514
  .live-label.is-dormant {
@@ -517,7 +621,7 @@ button:disabled {
517
 
518
  .live-label.is-dormant i {
519
  background: rgba(17, 21, 18, 0.36);
520
- box-shadow: 0 0 0 3px rgba(17, 21, 18, 0.09);
521
  }
522
 
523
  .runtime-error {
@@ -599,16 +703,12 @@ button:disabled {
599
  margin-bottom: 22px;
600
  color: rgba(17, 21, 18, 0.45);
601
  font: var(--type-utility)/1 var(--mono);
602
- letter-spacing: 0.1em;
603
- text-transform: uppercase;
604
  }
605
 
606
  .transcript-date::before,
607
  .transcript-date::after {
608
- content: "";
609
- height: 1px;
610
- flex: 1;
611
- background: rgba(17, 21, 18, 0.14);
612
  }
613
 
614
  .transcript-empty {
@@ -618,16 +718,10 @@ button:disabled {
618
  color: rgba(17, 21, 18, 0.5);
619
  }
620
 
621
- .transcript-empty > span {
622
- display: block;
623
- color: #507324;
624
- font: 42px/1 var(--display);
625
- }
626
-
627
  .transcript-empty h3 {
628
- margin: 8px 0 6px;
629
  color: var(--ink);
630
- font: 400 25px/1 var(--display);
631
  }
632
 
633
  .transcript-empty p {
@@ -636,10 +730,159 @@ button:disabled {
636
  line-height: 1.5;
637
  }
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  .message {
640
- max-width: 690px;
641
  margin-bottom: 25px;
642
- animation: note-in 430ms cubic-bezier(.16, 1, .3, 1) both;
643
  }
644
 
645
  .message > header {
@@ -649,8 +892,7 @@ button:disabled {
649
  margin-bottom: 8px;
650
  color: rgba(17, 21, 18, 0.47);
651
  font: var(--type-utility)/1.2 var(--mono);
652
- text-transform: uppercase;
653
- letter-spacing: 0.07em;
654
  }
655
 
656
  .message > header strong {
@@ -747,16 +989,15 @@ button:disabled {
747
  margin: 0 0 11px;
748
  padding: 8px 10px;
749
  color: rgba(17, 21, 18, 0.62);
750
- border: 1px dashed rgba(17, 21, 18, 0.24);
751
- background: rgba(17, 21, 18, 0.035);
 
752
  font-size: var(--type-caption);
753
  }
754
 
755
  .reasoning-block summary {
756
  color: #4d6728;
757
- font: 700 var(--type-utility)/1 var(--mono);
758
- letter-spacing: 0.08em;
759
- text-transform: uppercase;
760
  cursor: pointer;
761
  }
762
 
@@ -782,14 +1023,14 @@ button:disabled {
782
 
783
  .message-assistant {
784
  padding-left: 22px;
785
- border-left: 2px solid var(--ink);
786
  }
787
 
788
  .message-assistant .message-content > p::first-letter {
789
- float: left;
790
- margin: 1px 6px 0 0;
791
- color: #38511d;
792
- font: 42px/0.75 var(--display);
793
  }
794
 
795
  .message-user {
@@ -798,8 +1039,7 @@ button:disabled {
798
  padding: 12px 15px 13px;
799
  color: var(--paper);
800
  background: var(--ink);
801
- box-shadow: 5px 5px 0 rgba(17, 21, 18, 0.12);
802
- clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%);
803
  }
804
 
805
  .message-user > header,
@@ -817,7 +1057,7 @@ button:disabled {
817
  margin-top: 9px;
818
  padding-top: 7px;
819
  color: rgba(17, 21, 18, 0.43);
820
- border-top: 1px dashed rgba(17, 21, 18, 0.19);
821
  font: var(--type-utility)/1.2 var(--mono);
822
  }
823
 
@@ -885,287 +1125,107 @@ button:disabled {
885
  background: var(--rust);
886
  }
887
 
888
- .tool-call > div {
889
- padding: 9px 10px;
890
- border-top: 1px solid rgba(17, 21, 18, 0.13);
891
- }
892
-
893
- .tool-call > div p {
894
- margin: 5px 0 0;
895
- color: rgba(17, 21, 18, 0.58);
896
- font-size: var(--type-caption);
897
- }
898
-
899
- .composer {
900
- position: relative;
901
- padding: 12px clamp(21px, 3vw, 44px) 18px;
902
- border-top: 1px solid var(--line-dark);
903
- background: rgba(216, 205, 180, 0.82);
904
- box-shadow: 0 -15px 38px rgba(17, 21, 18, 0.06);
905
- backdrop-filter: blur(9px);
906
- }
907
-
908
- .composer::before {
909
- position: absolute;
910
- top: -5px;
911
- right: 0;
912
- left: 0;
913
- height: 5px;
914
- content: "";
915
- background: repeating-linear-gradient(90deg, var(--ink) 0 1px, transparent 1px 12px);
916
- opacity: 0.23;
917
- }
918
-
919
- .composer-activity {
920
- height: 164px;
921
- display: grid;
922
- grid-template-rows: 24px 26px 40px 34px;
923
- gap: 6px;
924
- margin-bottom: 10px;
925
- padding: 11px 12px;
926
- overflow: hidden;
927
- color: var(--paper);
928
- border: 1px solid rgba(17, 21, 18, 0.82);
929
- background:
930
- linear-gradient(105deg, rgba(185, 255, 69, 0.07), transparent 58%),
931
- var(--ink);
932
- box-shadow: 4px 4px 0 rgba(17, 21, 18, 0.12);
933
- }
934
-
935
- .activity-stage-line {
936
- display: grid;
937
- grid-template-columns: repeat(5, minmax(0, 1fr));
938
- gap: 6px;
939
- align-items: start;
940
- }
941
-
942
- .activity-stage-line span {
943
- min-width: 0;
944
- display: flex;
945
- align-items: center;
946
- gap: 5px;
947
- padding-top: 5px;
948
- overflow: hidden;
949
- color: rgba(232, 223, 202, 0.34);
950
- border-top: 1px solid rgba(232, 223, 202, 0.14);
951
- font: 700 var(--type-utility)/1 var(--mono);
952
- letter-spacing: 0.04em;
953
- text-overflow: ellipsis;
954
- text-transform: uppercase;
955
- white-space: nowrap;
956
- }
957
-
958
- .activity-stage-line i {
959
- width: 6px;
960
- height: 6px;
961
- flex: 0 0 6px;
962
- border: 1px solid currentColor;
963
- border-radius: 50%;
964
- }
965
-
966
- .activity-stage-line span.is-complete,
967
- .activity-stage-line span.is-current {
968
- color: var(--phosphor);
969
- border-color: rgba(185, 255, 69, 0.52);
970
- }
971
-
972
- .activity-stage-line span.is-complete i {
973
- background: var(--phosphor);
974
- }
975
-
976
- .activity-stage-line span.is-current i {
977
- box-shadow: 0 0 0 4px var(--phosphor-dim);
978
- }
979
-
980
- .activity-status,
981
- .activity-status > div {
982
- display: flex;
983
- align-items: center;
984
- }
985
-
986
- .activity-status {
987
- min-width: 0;
988
- justify-content: space-between;
989
- gap: 12px;
990
- border-bottom: 1px solid var(--line-light);
991
- font: var(--type-utility)/1 var(--mono);
992
- }
993
-
994
- .activity-status > div {
995
- min-width: 0;
996
- gap: 8px;
997
- }
998
-
999
- .activity-status strong {
1000
- overflow: hidden;
1001
- color: var(--paper);
1002
- font-weight: 600;
1003
- letter-spacing: 0.04em;
1004
- text-overflow: ellipsis;
1005
- text-transform: uppercase;
1006
- white-space: nowrap;
1007
- }
1008
-
1009
- .activity-status > span {
1010
- flex: 0 0 auto;
1011
- color: rgba(232, 223, 202, 0.5);
1012
- }
1013
-
1014
- .activity-pulse {
1015
- width: 7px;
1016
- height: 7px;
1017
- flex: 0 0 7px;
1018
- border-radius: 50%;
1019
- background: rgba(232, 223, 202, 0.35);
1020
- }
1021
-
1022
- .composer-activity.is-loading .activity-pulse,
1023
- .composer-activity.is-inference .activity-pulse {
1024
- background: var(--phosphor);
1025
- box-shadow: 0 0 0 4px var(--phosphor-dim);
1026
- animation: activity-pulse 1.15s ease-in-out infinite;
1027
- }
1028
-
1029
- .load-live-grid {
1030
- display: grid;
1031
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
1032
- gap: 12px;
1033
- align-items: end;
1034
- }
1035
-
1036
- .activity-track-block,
1037
- .live-metric {
1038
- min-width: 0;
1039
- display: grid;
1040
- gap: 6px;
1041
- }
1042
-
1043
- .activity-track-block > div,
1044
- .live-metric > div {
1045
- display: flex;
1046
- align-items: baseline;
1047
- justify-content: space-between;
1048
- gap: 8px;
1049
- font: var(--type-utility)/1 var(--mono);
1050
- }
1051
-
1052
- .activity-track-block span,
1053
- .live-metric span,
1054
- .activity-memory span {
1055
- color: rgba(232, 223, 202, 0.46);
1056
- text-transform: uppercase;
1057
- }
1058
-
1059
- .activity-track-block small,
1060
- .live-metric small {
1061
- color: rgba(232, 223, 202, 0.5);
1062
- font-size: inherit;
1063
- }
1064
-
1065
- .activity-memory {
1066
- min-width: 86px;
1067
- display: grid;
1068
- gap: 6px;
1069
- justify-items: end;
1070
- font: var(--type-utility)/1 var(--mono);
1071
- }
1072
-
1073
- .activity-memory strong {
1074
- color: var(--phosphor);
1075
- font-size: var(--type-caption);
1076
- font-weight: 600;
1077
- }
1078
-
1079
- .activity-track {
1080
- height: 5px;
1081
- overflow: hidden;
1082
- border: 1px solid rgba(232, 223, 202, 0.18);
1083
- background: rgba(232, 223, 202, 0.055);
1084
- }
1085
-
1086
- .activity-track > span {
1087
- display: block;
1088
- height: 100%;
1089
- background: repeating-linear-gradient(90deg, var(--phosphor) 0 7px, transparent 7px 9px);
1090
- box-shadow: 0 0 10px rgba(185, 255, 69, 0.34);
1091
- transition: width 120ms linear;
1092
- }
1093
-
1094
- .inference-live-grid {
1095
- display: grid;
1096
- grid-template-columns: repeat(2, minmax(0, 1fr));
1097
- gap: 12px;
1098
  }
1099
 
1100
- .live-metric {
1101
- padding: 5px 8px 6px;
1102
- border: 1px solid var(--line-light);
1103
- opacity: 0.64;
1104
  }
1105
 
1106
- .live-metric.is-active {
1107
- border-color: rgba(185, 255, 69, 0.48);
1108
- background: rgba(185, 255, 69, 0.06);
1109
- opacity: 1;
 
1110
  }
1111
 
1112
- .live-metric strong {
1113
- margin-left: auto;
1114
- color: var(--phosphor);
1115
- font-size: var(--type-caption);
1116
- font-weight: 600;
1117
  }
1118
 
1119
- .shard-strip {
1120
- min-width: 0;
1121
  display: grid;
1122
- grid-template-columns: repeat(8, minmax(0, 1fr));
1123
- gap: 5px;
1124
- align-items: end;
 
 
 
 
1125
  }
1126
 
1127
- .shard-strip.is-empty {
 
1128
  display: flex;
1129
  align-items: center;
1130
- border-top: 1px dashed var(--line-light);
1131
  }
1132
 
1133
- .shard-placeholder {
1134
- color: rgba(232, 223, 202, 0.32);
1135
- font: var(--type-utility)/1.2 var(--mono);
 
 
1136
  }
1137
 
1138
- .shard-cell {
1139
  min-width: 0;
1140
- display: grid;
1141
- gap: 5px;
1142
- opacity: 0.55;
1143
  }
1144
 
1145
- .shard-cell.state-downloading,
1146
- .shard-cell.state-verifying,
1147
- .shard-cell.state-complete,
1148
- .shard-cell.state-cached {
1149
- opacity: 1;
1150
  }
1151
 
1152
- .shard-cell > span {
1153
- display: flex;
1154
- justify-content: space-between;
1155
- gap: 3px;
1156
- color: rgba(232, 223, 202, 0.48);
1157
- font: var(--type-utility)/1 var(--mono);
1158
  }
1159
 
1160
- .shard-cell b {
1161
  color: var(--paper);
1162
  font-weight: 600;
1163
  }
1164
 
1165
- .shard-cell small {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1166
  overflow: hidden;
1167
- font-size: inherit;
1168
- text-overflow: ellipsis;
 
 
 
 
 
 
 
 
1169
  }
1170
 
1171
  .composer-meta {
@@ -1189,7 +1249,6 @@ button:disabled {
1189
  border: 0;
1190
  background: transparent;
1191
  font: 700 var(--type-utility)/1 var(--mono);
1192
- text-transform: uppercase;
1193
  cursor: pointer;
1194
  }
1195
 
@@ -1205,7 +1264,6 @@ button:disabled {
1205
  border: 0;
1206
  background: transparent;
1207
  font: 700 var(--type-utility)/1 var(--mono);
1208
- text-transform: uppercase;
1209
  cursor: pointer;
1210
  }
1211
 
@@ -1226,9 +1284,10 @@ button:disabled {
1226
  grid-template-columns: 1fr auto;
1227
  position: relative;
1228
  min-height: 84px;
1229
- border: 1px solid var(--ink);
1230
- background: rgba(232, 223, 202, 0.83);
1231
- box-shadow: 4px 4px 0 rgba(17, 21, 18, 0.1);
 
1232
  }
1233
 
1234
  .composer-field label {
@@ -1237,15 +1296,14 @@ button:disabled {
1237
  left: 11px;
1238
  color: rgba(17, 21, 18, 0.45);
1239
  font: var(--type-utility)/1 var(--mono);
1240
- letter-spacing: 0.09em;
1241
- text-transform: uppercase;
1242
  }
1243
 
1244
  .composer-field textarea {
1245
  min-width: 0;
1246
  resize: none;
1247
  padding: 23px 13px 9px;
1248
- color: var(--ink);
1249
  border: 0;
1250
  outline: none;
1251
  background: transparent;
@@ -1266,16 +1324,15 @@ button:disabled {
1266
  margin: 7px;
1267
  color: var(--ink);
1268
  border: 1px solid var(--ink);
 
1269
  background: var(--phosphor);
1270
  font: 700 var(--type-caption)/1 var(--mono);
1271
- text-transform: uppercase;
1272
  cursor: pointer;
1273
- transition: transform 150ms ease, box-shadow 150ms ease, background 150ms ease;
1274
  }
1275
 
1276
  .send-button:hover:not(:disabled) {
1277
- transform: translate(-2px, -2px);
1278
- box-shadow: 3px 3px 0 var(--ink);
1279
  }
1280
 
1281
  .send-button:disabled {
@@ -1304,14 +1361,121 @@ button:disabled {
1304
  background: var(--phosphor);
1305
  }
1306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307
  .inspector {
1308
  grid-area: inspector;
1309
- min-height: calc(100dvh - 76px);
1310
- height: calc(100dvh - 76px);
1311
  overflow-y: auto;
1312
  border-left: 1px solid var(--line-light);
1313
  scrollbar-color: rgba(232, 223, 202, 0.18) transparent;
1314
- animation: rise-in 570ms 220ms cubic-bezier(.16, 1, .3, 1) both;
1315
  }
1316
 
1317
  .inspector-section {
@@ -1322,6 +1486,7 @@ button:disabled {
1322
  .section-heading {
1323
  align-items: center;
1324
  margin-bottom: 17px;
 
1325
  }
1326
 
1327
  .section-heading.compact {
@@ -1333,22 +1498,21 @@ button:disabled {
1333
  }
1334
 
1335
  .section-heading h2 {
1336
- font-size: 24px;
1337
  }
1338
 
1339
  .sampling-label {
1340
- display: flex;
1341
- align-items: center;
1342
- gap: 6px;
1343
- color: rgba(232, 223, 202, 0.42);
1344
- font: var(--type-utility)/1 var(--mono);
1345
- text-transform: uppercase;
1346
- letter-spacing: 0.06em;
1347
  }
1348
 
1349
- .sampling-label i {
1350
- width: 4px;
1351
- height: 4px;
 
1352
  }
1353
 
1354
  .speed-gauge {
@@ -1363,21 +1527,15 @@ button:disabled {
1363
  height: 116px;
1364
  display: grid;
1365
  place-items: center;
1366
- border-radius: 49% 51% 47% 53%;
1367
  background:
1368
- radial-gradient(circle, var(--ink) 0 51%, transparent 52%),
1369
- conic-gradient(var(--phosphor) 0 var(--gauge), rgba(232, 223, 202, 0.11) var(--gauge) 100%);
1370
- transform: rotate(-9deg);
1371
- box-shadow: inset 0 0 0 1px rgba(232, 223, 202, 0.1), 0 0 30px rgba(185, 255, 69, 0.055);
1372
  }
1373
 
1374
  .gauge-ring::after {
1375
- position: absolute;
1376
- width: 128px;
1377
- height: 128px;
1378
- border: 1px dashed rgba(232, 223, 202, 0.13);
1379
- border-radius: 45% 55% 52% 48%;
1380
- content: "";
1381
  }
1382
 
1383
  .gauge-ring > div {
@@ -1385,21 +1543,30 @@ button:disabled {
1385
  z-index: 1;
1386
  display: grid;
1387
  justify-items: center;
1388
- transform: rotate(9deg);
1389
  }
1390
 
1391
  .gauge-ring strong {
1392
  color: var(--phosphor);
1393
- font: 400 29px/1 var(--display);
 
1394
  }
1395
 
1396
  .gauge-ring small {
 
 
 
1397
  margin-top: 3px;
1398
  color: var(--paper-ghost);
1399
- font: var(--type-utility)/1 var(--mono);
 
1400
  text-transform: uppercase;
1401
  }
1402
 
 
 
 
 
1403
  .speed-gauge dl {
1404
  margin: 0;
1405
  }
@@ -1421,6 +1588,65 @@ button:disabled {
1421
  color: var(--paper);
1422
  }
1423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1424
  .device-card {
1425
  display: grid;
1426
  grid-template-columns: auto 1fr auto;
@@ -1428,8 +1654,9 @@ button:disabled {
1428
  gap: 10px;
1429
  margin: 19px 0 14px;
1430
  padding: 10px;
1431
- border: 1px solid rgba(185, 255, 69, 0.23);
1432
- background: linear-gradient(90deg, rgba(185, 255, 69, 0.08), transparent);
 
1433
  }
1434
 
1435
  .device-icon {
@@ -1501,7 +1728,7 @@ button:disabled {
1501
  .meter-track > span {
1502
  display: block;
1503
  height: 100%;
1504
- background: repeating-linear-gradient(90deg, var(--phosphor) 0 5px, transparent 5px 7px);
1505
  }
1506
 
1507
  .memory-grid {
@@ -1586,15 +1813,15 @@ button:disabled {
1586
  width: 25px;
1587
  height: 25px;
1588
  border: 1px solid var(--line-light);
1589
- border-radius: 50%;
1590
  background: transparent;
1591
  cursor: pointer;
1592
  }
1593
 
1594
  .artifact-empty {
1595
  margin: 0;
1596
- color: rgba(232, 223, 202, 0.4);
1597
- font: var(--type-utility)/1.5 var(--mono);
1598
  }
1599
 
1600
  .artifact-pane {
@@ -1711,7 +1938,6 @@ button:disabled {
1711
  display: flex;
1712
  justify-content: flex-end;
1713
  background: rgba(8, 11, 9, 0.64);
1714
- backdrop-filter: blur(4px);
1715
  animation: station-in 160ms ease both;
1716
  }
1717
 
@@ -1722,9 +1948,7 @@ button:disabled {
1722
  padding: 30px 27px;
1723
  color: var(--paper);
1724
  border-left: 1px solid var(--phosphor);
1725
- background:
1726
- repeating-radial-gradient(ellipse at 104% 101%, transparent 0 13px, rgba(185, 255, 69, 0.055) 13.5px 14.5px),
1727
- var(--ink);
1728
  box-shadow: -24px 0 70px rgba(0, 0, 0, 0.32);
1729
  animation: sheet-in 300ms cubic-bezier(.16, 1, .3, 1) both;
1730
  }
@@ -1761,18 +1985,13 @@ button:disabled {
1761
 
1762
  .setting-group {
1763
  display: grid;
1764
- grid-template-columns: 24px 1fr auto;
1765
  align-items: center;
1766
  gap: 12px;
1767
  padding: 15px 0;
1768
  border-bottom: 1px solid var(--line-light);
1769
  }
1770
 
1771
- .setting-number {
1772
- color: var(--phosphor);
1773
- font: var(--type-utility)/1 var(--mono);
1774
- }
1775
-
1776
  .setting-group > div,
1777
  .range-setting > span:first-child {
1778
  display: grid;
@@ -2012,28 +2231,37 @@ button:disabled {
2012
 
2013
  @media (max-width: 1279px) {
2014
  .station-header {
2015
- grid-template-columns: minmax(320px, 1fr) auto auto;
2016
  }
2017
 
2018
- .local-promise {
2019
- min-width: auto;
2020
  }
2021
 
2022
- .local-promise small {
2023
- display: none;
2024
  }
2025
 
2026
  .workspace-grid {
2027
- grid-template-columns: 280px minmax(500px, 1fr);
2028
  grid-template-areas:
2029
  "models chat"
2030
  "inspector inspector";
2031
  }
2032
 
 
 
 
 
 
 
 
 
 
2033
  .model-rail,
2034
  .chat-surface {
2035
- min-height: calc(100dvh - 76px);
2036
- height: calc(100dvh - 76px);
2037
  }
2038
 
2039
  .inspector {
@@ -2042,7 +2270,7 @@ button:disabled {
2042
  display: grid;
2043
  grid-template-columns: repeat(2, minmax(0, 1fr));
2044
  overflow: visible;
2045
- border-top: 1px solid var(--phosphor);
2046
  border-left: 0;
2047
  }
2048
 
@@ -2054,36 +2282,37 @@ button:disabled {
2054
 
2055
  @media (max-width: 820px) {
2056
  .station-header {
2057
- display: block;
2058
- min-height: 70px;
 
2059
  }
2060
 
2061
  .brand-block {
2062
- padding: 11px 15px;
2063
- padding-right: 70px;
 
2064
  }
2065
 
2066
- .brand-mark {
2067
- width: 36px;
2068
- height: 36px;
2069
- flex-basis: 36px;
2070
  }
2071
 
2072
- .local-promise {
2073
- min-height: 31px;
2074
- padding: 0 16px;
2075
- border: 0;
 
2076
  border-top: 1px solid var(--line-light);
2077
  }
2078
 
2079
  .header-actions {
2080
- position: absolute;
2081
- top: 10px;
2082
- right: 0;
2083
- }
2084
-
2085
- .station-clock {
2086
- display: none;
2087
  }
2088
 
2089
  .workspace-grid {
@@ -2101,7 +2330,7 @@ button:disabled {
2101
  display: block;
2102
  padding: 18px 15px;
2103
  border-right: 0;
2104
- border-bottom: 1px solid var(--phosphor);
2105
  }
2106
 
2107
  .model-list {
@@ -2123,7 +2352,9 @@ button:disabled {
2123
 
2124
  .chat-surface {
2125
  display: grid;
2126
- grid-template-rows: auto 19px minmax(240px, auto) auto;
 
 
2127
  overflow: visible;
2128
  }
2129
 
@@ -2156,7 +2387,7 @@ button:disabled {
2156
 
2157
  .inspector {
2158
  display: block;
2159
- border-top: 1px solid var(--phosphor);
2160
  }
2161
 
2162
  .inspector-section {
@@ -2167,12 +2398,21 @@ button:disabled {
2167
 
2168
  @media (max-width: 520px) {
2169
  .brand-block h1 {
2170
- font-size: 24px;
 
 
 
 
 
 
 
 
2171
  }
2172
 
2173
- .brand-block .eyebrow,
2174
- .local-promise strong {
2175
- font-size: var(--type-utility);
 
2176
  }
2177
 
2178
  .chat-header {
@@ -2180,8 +2420,7 @@ button:disabled {
2180
  }
2181
 
2182
  .botanical-subtitle {
2183
- display: block;
2184
- margin: 4px 0 0;
2185
  }
2186
 
2187
  .message-user {
@@ -2193,38 +2432,11 @@ button:disabled {
2193
  }
2194
 
2195
  .composer-activity {
2196
- height: 176px;
2197
- grid-template-rows: 24px 30px 44px 36px;
2198
- padding-inline: 9px;
2199
- }
2200
-
2201
- .activity-stage-line {
2202
- gap: 3px;
2203
- }
2204
-
2205
- .activity-stage-line span {
2206
- gap: 3px;
2207
- letter-spacing: 0;
2208
- }
2209
-
2210
- .activity-stage-line i {
2211
- display: none;
2212
- }
2213
-
2214
- .load-live-grid {
2215
- gap: 7px;
2216
- }
2217
-
2218
- .activity-memory {
2219
- min-width: 68px;
2220
- }
2221
-
2222
- .shard-strip {
2223
- gap: 3px;
2224
  }
2225
 
2226
- .shard-cell small {
2227
- display: none;
2228
  }
2229
 
2230
  .composer-field {
@@ -2249,8 +2461,11 @@ button:disabled {
2249
  }
2250
 
2251
  .gauge-ring::after {
2252
- width: 113px;
2253
- height: 113px;
 
 
 
2254
  }
2255
  }
2256
 
 
1
  :root {
2
  color-scheme: dark;
3
+ --ink: #141613;
4
+ --ink-raised: #1b1e1a;
5
+ --ink-soft: #252921;
6
+ --paper: #f1f2ed;
7
+ --paper-deep: #e4e6df;
8
+ --paper-ghost: rgba(241, 242, 237, 0.64);
9
+ --phosphor: #a8d574;
10
+ --phosphor-dim: rgba(168, 213, 116, 0.14);
11
+ --moss: #738066;
12
+ --rust: #c27a56;
13
+ --line-light: rgba(241, 242, 237, 0.13);
14
+ --line-dark: rgba(20, 22, 19, 0.15);
15
+ --display: "Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;
16
+ --body: "Avenir Next", "Helvetica Neue", ui-sans-serif, sans-serif;
17
  --mono: "Berkeley Mono", Iosevka, "SFMono-Regular", Consolas, monospace;
18
+ --type-utility: clamp(0.75rem, 0.12vw + 0.72rem, 0.8125rem);
19
+ --type-caption: clamp(0.8125rem, 0.15vw + 0.77rem, 0.9375rem);
20
+ --type-body: clamp(1.0625rem, 0.18vw + 1.02rem, 1.1875rem);
21
+ --type-heading: clamp(1.55rem, 0.85vw + 1.35rem, 2.15rem);
22
  font-family: var(--body);
23
  font-synthesis: none;
24
  text-rendering: optimizeLegibility;
 
36
  margin: 0;
37
  min-width: 320px;
38
  min-height: 100vh;
39
+ background: var(--ink);
 
 
 
40
  color: var(--paper);
41
  font-size: var(--type-body);
42
  }
 
69
  }
70
 
71
  .station-frame {
72
+ --canvas-bg: #f1f2ed;
73
+ --canvas-raised: #e4e6df;
74
+ --canvas-field: #f8f9f5;
75
+ --canvas-text: #141613;
76
+ --canvas-muted: rgba(20, 22, 19, 0.52);
77
+ --canvas-line: rgba(20, 22, 19, 0.15);
78
  min-height: 100dvh;
79
+ color-scheme: light;
80
  isolation: isolate;
81
  animation: station-in 580ms cubic-bezier(.16, 1, .3, 1) both;
82
  }
83
 
84
+ .station-frame[data-theme="dark"] {
85
+ --canvas-bg: #1a1c18;
86
+ --canvas-raised: #20231f;
87
+ --canvas-field: #252823;
88
+ --canvas-text: #edf0e8;
89
+ --canvas-muted: rgba(237, 240, 232, 0.58);
90
+ --canvas-line: rgba(237, 240, 232, 0.13);
91
+ color-scheme: dark;
92
+ }
93
+
94
  .station-frame::before {
95
+ display: none;
 
 
 
 
 
 
 
96
  }
97
 
98
  .station-header {
99
+ min-height: 64px;
100
  display: grid;
101
+ grid-template-columns: 260px minmax(320px, 1fr) auto;
102
  align-items: stretch;
103
  background: var(--ink);
104
+ border-bottom: 1px solid var(--line-light);
 
105
  position: relative;
106
  z-index: 10;
107
  }
108
 
109
  .brand-block {
110
  display: flex;
111
+ gap: 10px;
112
  align-items: center;
113
+ padding: 10px 18px;
114
+ border-right: 1px solid var(--line-light);
115
  }
116
 
117
  .brand-block h1 {
118
  margin: 0;
119
+ font: 650 20px/1.1 var(--display);
120
+ letter-spacing: -0.02em;
121
  }
122
 
123
+ .brand-block > div {
124
+ display: grid;
125
+ gap: 2px;
126
  }
127
 
128
+ .brand-block > div > span {
129
+ color: var(--paper-ghost);
130
+ font-size: var(--type-caption);
131
+ }
132
+
133
+ .brand-glyph {
134
+ width: 34px;
135
+ height: 34px;
136
+ flex: 0 0 34px;
137
  display: grid;
138
  place-items: center;
139
+ color: var(--phosphor);
140
+ border: 1px solid rgba(168, 213, 116, 0.42);
141
+ border-radius: 8px;
142
+ font: 650 16px/1 var(--mono);
143
  }
144
 
145
  .brand-mark::before,
 
185
  color: rgba(17, 21, 18, 0.52);
186
  }
187
 
188
+ .browser-notice {
189
  display: flex;
190
  align-items: center;
191
+ gap: 10px;
192
+ min-width: 0;
193
+ padding: 0 20px;
 
 
194
  }
195
 
196
+ .notice-mark {
197
+ width: 6px;
198
+ height: 6px;
199
+ flex: 0 0 6px;
200
+ border-radius: 2px;
 
 
201
  background: var(--phosphor);
 
202
  }
203
 
204
+ .browser-notice > span:last-child {
205
  display: grid;
206
+ gap: 2px;
207
  }
208
 
209
+ .browser-notice strong {
210
+ color: var(--paper);
211
+ font: 600 var(--type-caption)/1.2 var(--body);
 
 
212
  }
213
 
214
+ .browser-notice > span > span {
215
  color: var(--paper-ghost);
216
  font: var(--type-caption)/1.25 var(--body);
217
  }
218
 
219
+ .browser-notice.is-warning .notice-mark {
220
+ background: var(--rust);
221
+ }
222
+
223
+ .browser-notice.is-warning strong {
224
+ color: #efb08f;
225
+ }
226
+
227
  .header-actions {
228
  justify-self: end;
229
  display: flex;
230
  align-items: center;
231
+ gap: 10px;
232
+ padding: 0 14px;
233
+ border-left: 1px solid var(--line-light);
234
+ }
235
+
236
+ .layout-controls {
237
+ display: flex;
238
+ gap: 4px;
239
+ }
240
+
241
+ .layout-controls button,
242
+ .icon-button {
243
+ width: 36px;
244
+ height: 36px;
245
+ display: grid;
246
+ place-items: center;
247
+ color: var(--paper-ghost);
248
+ border: 1px solid var(--line-light);
249
+ border-radius: 7px;
250
+ background: transparent;
251
+ cursor: pointer;
252
+ transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
253
+ }
254
+
255
+ .layout-controls button:hover,
256
+ .icon-button:hover {
257
+ color: var(--paper);
258
+ border-color: rgba(241, 242, 237, 0.3);
259
+ background: rgba(241, 242, 237, 0.05);
260
+ }
261
+
262
+ .layout-controls button[aria-pressed="true"] {
263
+ color: var(--phosphor);
264
+ border-color: rgba(168, 213, 116, 0.34);
265
+ background: var(--phosphor-dim);
266
+ }
267
+
268
+ .layout-controls svg,
269
+ .icon-button svg {
270
+ width: 18px;
271
+ fill: none;
272
+ stroke: currentColor;
273
+ stroke-width: 1.4;
274
  }
275
 
276
  .station-clock {
 
293
  }
294
 
295
  .icon-button {
296
+ flex: 0 0 36px;
 
 
 
 
 
 
 
 
297
  }
298
 
299
  .icon-button:hover {
300
+ color: var(--paper);
 
 
301
  }
302
 
303
  .icon-button svg {
 
308
  }
309
 
310
  .workspace-grid {
311
+ min-height: calc(100dvh - 64px);
312
  display: grid;
313
+ grid-template-columns: 260px minmax(0, 1fr) 340px;
314
  grid-template-areas: "models chat inspector";
315
+ background: #10120f;
316
+ }
317
+
318
+ .workspace-grid.is-models-collapsed {
319
+ grid-template-columns: 0 minmax(0, 1fr) 340px;
320
+ }
321
+
322
+ .workspace-grid.is-inspector-collapsed {
323
+ grid-template-columns: 260px minmax(0, 1fr) 0;
324
+ }
325
+
326
+ .workspace-grid.is-models-collapsed.is-inspector-collapsed {
327
+ grid-template-columns: 0 minmax(0, 1fr) 0;
328
+ }
329
+
330
+ .workspace-grid.layout-centered .chat-surface {
331
+ width: min(100%, 1120px);
332
+ justify-self: center;
333
+ }
334
+
335
+ .workspace-grid.layout-wide .chat-surface {
336
+ width: 100%;
337
+ }
338
+
339
+ .model-rail[hidden],
340
+ .inspector[hidden] {
341
+ display: none !important;
342
  }
343
 
344
  .model-rail,
 
354
  display: flex;
355
  flex-direction: column;
356
  border-right: 1px solid var(--line-light);
357
+ padding: 20px 14px 14px;
 
358
  }
359
 
360
  .model-rail::before {
361
+ display: none;
 
 
 
 
 
 
 
 
 
 
362
  }
363
 
364
  .rail-heading,
 
366
  position: relative;
367
  z-index: 1;
368
  display: flex;
369
+ align-items: center;
370
+ gap: 10px;
371
  }
372
 
373
  .rail-heading > span,
 
381
  .rail-heading h2,
382
  .section-heading h2 {
383
  margin: 0;
384
+ font: 600 20px/1.2 var(--display);
385
+ letter-spacing: -0.015em;
386
+ }
387
+
388
+ .rail-heading p {
389
+ margin: 4px 0 0;
390
+ color: var(--paper-ghost);
391
+ font-size: var(--type-caption);
392
+ line-height: 1.35;
393
  }
394
 
395
  .model-list {
 
401
  }
402
 
403
  .model-card {
404
+ border: 1px solid var(--line-light);
405
+ border-radius: 8px;
406
  transition: border-color 180ms ease, background 180ms ease;
407
  }
408
 
409
  .model-card.is-active {
410
+ border-color: rgba(168, 213, 116, 0.58);
411
  background: var(--phosphor-dim);
 
412
  }
413
 
414
  .model-select {
415
  width: 100%;
416
  display: grid;
417
+ grid-template-columns: 1fr auto;
418
  gap: 8px;
419
+ min-height: 72px;
420
  padding: 13px 9px 12px;
421
  text-align: left;
422
  color: var(--paper);
 
426
  }
427
 
428
  .model-card:not(.is-active) .model-select:hover {
429
+ background: rgba(241, 242, 237, 0.045);
430
  }
431
 
432
  .model-index {
 
449
  }
450
 
451
  .model-title-line strong {
452
+ font: 600 20px/1 var(--display);
453
  }
454
 
455
  .model-copy em {
 
469
  .availability {
470
  padding: 2px 4px;
471
  font: 600 var(--type-utility)/1 var(--mono);
472
+ letter-spacing: 0;
 
473
  border: 1px solid var(--line-light);
474
  }
475
 
 
495
 
496
  .model-limit-reason {
497
  display: block;
498
+ margin: -4px 8px 9px;
499
  color: rgba(232, 223, 202, 0.58);
500
  font: var(--type-utility)/1.35 var(--mono);
501
  }
 
515
  .chat-surface {
516
  grid-area: chat;
517
  min-width: 0;
518
+ min-height: calc(100dvh - 64px);
519
+ height: calc(100dvh - 64px);
520
  display: grid;
521
+ grid-template-rows: auto minmax(0, 1fr) auto;
522
  overflow: hidden;
523
+ color: var(--canvas-text);
524
+ background: var(--canvas-bg);
 
 
 
 
525
  position: relative;
526
  }
527
 
 
530
  align-items: center;
531
  justify-content: space-between;
532
  gap: 20px;
533
+ min-height: 72px;
534
+ padding: 14px clamp(20px, 3vw, 40px);
535
+ border-bottom: 1px solid var(--canvas-line);
536
  }
537
 
538
  .chat-header h2 {
539
  display: inline;
540
  margin: 0;
541
+ font: 650 var(--type-heading)/1.1 var(--display);
542
+ letter-spacing: -0.025em;
543
+ }
544
+
545
+ .chat-header-main {
546
+ min-width: 0;
547
+ display: flex;
548
+ align-items: center;
549
+ gap: 8px;
550
+ }
551
+
552
+ .chat-list-button,
553
+ .chat-settings-button {
554
+ min-height: 36px;
555
+ display: inline-flex;
556
+ align-items: center;
557
+ gap: 5px;
558
+ padding: 0 9px;
559
+ color: var(--canvas-muted);
560
+ border: 1px solid var(--canvas-line);
561
+ border-radius: 7px;
562
+ background: transparent;
563
+ font: 600 var(--type-utility)/1 var(--body);
564
+ white-space: nowrap;
565
+ cursor: pointer;
566
+ }
567
+
568
+ .chat-list-button:hover,
569
+ .chat-settings-button:hover {
570
+ color: var(--canvas-text);
571
+ border-color: var(--canvas-muted);
572
+ }
573
+
574
+ .chat-list-button svg {
575
+ width: 16px;
576
+ fill: none;
577
+ stroke: currentColor;
578
+ stroke-width: 1.5;
579
+ }
580
+
581
+ .chat-title {
582
+ min-width: 0;
583
+ display: flex;
584
+ align-items: baseline;
585
+ gap: 10px;
586
  }
587
 
588
  .botanical-subtitle {
 
589
  color: rgba(17, 21, 18, 0.46);
590
+ font: var(--type-caption)/1 var(--body);
591
  }
592
 
593
  .chat-header-meta {
 
596
  gap: 5px 13px;
597
  color: rgba(17, 21, 18, 0.55);
598
  font: var(--type-utility)/1.2 var(--mono);
599
+ letter-spacing: 0;
 
600
  }
601
 
602
  .live-label {
 
605
  display: flex;
606
  align-items: center;
607
  gap: 7px;
608
+ color: #46622f;
609
  }
610
 
611
  .live-label i {
612
  width: 5px;
613
  height: 5px;
614
+ box-shadow: none;
615
+ background: #668a45;
616
  }
617
 
618
  .live-label.is-dormant {
 
621
 
622
  .live-label.is-dormant i {
623
  background: rgba(17, 21, 18, 0.36);
624
+ box-shadow: none;
625
  }
626
 
627
  .runtime-error {
 
703
  margin-bottom: 22px;
704
  color: rgba(17, 21, 18, 0.45);
705
  font: var(--type-utility)/1 var(--mono);
706
+ letter-spacing: 0;
 
707
  }
708
 
709
  .transcript-date::before,
710
  .transcript-date::after {
711
+ display: none;
 
 
 
712
  }
713
 
714
  .transcript-empty {
 
718
  color: rgba(17, 21, 18, 0.5);
719
  }
720
 
 
 
 
 
 
 
721
  .transcript-empty h3 {
722
+ margin: 0 0 8px;
723
  color: var(--ink);
724
+ font: 650 22px/1.2 var(--display);
725
  }
726
 
727
  .transcript-empty p {
 
730
  line-height: 1.5;
731
  }
732
 
733
+ .system-instructions-button,
734
+ .conversation-list-empty button,
735
+ .new-chat-button {
736
+ min-height: 42px;
737
+ padding: 0 13px;
738
+ color: var(--ink);
739
+ border: 1px solid var(--ink);
740
+ border-radius: 7px;
741
+ background: var(--phosphor);
742
+ font: 650 var(--type-caption)/1 var(--body);
743
+ cursor: pointer;
744
+ }
745
+
746
+ .system-instructions-button {
747
+ margin-top: 18px;
748
+ }
749
+
750
+ .conversation-library {
751
+ grid-template-rows: auto minmax(0, 1fr);
752
+ }
753
+
754
+ .conversation-list-header > div {
755
+ display: grid;
756
+ gap: 4px;
757
+ }
758
+
759
+ .conversation-list-header p {
760
+ margin: 0;
761
+ color: var(--canvas-muted);
762
+ font-size: var(--type-caption);
763
+ }
764
+
765
+ .conversation-list {
766
+ min-height: 0;
767
+ overflow-y: auto;
768
+ padding: clamp(20px, 4vw, 48px);
769
+ scrollbar-color: var(--canvas-muted) transparent;
770
+ }
771
+
772
+ .conversation-row {
773
+ max-width: 850px;
774
+ display: grid;
775
+ grid-template-columns: minmax(0, 1fr) auto;
776
+ align-items: stretch;
777
+ margin: 0 auto 9px;
778
+ border: 1px solid var(--canvas-line);
779
+ border-radius: 9px;
780
+ background: var(--canvas-field);
781
+ }
782
+
783
+ .conversation-row.is-current {
784
+ border-color: rgba(115, 128, 102, 0.72);
785
+ }
786
+
787
+ .conversation-open {
788
+ min-width: 0;
789
+ display: grid;
790
+ gap: 6px;
791
+ padding: 15px 16px;
792
+ color: var(--canvas-text);
793
+ border: 0;
794
+ background: transparent;
795
+ text-align: left;
796
+ cursor: pointer;
797
+ }
798
+
799
+ .conversation-title-line {
800
+ min-width: 0;
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 9px;
804
+ }
805
+
806
+ .conversation-title-line strong {
807
+ overflow: hidden;
808
+ font: 650 var(--type-body)/1.2 var(--display);
809
+ text-overflow: ellipsis;
810
+ white-space: nowrap;
811
+ }
812
+
813
+ .conversation-title-line small {
814
+ flex: 0 0 auto;
815
+ color: #52712f;
816
+ font: 600 var(--type-utility)/1 var(--body);
817
+ }
818
+
819
+ .conversation-open > span:nth-child(2) {
820
+ overflow: hidden;
821
+ color: var(--canvas-muted);
822
+ font-size: var(--type-caption);
823
+ line-height: 1.35;
824
+ text-overflow: ellipsis;
825
+ white-space: nowrap;
826
+ }
827
+
828
+ .conversation-open > small {
829
+ color: var(--canvas-muted);
830
+ font: var(--type-utility)/1.2 var(--mono);
831
+ }
832
+
833
+ .conversation-actions {
834
+ display: grid;
835
+ align-content: center;
836
+ gap: 4px;
837
+ padding: 7px;
838
+ border-left: 1px solid var(--canvas-line);
839
+ }
840
+
841
+ .conversation-actions button {
842
+ min-height: 34px;
843
+ padding: 0 8px;
844
+ color: var(--canvas-muted);
845
+ border: 0;
846
+ background: transparent;
847
+ font: 600 var(--type-utility)/1 var(--body);
848
+ cursor: pointer;
849
+ }
850
+
851
+ .conversation-actions button:hover {
852
+ color: var(--canvas-text);
853
+ }
854
+
855
+ .conversation-actions .conversation-delete {
856
+ color: var(--rust);
857
+ }
858
+
859
+ .conversation-list-empty {
860
+ max-width: 430px;
861
+ display: grid;
862
+ justify-items: center;
863
+ gap: 10px;
864
+ margin: 15vh auto 0;
865
+ color: var(--canvas-muted);
866
+ text-align: center;
867
+ }
868
+
869
+ .conversation-list-empty h3,
870
+ .conversation-list-empty p {
871
+ margin: 0;
872
+ }
873
+
874
+ .conversation-list-empty h3 {
875
+ color: var(--canvas-text);
876
+ font: 650 22px/1.2 var(--display);
877
+ }
878
+
879
+ .conversation-list-empty button {
880
+ margin-top: 8px;
881
+ }
882
+
883
  .message {
884
+ max-width: 760px;
885
  margin-bottom: 25px;
 
886
  }
887
 
888
  .message > header {
 
892
  margin-bottom: 8px;
893
  color: rgba(17, 21, 18, 0.47);
894
  font: var(--type-utility)/1.2 var(--mono);
895
+ letter-spacing: 0;
 
896
  }
897
 
898
  .message > header strong {
 
989
  margin: 0 0 11px;
990
  padding: 8px 10px;
991
  color: rgba(17, 21, 18, 0.62);
992
+ border: 1px solid rgba(17, 21, 18, 0.16);
993
+ border-radius: 8px;
994
+ background: rgba(17, 21, 18, 0.03);
995
  font-size: var(--type-caption);
996
  }
997
 
998
  .reasoning-block summary {
999
  color: #4d6728;
1000
+ font: 600 var(--type-caption)/1.2 var(--body);
 
 
1001
  cursor: pointer;
1002
  }
1003
 
 
1023
 
1024
  .message-assistant {
1025
  padding-left: 22px;
1026
+ border-left: 2px solid rgba(84, 111, 60, 0.45);
1027
  }
1028
 
1029
  .message-assistant .message-content > p::first-letter {
1030
+ float: none;
1031
+ margin: 0;
1032
+ color: inherit;
1033
+ font: inherit;
1034
  }
1035
 
1036
  .message-user {
 
1039
  padding: 12px 15px 13px;
1040
  color: var(--paper);
1041
  background: var(--ink);
1042
+ border-radius: 10px;
 
1043
  }
1044
 
1045
  .message-user > header,
 
1057
  margin-top: 9px;
1058
  padding-top: 7px;
1059
  color: rgba(17, 21, 18, 0.43);
1060
+ border-top: 1px solid rgba(17, 21, 18, 0.13);
1061
  font: var(--type-utility)/1.2 var(--mono);
1062
  }
1063
 
 
1125
  background: var(--rust);
1126
  }
1127
 
1128
+ .tool-call > div {
1129
+ padding: 9px 10px;
1130
+ border-top: 1px solid rgba(17, 21, 18, 0.13);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1131
  }
1132
 
1133
+ .tool-call > div p {
1134
+ margin: 5px 0 0;
1135
+ color: rgba(17, 21, 18, 0.58);
1136
+ font-size: var(--type-caption);
1137
  }
1138
 
1139
+ .composer {
1140
+ position: relative;
1141
+ padding: 12px clamp(21px, 3vw, 44px) 18px;
1142
+ border-top: 1px solid var(--canvas-line);
1143
+ background: var(--canvas-raised);
1144
  }
1145
 
1146
+ .composer::before {
1147
+ display: none;
 
 
 
1148
  }
1149
 
1150
+ .composer-activity {
 
1151
  display: grid;
1152
+ gap: 10px;
1153
+ margin-bottom: 12px;
1154
+ padding: 12px 14px;
1155
+ color: var(--paper);
1156
+ border: 1px solid rgba(20, 22, 19, 0.9);
1157
+ border-radius: 8px;
1158
+ background: var(--ink);
1159
  }
1160
 
1161
+ .activity-status,
1162
+ .activity-status > div {
1163
  display: flex;
1164
  align-items: center;
 
1165
  }
1166
 
1167
+ .activity-status {
1168
+ min-width: 0;
1169
+ justify-content: space-between;
1170
+ gap: 12px;
1171
+ font: var(--type-caption)/1.2 var(--body);
1172
  }
1173
 
1174
+ .activity-status > div {
1175
  min-width: 0;
1176
+ gap: 10px;
 
 
1177
  }
1178
 
1179
+ .activity-status > div > span:last-child {
1180
+ min-width: 0;
1181
+ display: grid;
1182
+ gap: 3px;
 
1183
  }
1184
 
1185
+ .activity-status strong,
1186
+ .activity-status small {
1187
+ overflow: hidden;
1188
+ text-overflow: ellipsis;
1189
+ white-space: nowrap;
 
1190
  }
1191
 
1192
+ .activity-status strong {
1193
  color: var(--paper);
1194
  font-weight: 600;
1195
  }
1196
 
1197
+ .activity-status small {
1198
+ color: var(--paper-ghost);
1199
+ font-size: var(--type-utility);
1200
+ }
1201
+
1202
+ .activity-status > b {
1203
+ flex: 0 0 auto;
1204
+ color: var(--phosphor);
1205
+ font: 600 var(--type-caption)/1 var(--mono);
1206
+ }
1207
+
1208
+ .activity-pulse {
1209
+ width: 5px;
1210
+ height: 22px;
1211
+ flex: 0 0 5px;
1212
+ border-radius: 2px;
1213
+ background: var(--phosphor);
1214
+ }
1215
+
1216
+ .activity-track {
1217
+ height: 6px;
1218
  overflow: hidden;
1219
+ border-radius: 3px;
1220
+ background: rgba(241, 242, 237, 0.12);
1221
+ }
1222
+
1223
+ .activity-track > span {
1224
+ display: block;
1225
+ height: 100%;
1226
+ border-radius: inherit;
1227
+ background: var(--phosphor);
1228
+ transition: width 120ms linear;
1229
  }
1230
 
1231
  .composer-meta {
 
1249
  border: 0;
1250
  background: transparent;
1251
  font: 700 var(--type-utility)/1 var(--mono);
 
1252
  cursor: pointer;
1253
  }
1254
 
 
1264
  border: 0;
1265
  background: transparent;
1266
  font: 700 var(--type-utility)/1 var(--mono);
 
1267
  cursor: pointer;
1268
  }
1269
 
 
1284
  grid-template-columns: 1fr auto;
1285
  position: relative;
1286
  min-height: 84px;
1287
+ border: 1px solid var(--canvas-text);
1288
+ border-radius: 10px;
1289
+ background: var(--canvas-field);
1290
+ overflow: hidden;
1291
  }
1292
 
1293
  .composer-field label {
 
1296
  left: 11px;
1297
  color: rgba(17, 21, 18, 0.45);
1298
  font: var(--type-utility)/1 var(--mono);
1299
+ letter-spacing: 0;
 
1300
  }
1301
 
1302
  .composer-field textarea {
1303
  min-width: 0;
1304
  resize: none;
1305
  padding: 23px 13px 9px;
1306
+ color: var(--canvas-text);
1307
  border: 0;
1308
  outline: none;
1309
  background: transparent;
 
1324
  margin: 7px;
1325
  color: var(--ink);
1326
  border: 1px solid var(--ink);
1327
+ border-radius: 8px;
1328
  background: var(--phosphor);
1329
  font: 700 var(--type-caption)/1 var(--mono);
 
1330
  cursor: pointer;
1331
+ transition: background 150ms ease, opacity 150ms ease;
1332
  }
1333
 
1334
  .send-button:hover:not(:disabled) {
1335
+ background: #b6df87;
 
1336
  }
1337
 
1338
  .send-button:disabled {
 
1361
  background: var(--phosphor);
1362
  }
1363
 
1364
+ .station-frame[data-theme="dark"] .botanical-subtitle,
1365
+ .station-frame[data-theme="dark"] .chat-header-meta,
1366
+ .station-frame[data-theme="dark"] .transcript-date,
1367
+ .station-frame[data-theme="dark"] .transcript-empty,
1368
+ .station-frame[data-theme="dark"] .message > header,
1369
+ .station-frame[data-theme="dark"] .message > footer,
1370
+ .station-frame[data-theme="dark"] .composer-meta,
1371
+ .station-frame[data-theme="dark"] .tool-toggle,
1372
+ .station-frame[data-theme="dark"] .regenerate-button,
1373
+ .station-frame[data-theme="dark"] .composer-field label {
1374
+ color: var(--canvas-muted);
1375
+ }
1376
+
1377
+ .station-frame[data-theme="dark"] .transcript-empty h3,
1378
+ .station-frame[data-theme="dark"] .message > header strong {
1379
+ color: var(--canvas-text);
1380
+ }
1381
+
1382
+ .station-frame[data-theme="dark"] .live-label {
1383
+ color: #a8d574;
1384
+ }
1385
+
1386
+ .station-frame[data-theme="dark"] .conversation-title-line small {
1387
+ color: var(--phosphor);
1388
+ }
1389
+
1390
+ .station-frame[data-theme="dark"] .live-label.is-dormant {
1391
+ color: var(--canvas-muted);
1392
+ }
1393
+
1394
+ .station-frame[data-theme="dark"] .live-label.is-dormant i {
1395
+ background: var(--canvas-muted);
1396
+ }
1397
+
1398
+ .station-frame[data-theme="dark"] .transcript {
1399
+ scrollbar-color: rgba(237, 240, 232, 0.24) transparent;
1400
+ }
1401
+
1402
+ .station-frame[data-theme="dark"] .markdown-content blockquote,
1403
+ .station-frame[data-theme="dark"] .reasoning-block,
1404
+ .station-frame[data-theme="dark"] .tool-call > div p {
1405
+ color: rgba(237, 240, 232, 0.7);
1406
+ }
1407
+
1408
+ .station-frame[data-theme="dark"] .markdown-content code {
1409
+ background: rgba(237, 240, 232, 0.09);
1410
+ }
1411
+
1412
+ .station-frame[data-theme="dark"] .markdown-content pre {
1413
+ color: var(--paper);
1414
+ background: #0f110e;
1415
+ }
1416
+
1417
+ .station-frame[data-theme="dark"] .markdown-content a,
1418
+ .station-frame[data-theme="dark"] .reasoning-block summary {
1419
+ color: var(--phosphor);
1420
+ }
1421
+
1422
+ .station-frame[data-theme="dark"] .reasoning-block,
1423
+ .station-frame[data-theme="dark"] .tool-call {
1424
+ border-color: var(--canvas-line);
1425
+ background: rgba(237, 240, 232, 0.035);
1426
+ }
1427
+
1428
+ .station-frame[data-theme="dark"] .generation-placeholder,
1429
+ .station-frame[data-theme="dark"] .tool-call summary > span:last-child,
1430
+ .station-frame[data-theme="dark"] .composer-field textarea::placeholder {
1431
+ color: rgba(237, 240, 232, 0.4);
1432
+ }
1433
+
1434
+ .station-frame[data-theme="dark"] .message-assistant {
1435
+ border-left-color: rgba(168, 213, 116, 0.44);
1436
+ }
1437
+
1438
+ .station-frame[data-theme="dark"] .message-user {
1439
+ color: var(--canvas-text);
1440
+ border: 1px solid var(--canvas-line);
1441
+ background: #11130f;
1442
+ }
1443
+
1444
+ .station-frame[data-theme="dark"] .message > footer,
1445
+ .station-frame[data-theme="dark"] .tool-call > div {
1446
+ border-color: var(--canvas-line);
1447
+ }
1448
+
1449
+ .station-frame[data-theme="dark"] .tool-toggle.is-on {
1450
+ color: var(--phosphor);
1451
+ }
1452
+
1453
+ .station-frame[data-theme="dark"] .composer-field textarea {
1454
+ color: var(--canvas-text);
1455
+ }
1456
+
1457
+ .station-frame[data-theme="dark"] .composer-field {
1458
+ border-color: rgba(237, 240, 232, 0.3);
1459
+ }
1460
+
1461
+ .station-frame[data-theme="dark"] .send-button {
1462
+ color: var(--ink);
1463
+ border-color: #0f110e;
1464
+ }
1465
+
1466
+ .station-frame[data-theme="dark"] .send-button.stop {
1467
+ color: var(--canvas-text);
1468
+ border-color: var(--canvas-line);
1469
+ background: #11130f;
1470
+ }
1471
+
1472
  .inspector {
1473
  grid-area: inspector;
1474
+ min-height: calc(100dvh - 64px);
1475
+ height: calc(100dvh - 64px);
1476
  overflow-y: auto;
1477
  border-left: 1px solid var(--line-light);
1478
  scrollbar-color: rgba(232, 223, 202, 0.18) transparent;
 
1479
  }
1480
 
1481
  .inspector-section {
 
1486
  .section-heading {
1487
  align-items: center;
1488
  margin-bottom: 17px;
1489
+ justify-content: space-between;
1490
  }
1491
 
1492
  .section-heading.compact {
 
1498
  }
1499
 
1500
  .section-heading h2 {
1501
+ font-size: 20px;
1502
  }
1503
 
1504
  .sampling-label {
1505
+ padding: 3px 6px;
1506
+ color: var(--paper-ghost);
1507
+ border: 1px solid var(--line-light);
1508
+ border-radius: 6px;
1509
+ font: var(--type-utility)/1 var(--body);
 
 
1510
  }
1511
 
1512
+ .sampling-label.phase-prefill,
1513
+ .sampling-label.phase-decode {
1514
+ color: var(--phosphor);
1515
+ border-color: rgba(168, 213, 116, 0.35);
1516
  }
1517
 
1518
  .speed-gauge {
 
1527
  height: 116px;
1528
  display: grid;
1529
  place-items: center;
1530
+ border-radius: 50%;
1531
  background:
1532
+ radial-gradient(circle, var(--ink) 0 56%, transparent 57%),
1533
+ conic-gradient(var(--phosphor) 0 var(--gauge), rgba(241, 242, 237, 0.11) var(--gauge) 100%);
1534
+ box-shadow: inset 0 0 0 1px rgba(241, 242, 237, 0.08);
 
1535
  }
1536
 
1537
  .gauge-ring::after {
1538
+ display: none;
 
 
 
 
 
1539
  }
1540
 
1541
  .gauge-ring > div {
 
1543
  z-index: 1;
1544
  display: grid;
1545
  justify-items: center;
1546
+ min-width: 0;
1547
  }
1548
 
1549
  .gauge-ring strong {
1550
  color: var(--phosphor);
1551
+ font: 600 clamp(24px, 2vw, 29px)/1 var(--display);
1552
+ white-space: nowrap;
1553
  }
1554
 
1555
  .gauge-ring small {
1556
+ display: grid;
1557
+ justify-items: center;
1558
+ gap: 2px;
1559
  margin-top: 3px;
1560
  color: var(--paper-ghost);
1561
+ font: 600 10px/1 var(--mono);
1562
+ letter-spacing: 0.02em;
1563
  text-transform: uppercase;
1564
  }
1565
 
1566
+ .gauge-ring small span {
1567
+ white-space: nowrap;
1568
+ }
1569
+
1570
  .speed-gauge dl {
1571
  margin: 0;
1572
  }
 
1588
  color: var(--paper);
1589
  }
1590
 
1591
+ .telemetry-live-grid {
1592
+ display: grid;
1593
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1594
+ gap: 8px;
1595
+ margin-top: 16px;
1596
+ }
1597
+
1598
+ .telemetry-metric {
1599
+ min-width: 0;
1600
+ display: grid;
1601
+ gap: 6px;
1602
+ padding: 10px;
1603
+ border: 1px solid var(--line-light);
1604
+ border-radius: 8px;
1605
+ background: var(--ink-raised);
1606
+ }
1607
+
1608
+ .telemetry-metric.is-active {
1609
+ border-color: rgba(168, 213, 116, 0.4);
1610
+ }
1611
+
1612
+ .telemetry-metric > div:first-child {
1613
+ display: flex;
1614
+ align-items: baseline;
1615
+ justify-content: space-between;
1616
+ gap: 8px;
1617
+ }
1618
+
1619
+ .telemetry-metric span,
1620
+ .telemetry-metric small {
1621
+ color: var(--paper-ghost);
1622
+ font-size: var(--type-utility);
1623
+ }
1624
+
1625
+ .telemetry-metric strong {
1626
+ color: var(--paper);
1627
+ font: 600 var(--type-caption)/1 var(--mono);
1628
+ white-space: nowrap;
1629
+ }
1630
+
1631
+ .telemetry-metric.is-active strong {
1632
+ color: var(--phosphor);
1633
+ }
1634
+
1635
+ .telemetry-meter {
1636
+ height: 4px;
1637
+ overflow: hidden;
1638
+ border-radius: 2px;
1639
+ background: rgba(241, 242, 237, 0.1);
1640
+ }
1641
+
1642
+ .telemetry-meter > span {
1643
+ display: block;
1644
+ height: 100%;
1645
+ border-radius: inherit;
1646
+ background: var(--phosphor);
1647
+ transition: width 120ms linear;
1648
+ }
1649
+
1650
  .device-card {
1651
  display: grid;
1652
  grid-template-columns: auto 1fr auto;
 
1654
  gap: 10px;
1655
  margin: 19px 0 14px;
1656
  padding: 10px;
1657
+ border: 1px solid var(--line-light);
1658
+ border-radius: 8px;
1659
+ background: var(--ink-raised);
1660
  }
1661
 
1662
  .device-icon {
 
1728
  .meter-track > span {
1729
  display: block;
1730
  height: 100%;
1731
+ background: var(--phosphor);
1732
  }
1733
 
1734
  .memory-grid {
 
1813
  width: 25px;
1814
  height: 25px;
1815
  border: 1px solid var(--line-light);
1816
+ border-radius: 6px;
1817
  background: transparent;
1818
  cursor: pointer;
1819
  }
1820
 
1821
  .artifact-empty {
1822
  margin: 0;
1823
+ color: var(--paper-ghost);
1824
+ font: var(--type-caption)/1.5 var(--body);
1825
  }
1826
 
1827
  .artifact-pane {
 
1938
  display: flex;
1939
  justify-content: flex-end;
1940
  background: rgba(8, 11, 9, 0.64);
 
1941
  animation: station-in 160ms ease both;
1942
  }
1943
 
 
1948
  padding: 30px 27px;
1949
  color: var(--paper);
1950
  border-left: 1px solid var(--phosphor);
1951
+ background: var(--ink);
 
 
1952
  box-shadow: -24px 0 70px rgba(0, 0, 0, 0.32);
1953
  animation: sheet-in 300ms cubic-bezier(.16, 1, .3, 1) both;
1954
  }
 
1985
 
1986
  .setting-group {
1987
  display: grid;
1988
+ grid-template-columns: minmax(0, 1fr) auto;
1989
  align-items: center;
1990
  gap: 12px;
1991
  padding: 15px 0;
1992
  border-bottom: 1px solid var(--line-light);
1993
  }
1994
 
 
 
 
 
 
1995
  .setting-group > div,
1996
  .range-setting > span:first-child {
1997
  display: grid;
 
2231
 
2232
  @media (max-width: 1279px) {
2233
  .station-header {
2234
+ grid-template-columns: 240px minmax(260px, 1fr) auto;
2235
  }
2236
 
2237
+ .browser-notice > span > span {
2238
+ display: none;
2239
  }
2240
 
2241
+ .browser-notice.is-warning > span > span {
2242
+ display: block;
2243
  }
2244
 
2245
  .workspace-grid {
2246
+ grid-template-columns: 240px minmax(0, 1fr);
2247
  grid-template-areas:
2248
  "models chat"
2249
  "inspector inspector";
2250
  }
2251
 
2252
+ .workspace-grid.is-models-collapsed,
2253
+ .workspace-grid.is-models-collapsed.is-inspector-collapsed {
2254
+ grid-template-columns: 0 minmax(0, 1fr);
2255
+ }
2256
+
2257
+ .workspace-grid.is-inspector-collapsed:not(.is-models-collapsed) {
2258
+ grid-template-columns: 240px minmax(0, 1fr);
2259
+ }
2260
+
2261
  .model-rail,
2262
  .chat-surface {
2263
+ min-height: calc(100dvh - 64px);
2264
+ height: calc(100dvh - 64px);
2265
  }
2266
 
2267
  .inspector {
 
2270
  display: grid;
2271
  grid-template-columns: repeat(2, minmax(0, 1fr));
2272
  overflow: visible;
2273
+ border-top: 1px solid var(--line-light);
2274
  border-left: 0;
2275
  }
2276
 
 
2282
 
2283
  @media (max-width: 820px) {
2284
  .station-header {
2285
+ min-height: 58px;
2286
+ display: grid;
2287
+ grid-template-columns: minmax(0, 1fr) auto;
2288
  }
2289
 
2290
  .brand-block {
2291
+ min-width: 0;
2292
+ padding: 10px 14px;
2293
+ border-right: 0;
2294
  }
2295
 
2296
+ .brand-glyph {
2297
+ width: 32px;
2298
+ height: 32px;
2299
+ flex-basis: 32px;
2300
  }
2301
 
2302
+ .browser-notice {
2303
+ grid-column: 1 / -1;
2304
+ grid-row: 2;
2305
+ min-height: 38px;
2306
+ padding: 7px 14px;
2307
  border-top: 1px solid var(--line-light);
2308
  }
2309
 
2310
  .header-actions {
2311
+ grid-column: 2;
2312
+ grid-row: 1;
2313
+ position: static;
2314
+ padding-inline: 10px;
2315
+ border-left: 0;
 
 
2316
  }
2317
 
2318
  .workspace-grid {
 
2330
  display: block;
2331
  padding: 18px 15px;
2332
  border-right: 0;
2333
+ border-bottom: 1px solid var(--line-light);
2334
  }
2335
 
2336
  .model-list {
 
2352
 
2353
  .chat-surface {
2354
  display: grid;
2355
+ width: 100% !important;
2356
+ min-height: calc(100dvh - 96px);
2357
+ grid-template-rows: auto minmax(240px, auto) auto;
2358
  overflow: visible;
2359
  }
2360
 
 
2387
 
2388
  .inspector {
2389
  display: block;
2390
+ border-top: 1px solid var(--line-light);
2391
  }
2392
 
2393
  .inspector-section {
 
2398
 
2399
  @media (max-width: 520px) {
2400
  .brand-block h1 {
2401
+ font-size: 18px;
2402
+ }
2403
+
2404
+ .brand-block > div > span {
2405
+ display: none;
2406
+ }
2407
+
2408
+ .layout-controls {
2409
+ gap: 2px;
2410
  }
2411
 
2412
+ .layout-controls button,
2413
+ .icon-button {
2414
+ width: 32px;
2415
+ height: 32px;
2416
  }
2417
 
2418
  .chat-header {
 
2420
  }
2421
 
2422
  .botanical-subtitle {
2423
+ display: none;
 
2424
  }
2425
 
2426
  .message-user {
 
2432
  }
2433
 
2434
  .composer-activity {
2435
+ padding-inline: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2436
  }
2437
 
2438
+ .activity-status small {
2439
+ max-width: 210px;
2440
  }
2441
 
2442
  .composer-field {
 
2461
  }
2462
 
2463
  .gauge-ring::after {
2464
+ display: none;
2465
+ }
2466
+
2467
+ .telemetry-live-grid {
2468
+ grid-template-columns: 1fr;
2469
  }
2470
  }
2471
 
src/app/theme.test.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeThemeMode, resolveThemeMode } from './theme';
3
+
4
+ describe('workspace theme', () => {
5
+ it('falls back to the system theme for missing or invalid persisted values', () => {
6
+ expect(normalizeThemeMode(null)).toBe('system');
7
+ expect(normalizeThemeMode('sepia')).toBe('system');
8
+ });
9
+
10
+ it('resolves system mode without overriding explicit light and dark choices', () => {
11
+ expect(resolveThemeMode('system', true)).toBe('dark');
12
+ expect(resolveThemeMode('system', false)).toBe('light');
13
+ expect(resolveThemeMode('light', true)).toBe('light');
14
+ expect(resolveThemeMode('dark', false)).toBe('dark');
15
+ });
16
+ });
src/app/theme.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type ThemeMode = 'system' | 'light' | 'dark';
2
+ export type ResolvedTheme = Exclude<ThemeMode, 'system'>;
3
+
4
+ export const THEME_STORAGE_KEY = 'bonsai-theme';
5
+
6
+ export function normalizeThemeMode(value: string | null | undefined): ThemeMode {
7
+ return value === 'light' || value === 'dark' || value === 'system' ? value : 'system';
8
+ }
9
+
10
+ export function resolveThemeMode(mode: ThemeMode, systemDark: boolean): ResolvedTheme {
11
+ return mode === 'system' ? (systemDark ? 'dark' : 'light') : mode;
12
+ }
src/lib/contracts.ts CHANGED
@@ -50,6 +50,15 @@ export interface ChatMessage {
50
  error?: string;
51
  }
52
 
 
 
 
 
 
 
 
 
 
53
  export interface TelemetrySnapshot {
54
  backend: 'WebGPU' | 'WASM' | 'Unavailable';
55
  device: string;
@@ -140,6 +149,9 @@ export interface BonsaiShellProps {
140
  activeModelId: ModelId;
141
  loadedModelId: ModelId | null;
142
  messages: ChatMessage[];
 
 
 
143
  streamState: StreamState;
144
  modelLoadProgress: ModelLoadProgress;
145
  telemetry: TelemetrySnapshot;
@@ -164,6 +176,11 @@ export interface BonsaiShellProps {
164
  onPersistStorage: () => void;
165
  onClearStorage: () => void;
166
  onClearConversation: () => void;
 
 
 
 
 
167
  onDismissError: () => void;
168
  onRetryShard: (modelId: ModelId) => void;
169
  onCloseArtifact: () => void;
 
50
  error?: string;
51
  }
52
 
53
+ export interface ChatSession {
54
+ id: string;
55
+ title: string;
56
+ messages: ChatMessage[];
57
+ modelId: ModelId;
58
+ systemPrompt: string;
59
+ updatedAt: number;
60
+ }
61
+
62
  export interface TelemetrySnapshot {
63
  backend: 'WebGPU' | 'WASM' | 'Unavailable';
64
  device: string;
 
149
  activeModelId: ModelId;
150
  loadedModelId: ModelId | null;
151
  messages: ChatMessage[];
152
+ conversations: ChatSession[];
153
+ activeConversationId: string;
154
+ conversationListOpen: boolean;
155
  streamState: StreamState;
156
  modelLoadProgress: ModelLoadProgress;
157
  telemetry: TelemetrySnapshot;
 
176
  onPersistStorage: () => void;
177
  onClearStorage: () => void;
178
  onClearConversation: () => void;
179
+ onOpenConversationList: () => void;
180
+ onNewConversation: () => void;
181
+ onOpenConversation: (conversationId: string) => void;
182
+ onOpenConversationSettings: (conversationId: string) => void;
183
+ onDeleteConversation: (conversationId: string) => void;
184
  onDismissError: () => void;
185
  onRetryShard: (modelId: ModelId) => void;
186
  onCloseArtifact: () => void;
src/lib/conversations.test.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ChatMessage, ChatSession } from './contracts';
3
+ import { conversationPreview, conversationTitle, normalizeConversationModelId, upsertConversation } from './conversations';
4
+
5
+ const message = (content: string, role: ChatMessage['role'] = 'user'): ChatMessage => ({
6
+ id: content,
7
+ role,
8
+ content,
9
+ timestamp: '12:00:00',
10
+ });
11
+
12
+ describe('local conversation sessions', () => {
13
+ it('derives a compact title from the first user prompt and a preview from the latest message', () => {
14
+ const messages = [message(' Explain recurrent models '), message('A compact answer', 'assistant')];
15
+ expect(conversationTitle(messages)).toBe('Explain recurrent models');
16
+ expect(conversationPreview(messages)).toBe('A compact answer');
17
+ });
18
+
19
+ it('falls back to the flagship model for stale persisted model ids', () => {
20
+ expect(normalizeConversationModelId('bonsai-8b')).toBe('bonsai-8b');
21
+ expect(normalizeConversationModelId('removed-model')).toBe('bonsai-27b');
22
+ });
23
+
24
+ it('moves an updated session to the front without duplicating it', () => {
25
+ const older: ChatSession = { id: 'a', title: 'A', messages: [], modelId: 'bonsai-27b', systemPrompt: 'A', updatedAt: 1 };
26
+ const other: ChatSession = { id: 'b', title: 'B', messages: [], modelId: 'bonsai-8b', systemPrompt: 'B', updatedAt: 2 };
27
+ const updated = { ...older, title: 'Updated', updatedAt: 3 };
28
+ expect(upsertConversation([other, older], updated).map(({ id, title }) => ({ id, title }))).toEqual([
29
+ { id: 'a', title: 'Updated' },
30
+ { id: 'b', title: 'B' },
31
+ ]);
32
+ });
33
+ });
src/lib/conversations.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ChatMessage, ChatSession, ModelId } from './contracts';
2
+
3
+ const MODEL_IDS: readonly ModelId[] = ['bonsai-1.7b', 'bonsai-4b', 'bonsai-8b', 'bonsai-27b'];
4
+
5
+ export function conversationTitle(messages: ChatMessage[]): string {
6
+ const firstPrompt = messages.find((message) => message.role === 'user' && message.content.trim())?.content.trim();
7
+ return firstPrompt ? firstPrompt.replace(/\s+/g, ' ').slice(0, 80) : 'New chat';
8
+ }
9
+
10
+ export function conversationPreview(messages: ChatMessage[]): string {
11
+ const latest = [...messages].reverse().find((message) => message.content.trim())?.content.trim();
12
+ return latest ? latest.replace(/\s+/g, ' ').slice(0, 140) : 'No messages yet';
13
+ }
14
+
15
+ export function normalizeConversationModelId(value: string): ModelId {
16
+ return MODEL_IDS.includes(value as ModelId) ? value as ModelId : 'bonsai-27b';
17
+ }
18
+
19
+ export function upsertConversation(sessions: ChatSession[], next: ChatSession): ChatSession[] {
20
+ return [next, ...sessions.filter((session) => session.id !== next.id)]
21
+ .sort((left, right) => right.updatedAt - left.updatedAt);
22
+ }
src/lib/idb.ts CHANGED
@@ -14,6 +14,7 @@ export interface ConversationRecord<TMessage = unknown> {
14
  title: string;
15
  messages: TMessage[];
16
  modelId: string;
 
17
  updatedAt: number;
18
  }
19
 
@@ -116,3 +117,11 @@ export async function listConversations<TMessage>(): Promise<ConversationRecord<
116
  database.close();
117
  return result.sort((left, right) => right.updatedAt - left.updatedAt);
118
  }
 
 
 
 
 
 
 
 
 
14
  title: string;
15
  messages: TMessage[];
16
  modelId: string;
17
+ systemPrompt?: string;
18
  updatedAt: number;
19
  }
20
 
 
117
  database.close();
118
  return result.sort((left, right) => right.updatedAt - left.updatedAt);
119
  }
120
+
121
+ export async function deleteConversation(id: string): Promise<void> {
122
+ const database = await openDatabase();
123
+ const transaction = database.transaction(CONVERSATION_STORE, 'readwrite');
124
+ transaction.objectStore(CONVERSATION_STORE).delete(validateIdentifier(id, 'conversation id'));
125
+ await transactionDone(transaction);
126
+ database.close();
127
+ }