From 9aa2b11c6850bea50cde1be93c8fc5548e5f877a Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:23:44 +0100 Subject: [PATCH 01/38] UI polish: one column, page titles, calmer metadata, firmer dark surfaces Co-Authored-By: Claude Opus 5.5 --- ui/src/App.tsx | 2 +- ui/src/index.css | 108 ++++++++++++++++++++------------------ ui/src/views/activity.tsx | 4 +- ui/src/views/memory.tsx | 2 + ui/src/views/routines.tsx | 2 + 5 files changed, 65 insertions(+), 53 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 96b00bd5..53815ddd 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -394,7 +394,7 @@ function Main() { ) : ( // Keyed: each view starts at the top (the scroller used to keep the last view's position).
-
+
{view === 'memory' && } {view === 'skills' && } {view === 'routines' && } diff --git a/ui/src/index.css b/ui/src/index.css index e0b9a0f7..f0316b24 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -15,13 +15,13 @@ --surface: #FFFFFF; --sunken: #F3F3F1; --field: #FFFFFF; - --hover: rgba(24, 24, 32, 0.045); - --selected: rgba(24, 24, 32, 0.08); + --hover: rgba(24, 24, 32, 0.035); + --selected: rgba(24, 24, 32, 0.075); --line: rgba(24, 24, 32, 0.085); --line-strong: rgba(24, 24, 32, 0.15); --text: #1C1C1E; - --text-2: #5D5D64; - --text-3: #8F8F96; + --text-2: #55555C; + --text-3: #7E7E86; --ink: #1C1C1E; --on-ink: #FFFFFF; --live: #2E9D62; @@ -31,27 +31,27 @@ --pending-soft: rgba(201, 134, 19, 0.12); --danger-soft: rgba(207, 62, 54, 0.09); --focus: rgba(74, 111, 196, 0.42); - --shadow-sm: 0 1px 1.5px rgba(20, 20, 30, 0.06), 0 0 0 0.5px rgba(20, 20, 30, 0.07); - --shadow-md: 0 4px 14px -4px rgba(20, 20, 40, 0.12), 0 0 0 0.5px rgba(20, 20, 30, 0.08); + --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.04), 0 0 0 1px rgba(20, 20, 30, 0.055); + --shadow-md: 0 6px 20px -8px rgba(20, 20, 40, 0.14), 0 0 0 1px rgba(20, 20, 30, 0.06); --shadow-lg: 0 18px 44px -12px rgba(20, 20, 40, 0.22), 0 0 0 0.5px rgba(20, 20, 30, 0.09); --radius: 10px; } @media (prefers-color-scheme: dark) { :root { - --bg: #1A1A1C; - --rail: #222225; + --bg: #171719; + --rail: #1E1E21; --rail-glass: rgba(34, 34, 37, 0.7); - --surface: #222225; - --sunken: #29292D; - --field: #1F1F22; - --hover: rgba(255, 255, 255, 0.05); - --selected: rgba(255, 255, 255, 0.09); + --surface: #222226; + --sunken: #2A2A2F; + --field: #1C1C1F; + --hover: rgba(255, 255, 255, 0.04); + --selected: rgba(255, 255, 255, 0.085); --line: rgba(255, 255, 255, 0.075); --line-strong: rgba(255, 255, 255, 0.14); --text: #EDEDEF; - --text-2: #A7A7AF; - --text-3: #727279; + --text-2: #B2B2BA; + --text-3: #86868E; --ink: #EDEDEF; --on-ink: #1A1A1C; --live: #42B97A; @@ -61,8 +61,8 @@ --pending-soft: rgba(224, 162, 58, 0.14); --danger-soft: rgba(238, 97, 88, 0.13); --focus: rgba(122, 152, 230, 0.5); - --shadow-sm: 0 1px 1.5px rgba(0, 0, 0, 0.3), 0 0 0 0.5px rgba(255, 255, 255, 0.06); - --shadow-md: 0 6px 18px -6px rgba(0, 0, 0, 0.5), 0 0 0 0.5px rgba(255, 255, 255, 0.07); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.055), inset 0 1px 0 rgba(255, 255, 255, 0.025); + --shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-lg: 0 22px 50px -12px rgba(0, 0, 0, 0.65), 0 0 0 0.5px rgba(255, 255, 255, 0.08); } } @@ -85,8 +85,9 @@ button, input, select, textarea { font: inherit; color: inherit; } button { cursor: default; } :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 6px; } ::selection { background: color-mix(in srgb, var(--focus) 70%, transparent); } -code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: var(--sunken); } +code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 8%, transparent); } a { color: inherit; } +a.btn { text-decoration: none; } .icon { flex: none; display: block; } /* Native shell (macOS desktop): transparent window, vibrancy shows through the sidebar. */ @@ -137,7 +138,7 @@ a { color: inherit; } .rail-item .ri-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub { font-size: 11px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } -.rail-item .ri-count { font: 11px var(--mono); color: var(--text-3); } +.rail-item .ri-count { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .rail-item .ri-glyph { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); background: var(--hover); flex: none; } .rail-hint { padding: 6px 8px; font-size: 11.5px; color: var(--text-3); line-height: 1.4; } .rail-foot { padding: 8px 10px 12px; border-top: 0.5px solid var(--line); } @@ -169,7 +170,7 @@ a { color: inherit; } .lead { margin: 3px 0 0; color: var(--text-2); font-size: 13px; } .section-title { display: flex; align-items: baseline; gap: 8px; margin: 28px 0 10px; } .section-title h2 { margin: 0; font: 600 17px/1.2 var(--serif); letter-spacing: -0.01em; } -.section-title .count { font: 11px var(--mono); color: var(--text-3); } +.section-title .count { font-size: 12px; font-weight: 500; color: var(--text-3); font-variant-numeric: tabular-nums; } .section-title .grow { flex: 1; } /* ---------------- primitives ---------------- */ @@ -234,7 +235,7 @@ a { color: inherit; } } .segmented button:hover { color: var(--text); } .segmented button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } -@media (prefers-color-scheme: dark) { .segmented button.on { background: #3A3A40; } } +@media (prefers-color-scheme: dark) { .segmented button.on { background: #36363C; } } .segmented.sm button { height: 22px; padding: 0 9px; font-size: 12px; } .badge { display: inline-flex; align-items: center; gap: 4px; height: 18px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; background: var(--hover); color: var(--text-2); white-space: nowrap; } @@ -268,10 +269,10 @@ a { color: inherit; } /* Inset grouped list, as in System Settings. */ .group-wrap { margin: 0 0 26px; } -.group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 4px 7px; } +.group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 2px 8px; } .group-head h3 { margin: 0; font-size: 12px; font-weight: 600; color: var(--text-2); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } -.group-note { margin: 7px 4px 0; font-size: 12px; color: var(--text-3); line-height: 1.45; } +.group-note { margin: 8px 2px 0; font-size: 12px; color: var(--text-3); line-height: 1.45; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); flex: none; } @@ -333,15 +334,15 @@ a { color: inherit; } /* ---------------- chat ---------------- */ .chat { display: flex; flex-direction: column; min-height: 100%; } -.thread { flex: 1; max-width: 720px; width: 100%; margin: 0 auto; padding: 28px 32px 12px; display: flex; flex-direction: column; gap: 18px; } -.chat-search { position: sticky; top: 0; z-index: 3; max-width: 720px; width: 100%; margin: 0 auto; padding: 10px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } +.thread { flex: 1; max-width: 760px; width: 100%; margin: 0 auto; padding: 28px 32px 12px; display: flex; flex-direction: column; gap: 18px; } +.chat-search { position: sticky; top: 0; z-index: 3; max-width: 760px; width: 100%; margin: 0 auto; padding: 10px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } .chat-search .search { display: block; max-width: 240px; margin-left: auto; } .chat-search .input { height: 28px; font-size: 12.5px; } .chat-search .transcript { margin: 6px 0 0; max-height: 320px; box-shadow: var(--shadow-md); } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: center; max-width: 520px; display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far { margin: 0; align-self: center; max-width: 520px; padding: 10px 16px; border-radius: 12px; background: var(--hover); display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } .so-far b { color: var(--text-2); font-weight: 600; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } @@ -377,7 +378,7 @@ a { color: inherit; } /* Cards a bot keeps up to date, pinned above the chat. */ .pinned-cards:empty { display: none; } -.pinned-cards { max-width: 720px; width: 100%; margin: 0 auto; padding: 14px 32px 0; display: grid; gap: 8px; } +.pinned-cards { max-width: 760px; width: 100%; margin: 0 auto; padding: 14px 32px 0; display: grid; gap: 8px; } .pinned-card { border: 0.5px solid var(--line-strong); border-left: 2px solid var(--pig); border-radius: 9px; background: var(--surface); padding: 8px 12px; } .pinned-card summary { display: flex; align-items: center; gap: 7px; cursor: pointer; list-style: none; color: var(--text-2); } .pinned-card summary::-webkit-details-marker { display: none; } @@ -481,10 +482,11 @@ a { color: inherit; } .composer-wrap { position: sticky; bottom: 0; padding: 10px 32px 20px; background: linear-gradient(to bottom, transparent, var(--bg) 26%); } .composer { - max-width: 696px; margin: 0 auto; display: flex; align-items: flex-end; gap: 8px; padding: 7px 7px 7px 8px; - border-radius: 16px; background: var(--surface); box-shadow: var(--shadow-md); border: 0.5px solid var(--line); + max-width: 696px; margin: 0 auto; display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 8px; + border-radius: 16px; background: var(--surface); border: 1px solid var(--line-strong); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px -14px rgba(0, 0, 0, 0.18); } -.composer:focus-within { border-color: var(--line-strong); box-shadow: var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--focus) 16%, transparent); } +.composer:focus-within { border-color: color-mix(in srgb, var(--focus) 70%, var(--line-strong)); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px -14px rgba(0, 0, 0, 0.18), 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); } .composer textarea { flex: 1; border: 0; background: none; resize: none; padding: 5px 4px; font-size: 14px; line-height: 1.45; max-height: 160px; outline: none; } .composer textarea::placeholder { color: var(--text-3); } .composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--ink); color: var(--on-ink); flex: none; transition: transform 90ms ease, opacity 120ms; } @@ -497,7 +499,7 @@ a { color: inherit; } .attach { position: relative; width: 60px; height: 60px; border-radius: 12px; overflow: hidden; box-shadow: var(--shadow-sm); animation: pop 340ms var(--spring-soft) both; } .attach img { width: 100%; height: 100%; object-fit: cover; display: block; } .attach button { position: absolute; top: 4px; right: 4px; width: 18px; height: 18px; border-radius: 50%; border: 0; display: grid; place-items: center; background: rgba(0, 0, 0, 0.55); color: #fff; padding: 0; } -.composer.dropping { box-shadow: var(--shadow-md), 0 0 0 3px color-mix(in srgb, var(--focus) 40%, transparent); } +.composer.dropping { box-shadow: 0 10px 30px -14px rgba(0, 0, 0, 0.18), 0 0 0 3px color-mix(in srgb, var(--focus) 40%, transparent); } .composer-note { max-width: 696px; margin: 7px auto 0; text-align: center; font-size: 11px; color: var(--text-3); } .starters { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 4px; } .starter { border: 0.5px solid var(--line-strong); background: var(--surface); border-radius: 99px; padding: 6px 12px; font-size: 12.5px; color: var(--text-2); box-shadow: var(--shadow-sm); } @@ -514,25 +516,26 @@ a { color: inherit; } .mem-list { display: grid; gap: 8px; } .mem { - position: relative; padding: 13px 16px 12px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); + position: relative; padding: 14px 16px 12px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); transition: box-shadow 160ms ease; } .mem:hover { box-shadow: var(--shadow-md); } .mem.superseded, .mem.forgotten { background: none; box-shadow: inset 0 0 0 0.5px var(--line-strong); } .mem.superseded .mem-title, .mem.superseded .mem-body, .mem.forgotten .mem-title, .mem.forgotten .mem-body { color: var(--text-3); } .mem.forgotten .mem-title { text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--danger) 60%, transparent); } -.mem-title { font: 600 15px/1.3 var(--serif); letter-spacing: -0.005em; padding-right: 92px; } +.mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } -.versions small { font: 10.5px var(--mono); color: var(--text-3); } +.versions small { font-size: 11px; color: var(--text-3); } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 4px 0 9px; font-size: 13.5px; line-height: 1.55; color: var(--text); white-space: pre-wrap; } -.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font: 11px var(--mono); color: var(--text-3); } +.mem-body { margin: 3px 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--text); white-space: pre-wrap; } +.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .gone { color: var(--danger); } .mem-meta .warn { color: var(--pending); } @@ -577,7 +580,7 @@ a { color: inherit; } .weekly .suggest-head .icon { color: var(--text-2); } .weekly .sugg .badge { max-width: 70px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .skill-steps { margin: 6px 0 2px; white-space: pre-wrap; font-size: 12.5px; line-height: 1.55; color: var(--text-2); } -.weekly-trend { margin: 0; padding: 9px 14px; border-top: 0.5px solid var(--line); font: 11px var(--mono); color: var(--text-3); } +.weekly-trend { margin: 0; padding: 9px 14px; border-top: 0.5px solid var(--line); font-size: 11.5px; color: var(--text-3); } /* ---------------- routines ---------------- */ @@ -592,7 +595,7 @@ a { color: inherit; } .routine .rh .bad { color: var(--danger); } .rh-dots { display: inline-flex; gap: 3px; } .dot.missed { background: none; box-shadow: inset 0 0 0 1.5px var(--pending); } -.routine-actions { display: flex; align-items: center; gap: 4px; } +.routine-actions { display: flex; align-items: center; gap: 2px; align-self: center; } .routine-actions .switch { margin-right: 6px; } .routine.paused .ri, .routine.paused .rt, .routine.paused .rp { opacity: .5; } .routine-inline { padding: 16px; } @@ -601,10 +604,10 @@ a { color: inherit; } .preset { text-align: left; padding: 14px; border-radius: 12px; border: 0; background: var(--surface); box-shadow: var(--shadow-sm); display: grid; gap: 4px; transition: box-shadow 160ms ease, transform 90ms ease; } .preset:hover { box-shadow: var(--shadow-md); } .preset:active { transform: scale(0.985); } -.preset .icon { color: var(--pending); margin-bottom: 4px; } +.preset .icon { color: var(--text-2); margin-bottom: 4px; } .preset b { font-size: 13px; } .preset span { font-size: 12px; color: var(--text-3); } -.presets.four { grid-template-columns: repeat(4, 1fr); } +.presets.four { grid-template-columns: repeat(3, 1fr); } .presets .preset { align-content: start; } .preset.new .icon { color: var(--text-2); } .routine-form { display: grid; gap: 14px; } @@ -624,8 +627,8 @@ a { color: inherit; } .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 22px; } .stat { padding: 14px 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); } -.stat dt { font-size: 11.5px; color: var(--text-3); font-weight: 500; } -.stat dd { margin: 3px 0 0; font: 600 22px/1.15 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } +.stat dt { font-size: 12px; color: var(--text-2); font-weight: 500; } +.stat dd { margin: 6px 0 0; font: 600 24px/1.1 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .stat dd small { font-size: 12px; font-weight: 500; color: var(--text-3); letter-spacing: 0; } .stat dd small.bad { color: var(--danger); } .stat p { margin: 4px 0 0; font-size: 11px; color: var(--text-3); line-height: 1.35; } @@ -637,7 +640,7 @@ a { color: inherit; } .run-t small { font-size: 11.5px; color: var(--text-3); } .run-bot { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-2); } .run-bot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.num { font: 11.5px var(--mono); color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; } +.num { font-size: 12px; color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; } .runrow .icon { color: var(--text-3); transition: transform 160ms ease; } .runrow[aria-expanded="true"] .icon { transform: rotate(90deg); } .run-detail { padding: 4px 14px 14px 36px; } @@ -655,7 +658,7 @@ a { color: inherit; } /* ---------------- bot page ---------------- */ .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } -.provider-card { text-align: left; padding: 12px; border-radius: 10px; border: 0.5px solid var(--line-strong); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } +.provider-card { text-align: left; padding: 10px 12px; border-radius: 10px; border: 0.5px solid var(--line-strong); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 11.5px; color: var(--text-3); line-height: 1.35; } .templates .provider-card { align-content: start; } @@ -665,13 +668,13 @@ a { color: inherit; } .starter-routine .row-title { display: flex; align-items: center; gap: 6px; } .starter-routine .textarea { margin-top: 6px; min-height: 0; } .starter-routine .textarea:disabled { opacity: .5; } -.provider-card.on { border-color: var(--pig, var(--ink)); box-shadow: inset 0 0 0 0.5px var(--pig, var(--ink)), 0 0 0 3px color-mix(in srgb, var(--pig, var(--ink)) 16%, transparent); } +.provider-card.on { border-color: color-mix(in srgb, var(--pig, var(--ink)) 70%, transparent); background: color-mix(in srgb, var(--pig, var(--ink)) 9%, var(--field)); box-shadow: inset 0 0 0 0.5px var(--pig, var(--ink)); } .chan-row .row-icon { color: var(--text); } .account-test { display: flex; align-items: center; gap: 10px; min-height: 24px; } .account-test .typing { padding: 0; } .pair { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 12px; background: var(--live-soft); margin-bottom: 22px; font-size: 13px; } .pair code { font-size: 13px; font-weight: 600; background: var(--surface); padding: 3px 8px; letter-spacing: 0.04em; } -.savebar { position: sticky; bottom: 0; display: flex; align-items: center; gap: 10px; padding: 12px 32px; margin: 0 -32px -64px; background: color-mix(in srgb, var(--bg) 85%, transparent); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-top: 0.5px solid var(--line); } +.savebar { position: sticky; bottom: 0; z-index: 2; display: flex; align-items: center; gap: 10px; padding: 14px 0 18px; margin: 28px 0 -64px; background: var(--bg); border-top: 1px solid var(--line); } .savebar .grow { flex: 1; } .tool-grid { display: grid; grid-template-columns: 1fr 1fr; } .tool-grid .row { border-top: 0.5px solid var(--line); } @@ -840,7 +843,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p transform: translateX(var(--x, 0)); width: var(--w, 0); transition: transform 460ms var(--spring-soft), width 460ms var(--spring-soft); } -@media (prefers-color-scheme: dark) { .segmented .thumb { background: #3A3A40; } } +@media (prefers-color-scheme: dark) { .segmented .thumb { background: #36363C; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.06); } } .segmented.measuring .thumb { transition: none; } .segmented.glide button.on { background: none; box-shadow: none; } .segmented button { transition: color 200ms ease, transform 120ms ease; } @@ -981,8 +984,8 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .teach .er .input { flex: 1; width: auto; } .standing { display: flex; align-items: center; gap: 10px; margin: 2px 0 12px; } .standing-bar { width: 120px; height: 5px; border-radius: 3px; background: var(--sunken, rgba(127,127,127,.15)); overflow: hidden; flex: none; } -.standing-bar i { display: block; height: 100%; background: var(--accent, #3a7); border-radius: 3px; } -.standing-bar i.full { background: var(--warn, #c80); } +.standing-bar i { display: block; height: 100%; background: var(--live); border-radius: 3px; } +.standing-bar i.full { background: var(--pending); } .retired { margin-top: 10px; } .retired summary { cursor: default; padding: 4px 0; } .slash-menu { display: grid; gap: 2px; margin: 0 0 6px; padding: 6px; border-radius: 12px; background: var(--surface); border: 0.5px solid var(--line); box-shadow: var(--shadow-sm); } @@ -1035,7 +1038,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .library { margin-top: 28px; } .library .section-title .select-inline { margin-left: auto; } .library .hint { margin: 0 0 6px; } -.lib-cat h3 { font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); margin: 18px 0 8px; } +.lib-cat h3 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); margin: 22px 2px 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ .library .mem-actions.lib-add { opacity: 1; transform: none; } .file-text { white-space: pre-wrap; font: 12px var(--mono); margin: 0; } @@ -1071,3 +1074,6 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .tips.plan small.warn { color: var(--pending); } .tips.plan li.off { opacity: 0.55; } .tips.plan input { margin-top: 2px; } + +/* The settings search is focused on arrival: a quiet edge, not the full glow (that's for typing elsewhere). */ +.settings-search .input:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); } diff --git a/ui/src/views/activity.tsx b/ui/src/views/activity.tsx index b022f7b4..ee6dcba9 100644 --- a/ui/src/views/activity.tsx +++ b/ui/src/views/activity.tsx @@ -27,11 +27,13 @@ export function ActivityView({ chatId }: { chatId: number }) { if (!list.length) return Each reply, routine and tidy-up shows up here with its model, tokens, cost and every tool it used. return ( <> +

Activity

+

Every run in this chat over the last 30 days. The cost is what the same use would cost on the API; subscriptions aren’t billed per token.

{t && (
Runs · 30 days
{t.runs}{t.errors ? · {t.errors} failed : null}
Tokens in / out
{tok(t.tokens_in)} / {tok(t.tokens_out)}
-
API-equivalent cost
{usd(t.cost_usd)}

Subscriptions aren’t billed per token; this is what the same use would cost on the API.

+
API-equivalent cost
{usd(t.cost_usd)}
)}
diff --git a/ui/src/views/memory.tsx b/ui/src/views/memory.tsx index 0f3e49dc..0a09130b 100644 --- a/ui/src/views/memory.tsx +++ b/ui/src/views/memory.tsx @@ -64,6 +64,8 @@ export function MemoryView({ chatId, onChange }: { chatId: number; onChange: () return ( <> +

Memory

+

What your bots know about you here. Correct it, pin it, or forget it anytime.

{ setDraft(null); setErr(null); rows.reload() }, e => setErr(String(e))) return ( <> +

Routines

+

What your bots do on a schedule, and what they keep an eye on for you.

{err &&

{err}

} {list.length === 0 ? ( From 0d3836c661801d7edda10946eb4946eed7081d11 Mon Sep 17 00:00:00 2001 From: Solenta Date: Thu, 5 Nov 2026 20:25:41 +0100 Subject: [PATCH 02/38] coder-checkpoint: turn 34 Solenta-Message-Id: 22d76fbf-7ebe-454d-ba75-fa99339d5479 --- ui/src/views/activity.tsx | 1 + ui/src/views/bot.tsx | 2 +- ui/src/views/inbox.tsx | 2 ++ ui/src/views/skills.tsx | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/views/activity.tsx b/ui/src/views/activity.tsx index ee6dcba9..3872ff43 100644 --- a/ui/src/views/activity.tsx +++ b/ui/src/views/activity.tsx @@ -37,6 +37,7 @@ export function ActivityView({ chatId }: { chatId: number }) { )}
+ {list.length > 0 && } {list.map(r => { const b = botOf(r.bot_id) return ( diff --git a/ui/src/views/bot.tsx b/ui/src/views/bot.tsx index 8449209c..4a75699f 100644 --- a/ui/src/views/bot.tsx +++ b/ui/src/views/bot.tsx @@ -311,7 +311,7 @@ export function BotPage({ bot, status, canDelete, onSaved, onDeleted, onChat, on )} -
+
{!isNew && canDelete && } diff --git a/ui/src/views/inbox.tsx b/ui/src/views/inbox.tsx index 9f781d85..6f553121 100644 --- a/ui/src/views/inbox.tsx +++ b/ui/src/views/inbox.tsx @@ -259,6 +259,8 @@ export function InboxView({ data, troubled, reload, chats, chatName, openBot, op ) return (
+

Needs you

+

What your bots are waiting on, and anything that stopped working.

{data.damage && <>

Your data

diff --git a/ui/src/views/skills.tsx b/ui/src/views/skills.tsx index 822bd991..6400b49c 100644 --- a/ui/src/views/skills.tsx +++ b/ui/src/views/skills.tsx @@ -142,7 +142,7 @@ function Skills({ chatId }: { chatId: number }) { setTeaching({ ...teaching, source: e.target.value })} /> - : } + : }
{proposed.length > 0 && (
From 8101ece8a3cd8592e02127d92e78d48b69c49e26 Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:27:27 +0100 Subject: [PATCH 03/38] UI polish: one accent, grouped lists, serif section heads, quieter actions Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index f0316b24..0a9cbe3e 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -30,7 +30,9 @@ --live-soft: rgba(46, 157, 98, 0.11); --pending-soft: rgba(201, 134, 19, 0.12); --danger-soft: rgba(207, 62, 54, 0.09); - --focus: rgba(74, 111, 196, 0.42); + --accent: #4469D6; + --accent-soft: rgba(68, 105, 214, 0.1); + --focus: rgba(68, 105, 214, 0.42); --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.04), 0 0 0 1px rgba(20, 20, 30, 0.055); --shadow-md: 0 6px 20px -8px rgba(20, 20, 40, 0.14), 0 0 0 1px rgba(20, 20, 30, 0.06); --shadow-lg: 0 18px 44px -12px rgba(20, 20, 40, 0.22), 0 0 0 0.5px rgba(20, 20, 30, 0.09); @@ -60,7 +62,9 @@ --live-soft: rgba(66, 185, 122, 0.14); --pending-soft: rgba(224, 162, 58, 0.14); --danger-soft: rgba(238, 97, 88, 0.13); - --focus: rgba(122, 152, 230, 0.5); + --accent: #809FF2; + --accent-soft: rgba(128, 159, 242, 0.13); + --focus: rgba(128, 159, 242, 0.5); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.055), inset 0 1px 0 rgba(255, 255, 255, 0.025); --shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-lg: 0 22px 50px -12px rgba(0, 0, 0, 0.65), 0 0 0 0.5px rgba(255, 255, 255, 0.08); @@ -184,6 +188,7 @@ a.btn { text-decoration: none; } .btn:hover { background: color-mix(in srgb, var(--surface) 92%, var(--text)); } .btn:active { transform: scale(0.97); } .btn:disabled { opacity: 0.45; transform: none; } +.btn-primary:disabled { opacity: 1; background: var(--selected); color: var(--text-3); box-shadow: none; } .btn-primary { background: var(--ink); color: var(--on-ink); border-color: transparent; } .btn-primary:hover { background: color-mix(in srgb, var(--ink) 86%, var(--bg)); } .btn-ghost { background: none; border-color: transparent; box-shadow: none; color: var(--text-2); } @@ -203,7 +208,7 @@ a.btn { text-decoration: none; } } .input { height: 30px; } .textarea { min-height: 72px; resize: vertical; line-height: 1.5; } -.input:focus, .textarea:focus, .select:focus { outline: none; border-color: color-mix(in srgb, var(--focus) 90%, var(--line-strong)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 45%, transparent); } +.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .input::placeholder, .textarea::placeholder { color: var(--text-3); } .select { height: 30px; appearance: none; padding-right: 26px; background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%); background-position: calc(100% - 13px) 13px, calc(100% - 9px) 13px; background-size: 4px 4px; background-repeat: no-repeat; } .mono { font-family: var(--mono); } @@ -224,7 +229,7 @@ a.btn { text-decoration: none; } content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); transition: transform 180ms cubic-bezier(.3, .7, .3, 1); } -.switch:checked { background: var(--live); } +.switch:checked { background: var(--accent); } .switch:checked::after { transform: translateX(13px); } .switch:disabled { opacity: 0.5; } @@ -242,7 +247,7 @@ a.btn { text-decoration: none; } .badge.live { background: var(--live-soft); color: var(--live); } .badge.pending { background: var(--pending-soft); color: var(--pending); } .badge.danger { background: var(--danger-soft); color: var(--danger); } -.badge.count { min-width: 18px; justify-content: center; background: var(--pending); color: #fff; padding: 0 5px; } +.badge.count { min-width: 18px; justify-content: center; background: #F2A33A; color: #2B1800; padding: 0 5px; } .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-3); flex: none; } .dot.live { background: var(--live); } @@ -270,7 +275,7 @@ a.btn { text-decoration: none; } /* Inset grouped list, as in System Settings. */ .group-wrap { margin: 0 0 26px; } .group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 2px 8px; } -.group-head h3 { margin: 0; font-size: 12px; font-weight: 600; color: var(--text-2); } +.group-head h3 { margin: 0; font: 600 15px/1.25 var(--serif); letter-spacing: -0.005em; color: var(--text); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } .group-note { margin: 8px 2px 0; font-size: 12px; color: var(--text-3); line-height: 1.45; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } @@ -486,7 +491,7 @@ a.btn { text-decoration: none; } border-radius: 16px; background: var(--surface); border: 1px solid var(--line-strong); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px -14px rgba(0, 0, 0, 0.18); } -.composer:focus-within { border-color: color-mix(in srgb, var(--focus) 70%, var(--line-strong)); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 10px 30px -14px rgba(0, 0, 0, 0.18), 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); } +.composer:focus-within { border-color: color-mix(in srgb, var(--text) 22%, transparent); } .composer textarea { flex: 1; border: 0; background: none; resize: none; padding: 5px 4px; font-size: 14px; line-height: 1.45; max-height: 160px; outline: none; } .composer textarea::placeholder { color: var(--text-3); } .composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--ink); color: var(--on-ink); flex: none; transition: transform 90ms ease, opacity 120ms; } @@ -514,7 +519,11 @@ a.btn { text-decoration: none; } .settings-search { display: block; margin: -6px 0 22px; } .group-wrap[hidden], .group > [hidden] { display: none !important; } -.mem-list { display: grid; gap: 8px; } +.mem-list { display: grid; gap: 0; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); overflow: hidden; } +.mem-list > * + * { border-top: 1px solid var(--line); } +.mem-list > .mem { border-radius: 0; box-shadow: none; background: none; } +.mem-list > .mem:hover { box-shadow: none; background: color-mix(in srgb, var(--text) 2%, transparent); } +.mem-list > .mem.superseded, .mem-list > .mem.forgotten { box-shadow: none; } .mem { position: relative; padding: 14px 16px 12px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); transition: box-shadow 160ms ease; @@ -526,15 +535,15 @@ a.btn { text-decoration: none; } .mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 3px 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--text); white-space: pre-wrap; } +.mem-body { margin: 3px 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } .mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .gone { color: var(--danger); } @@ -549,7 +558,7 @@ a.btn { text-decoration: none; } .composer.incog { background: color-mix(in srgb, var(--surface) 82%, var(--ink) 18%); } .guarded.incog { background: color-mix(in srgb, var(--surface) 86%, var(--ink) 14%); color: var(--text); } .guarded.incog .icon { color: var(--text-2); } -.mem-actions { position: absolute; top: 9px; right: 9px; display: flex; gap: 2px; opacity: 0; transition: opacity 120ms ease; } +.mem-actions { position: absolute; top: 11px; right: 12px; display: flex; gap: 2px; opacity: 0; transition: opacity 120ms ease; } .mem:hover .mem-actions, .mem:focus-within .mem-actions { opacity: 1; } @media (hover: none) { .mem-actions { opacity: 1; } } .editor { display: grid; gap: 8px; margin: 8px 0 4px; } @@ -597,6 +606,8 @@ a.btn { text-decoration: none; } .dot.missed { background: none; box-shadow: inset 0 0 0 1.5px var(--pending); } .routine-actions { display: flex; align-items: center; gap: 2px; align-self: center; } .routine-actions .switch { margin-right: 6px; } +.routine-actions .btn-ghost { color: var(--text-3); } +.routine-actions .btn-ghost:hover { color: var(--text); } .routine.paused .ri, .routine.paused .rt, .routine.paused .rp { opacity: .5; } .routine-inline { padding: 16px; } .routine + .routine-inline, .routine-inline + .routine { border-top: 0.5px solid var(--line); } @@ -610,6 +621,8 @@ a.btn { text-decoration: none; } .presets.four { grid-template-columns: repeat(3, 1fr); } .presets .preset { align-content: start; } .preset.new .icon { color: var(--text-2); } +.presets.four .preset.new { grid-row: span 2; align-content: center; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } +.presets.four .preset.new:hover { background: var(--hover); box-shadow: none; } .routine-form { display: grid; gap: 14px; } .routine-form .segmented { justify-self: start; } .when-row { align-items: center; } @@ -634,6 +647,8 @@ a.btn { text-decoration: none; } .stat p { margin: 4px 0 0; font-size: 11px; color: var(--text-3); line-height: 1.35; } .runrow { width: 100%; display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; align-items: center; padding: 10px 14px; border: 0; background: none; text-align: left; transition: background-color 120ms ease; } .runrow:hover { background: var(--hover); } +.runhead { display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; padding: 9px 14px 8px; border-bottom: 1px solid var(--line); font-size: 11px; font-weight: 600; color: var(--text-3); } +.runhead .num { font-size: 11px; color: var(--text-3); } .runrow + .run-detail, .run + .run { border-top: 0.5px solid var(--line); } .run-t { display: grid; min-width: 0; } .run-t b { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -658,6 +673,7 @@ a.btn { text-decoration: none; } /* ---------------- bot page ---------------- */ .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.provider-cards:has(> :nth-child(8):last-child) { grid-template-columns: repeat(4, 1fr); } .provider-card { text-align: left; padding: 10px 12px; border-radius: 10px; border: 0.5px solid var(--line-strong); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 11.5px; color: var(--text-3); line-height: 1.35; } @@ -668,14 +684,15 @@ a.btn { text-decoration: none; } .starter-routine .row-title { display: flex; align-items: center; gap: 6px; } .starter-routine .textarea { margin-top: 6px; min-height: 0; } .starter-routine .textarea:disabled { opacity: .5; } -.provider-card.on { border-color: color-mix(in srgb, var(--pig, var(--ink)) 70%, transparent); background: color-mix(in srgb, var(--pig, var(--ink)) 9%, var(--field)); box-shadow: inset 0 0 0 0.5px var(--pig, var(--ink)); } +.provider-card.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--field)); box-shadow: inset 0 0 0 0.5px var(--accent); } .chan-row .row-icon { color: var(--text); } .account-test { display: flex; align-items: center; gap: 10px; min-height: 24px; } .account-test .typing { padding: 0; } .pair { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: 12px; background: var(--live-soft); margin-bottom: 22px; font-size: 13px; } .pair code { font-size: 13px; font-weight: 600; background: var(--surface); padding: 3px 8px; letter-spacing: 0.04em; } -.savebar { position: sticky; bottom: 0; z-index: 2; display: flex; align-items: center; gap: 10px; padding: 14px 0 18px; margin: 28px 0 -64px; background: var(--bg); border-top: 1px solid var(--line); } +.savebar { position: relative; bottom: 0; z-index: 2; display: flex; align-items: center; gap: 10px; padding: 14px 0 18px; margin: 28px 0 -64px; background: var(--bg); border-top: 1px solid var(--line); } .savebar .grow { flex: 1; } +.savebar.stuck { position: sticky; } .tool-grid { display: grid; grid-template-columns: 1fr 1fr; } .tool-grid .row { border-top: 0.5px solid var(--line); } .tool-grid .row:nth-child(-n+2) { border-top: 0; } @@ -761,6 +778,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .presets, .stats, .provider-cards, .tool-grid { grid-template-columns: 1fr; } .tool-grid .row:nth-child(odd) { border-right: 0; } .runrow { grid-template-columns: 8px 1fr 70px 14px; } + .runhead { display: none; } .runrow .run-bot, .runrow .num:nth-of-type(1), .runrow .num:nth-of-type(2) { display: none; } } @@ -984,7 +1002,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .teach .er .input { flex: 1; width: auto; } .standing { display: flex; align-items: center; gap: 10px; margin: 2px 0 12px; } .standing-bar { width: 120px; height: 5px; border-radius: 3px; background: var(--sunken, rgba(127,127,127,.15)); overflow: hidden; flex: none; } -.standing-bar i { display: block; height: 100%; background: var(--live); border-radius: 3px; } +.standing-bar i { display: block; height: 100%; background: var(--accent); border-radius: 3px; } .standing-bar i.full { background: var(--pending); } .retired { margin-top: 10px; } .retired summary { cursor: default; padding: 4px 0; } @@ -1076,4 +1094,4 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .tips.plan input { margin-top: 2px; } /* The settings search is focused on arrival: a quiet edge, not the full glow (that's for typing elsewhere). */ -.settings-search .input:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 18%, transparent); } +.settings-search .input:focus { border-color: color-mix(in srgb, var(--text) 22%, transparent); box-shadow: none; } From a285539037145b1e6d414de27f2a6fffd68fe77c Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:37:56 +0100 Subject: [PATCH 04/38] UI polish: no duplicate titles, routine actions in a menu, readable small text, one control height Co-Authored-By: Claude Opus 5.5 --- scripts/ui-flows.mjs | 4 +-- ui/src/App.tsx | 12 +++---- ui/src/index.css | 75 ++++++++++++++++++++++----------------- ui/src/ui.tsx | 1 + ui/src/views/bot.tsx | 2 +- ui/src/views/chat.tsx | 7 ++-- ui/src/views/routines.tsx | 21 ++++++----- ui/src/views/skills.tsx | 3 +- 8 files changed, 72 insertions(+), 53 deletions(-) diff --git a/scripts/ui-flows.mjs b/scripts/ui-flows.mjs index b67a2d49..77481d0c 100644 --- a/scripts/ui-flows.mjs +++ b/scripts/ui-flows.mjs @@ -84,11 +84,11 @@ await sw.click(); await p.waitForTimeout(800) check('resume clears Paused', !(await p.locator('.routine').innerText()).includes('Paused')) await p.locator('.routine').getByRole('button', { name: 'Run now' }).click() check('run now finishes', await until(async () => (await p.locator('.routine').getByRole('button', { name: 'Run now' }).count()) > 0 && (await p.locator('.rh-dots .dot').count()) > 0, 20000)) -await p.locator('.routine').getByRole('button', { name: 'Edit' }).click(); await p.waitForTimeout(400) +await p.locator('.routine').getByRole('button', { name: 'More for this routine' }).click(); await p.getByRole('menuitem', { name: 'Edit' }).click(); await p.waitForTimeout(400) const prompt = p.locator('.routine-inline textarea').first() await prompt.fill('Brief me on tech news, edited'); await p.locator('.routine-inline').getByRole('button', { name: /Save/ }).click(); await p.waitForTimeout(800) check('edit saved', (await main()).includes('edited')) -await p.getByRole('button', { name: 'Delete routine' }).click(); await p.waitForTimeout(200) +await p.getByRole('button', { name: 'More for this routine' }).click(); await p.getByRole('menuitem', { name: 'Delete…' }).click(); await p.waitForTimeout(200) await p.locator('.ask-box').getByRole('button', { name: 'Delete' }).click(); await p.waitForTimeout(800) check('routine deleted', (await p.locator('.routine').count()) === 0) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 53815ddd..271e5411 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -344,11 +344,13 @@ function Main() {
-
+ {/* Pages outside a chat carry their own big title: the bar stays only as the window's drag area. */} +
{inChat ? ( <>
- + {botChat ? + : } {botChat ? botOf(chatBot ?? botList[0]?.id ?? 1).name : chatName(chat)}{ch.name} · {chat.memories} {chat.memories === 1 ? 'memory' : 'memories'}{household ? '' : ', shared by all your bots'}{chat.household && !household ? ' · reads the household' : ''}{!household && } {!botChat && chat.chat_id !== LOCAL && !household && !chat.family && (
{view !== 'inbox' && setView('inbox')} />} diff --git a/ui/src/index.css b/ui/src/index.css index 0a9cbe3e..7bb7ce58 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -18,10 +18,10 @@ --hover: rgba(24, 24, 32, 0.035); --selected: rgba(24, 24, 32, 0.075); --line: rgba(24, 24, 32, 0.085); - --line-strong: rgba(24, 24, 32, 0.15); + --line-strong: rgba(24, 24, 32, 0.19); --text: #1C1C1E; - --text-2: #55555C; - --text-3: #7E7E86; + --text-2: #4F4F56; + --text-3: #74747C; --ink: #1C1C1E; --on-ink: #FFFFFF; --live: #2E9D62; @@ -33,7 +33,7 @@ --accent: #4469D6; --accent-soft: rgba(68, 105, 214, 0.1); --focus: rgba(68, 105, 214, 0.42); - --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.04), 0 0 0 1px rgba(20, 20, 30, 0.055); + --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05), 0 0 0 1px rgba(20, 20, 30, 0.075); --shadow-md: 0 6px 20px -8px rgba(20, 20, 40, 0.14), 0 0 0 1px rgba(20, 20, 30, 0.06); --shadow-lg: 0 18px 44px -12px rgba(20, 20, 40, 0.22), 0 0 0 0.5px rgba(20, 20, 30, 0.09); --radius: 10px; @@ -45,15 +45,15 @@ --rail: #1E1E21; --rail-glass: rgba(34, 34, 37, 0.7); --surface: #222226; - --sunken: #2A2A2F; + --sunken: #2E2E34; --field: #1C1C1F; --hover: rgba(255, 255, 255, 0.04); --selected: rgba(255, 255, 255, 0.085); --line: rgba(255, 255, 255, 0.075); --line-strong: rgba(255, 255, 255, 0.14); --text: #EDEDEF; - --text-2: #B2B2BA; - --text-3: #86868E; + --text-2: #B8B8C0; + --text-3: #94949C; --ink: #EDEDEF; --on-ink: #1A1A1C; --live: #42B97A; @@ -89,7 +89,7 @@ button, input, select, textarea { font: inherit; color: inherit; } button { cursor: default; } :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 6px; } ::selection { background: color-mix(in srgb, var(--focus) 70%, transparent); } -code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 8%, transparent); } +code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 10%, transparent); white-space: nowrap; } a { color: inherit; } a.btn { text-decoration: none; } .icon { flex: none; display: block; } @@ -126,10 +126,14 @@ a.btn { text-decoration: none; } .brand .where { margin-left: auto; font: 500 10.5px var(--mono); color: var(--text-2); background: var(--hover); padding: 1px 6px; border-radius: 5px; } :root[data-shell="mac"] .brand { display: none; } -.rail-label { display: flex; align-items: center; justify-content: space-between; padding: 12px 8px 4px; font-size: 11px; font-weight: 600; color: var(--text-3); } +.rail-label { display: flex; align-items: center; justify-content: space-between; padding: 14px 8px 6px; font-size: 11.5px; font-weight: 600; color: var(--text-3); } .rail-label button { border: 0; background: none; color: var(--text-3); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; } .rail-label button:hover { background: var(--hover); color: var(--text); } .rail-label span { display: flex; gap: 2px; } +/* Pausing everything is a rare act: its button shows when you reach for the header. */ +.rail-label button[aria-label="Pause all bots"] { opacity: 0; transition: opacity 160ms ease; } +.rail-label:hover button, .rail-label:focus-within button { opacity: 1; } +@media (hover: none) { .rail-label button[aria-label="Pause all bots"] { opacity: 1; } } .rail-item { width: 100%; display: flex; align-items: center; gap: 9px; padding: 5px 8px; margin: 1px 0; @@ -138,9 +142,10 @@ a.btn { text-decoration: none; } } .rail-item:hover { background: var(--hover); } .rail-item.on { background: var(--selected); } +.rail-item.on .ri-name { font-weight: 600; } .rail-item .ri-text { flex: 1; min-width: 0; display: grid; } .rail-item .ri-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rail-item .ri-sub { font-size: 11px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rail-item .ri-sub { font-size: 11.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } .rail-item .ri-count { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .rail-item .ri-glyph { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); background: var(--hover); flex: none; } @@ -159,9 +164,10 @@ a.btn { text-decoration: none; } position: relative; z-index: 5; } .toolbar-title { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; } +.toolbar.bare { border-bottom-color: transparent; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; } .toolbar-title .tt { display: grid; min-width: 0; } .toolbar-title b { font-size: 13px; font-weight: 650; letter-spacing: -0.005em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.toolbar-title small { font-size: 11px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.toolbar-title small { font-size: 11.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .page { flex: 1; min-height: 0; overflow: auto; } .col { max-width: 760px; margin: 0 auto; padding: 28px 32px 64px; } @@ -180,7 +186,7 @@ a.btn { text-decoration: none; } /* ---------------- primitives ---------------- */ .btn { - height: 28px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; flex: none; + height: 30px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; flex: none; border-radius: 7px; border: 0.5px solid var(--line-strong); background: var(--surface); color: var(--text); font-size: 13px; font-weight: 500; box-shadow: var(--shadow-sm); white-space: nowrap; transition: transform 90ms ease, background-color 120ms ease, opacity 120ms ease; @@ -196,10 +202,10 @@ a.btn { text-decoration: none; } .btn-danger { color: var(--danger); } .btn-ghost.btn-danger { color: var(--text-3); } .btn-ghost.btn-danger:hover { background: var(--danger-soft); color: var(--danger); } -.btn-sm { height: 24px; padding: 0 9px; font-size: 12px; border-radius: 6px; } +.btn-sm { height: 26px; padding: 0 10px; font-size: 12.5px; border-radius: 7px; } .btn-lg { height: 34px; padding: 0 16px; font-size: 13.5px; border-radius: 9px; } -.icon-btn { width: 28px; padding: 0; justify-content: center; } -.btn-sm.icon-btn { width: 24px; } +.icon-btn { width: 30px; padding: 0; justify-content: center; } +.btn-sm.icon-btn { width: 26px; } .input, .textarea, .select { width: 100%; border: 0.5px solid var(--line-strong); background: var(--field); border-radius: 8px; @@ -214,7 +220,7 @@ a.btn { text-decoration: none; } .mono { font-family: var(--mono); } .field-label { display: grid; gap: 6px; } .field-label > span:first-child { font-size: 12px; font-weight: 600; color: var(--text-2); } -.hint { font-size: 12px; color: var(--text-3); line-height: 1.45; } +.hint { font-size: 12.5px; color: var(--text-3); line-height: 1.45; } .hint.warn { color: var(--pending); display: flex; gap: 6px; align-items: flex-start; } .connect-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .connect-wait { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-2); } @@ -223,7 +229,7 @@ a.btn { text-decoration: none; } .switch { appearance: none; -webkit-appearance: none; width: 32px; height: 19px; border-radius: 99px; position: relative; flex: none; - background: var(--line-strong); transition: background-color 160ms ease; margin: 0; + background: color-mix(in srgb, var(--text) 20%, transparent); transition: background-color 160ms ease; margin: 0; } .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; @@ -241,7 +247,7 @@ a.btn { text-decoration: none; } .segmented button:hover { color: var(--text); } .segmented button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } @media (prefers-color-scheme: dark) { .segmented button.on { background: #36363C; } } -.segmented.sm button { height: 22px; padding: 0 9px; font-size: 12px; } +.segmented.sm button { height: 26px; padding: 0 10px; font-size: 12.5px; } .badge { display: inline-flex; align-items: center; gap: 4px; height: 18px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; background: var(--hover); color: var(--text-2); white-space: nowrap; } .badge.live { background: var(--live-soft); color: var(--live); } @@ -273,17 +279,17 @@ a.btn { text-decoration: none; } } /* Inset grouped list, as in System Settings. */ -.group-wrap { margin: 0 0 26px; } -.group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 2px 8px; } +.group-wrap { margin: 0 0 32px; } +.group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 0 8px; } .group-head h3 { margin: 0; font: 600 15px/1.25 var(--serif); letter-spacing: -0.005em; color: var(--text); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } -.group-note { margin: 8px 2px 0; font-size: 12px; color: var(--text-3); line-height: 1.45; } +.group-note { margin: 8px 0 0; max-width: 600px; font-size: 12.5px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } .row-title { font-size: 13px; font-weight: 500; } -.row-desc { font-size: 12px; color: var(--text-3); margin-top: 1px; line-height: 1.4; } +.row-desc { font-size: 12.5px; color: var(--text-3); margin-top: 1px; line-height: 1.4; } .row-ctl { display: flex; align-items: center; gap: 8px; flex: none; } .row-block { padding: 12px 14px; display: grid; gap: 10px; } @@ -342,12 +348,14 @@ a.btn { text-decoration: none; } .thread { flex: 1; max-width: 760px; width: 100%; margin: 0 auto; padding: 28px 32px 12px; display: flex; flex-direction: column; gap: 18px; } .chat-search { position: sticky; top: 0; z-index: 3; max-width: 760px; width: 100%; margin: 0 auto; padding: 10px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } .chat-search .search { display: block; max-width: 240px; margin-left: auto; } -.chat-search .input { height: 28px; font-size: 12.5px; } +.chat-search .input { height: 28px; font-size: 12.5px; background: transparent; border-color: transparent; box-shadow: none; } +.chat-search .input:hover { background: var(--hover); } +.chat-search .input:focus { background: var(--field); } .chat-search .transcript { margin: 6px 0 0; max-height: 320px; box-shadow: var(--shadow-md); } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: center; max-width: 520px; padding: 10px 16px; border-radius: 12px; background: var(--hover); display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far { margin: 0; align-self: center; max-width: 520px; padding: 8px 14px; border-radius: 10px; border: 1px dashed var(--line-strong); display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } .so-far b { color: var(--text-2); font-weight: 600; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } @@ -535,8 +543,8 @@ a.btn { text-decoration: none; } .mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } @@ -552,7 +560,7 @@ a.btn { text-decoration: none; } .mem-check { margin: 8px 0 0; font-size: 12px; color: var(--text-2); } .rh .linkish, .mem-check .linkish, .routine-form .linkish { font: inherit; background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; color: inherit; } .mem-meta .inline-date::-webkit-calendar-picker-indicator { opacity: .45; margin-left: 2px; cursor: pointer; } -.incog-toggle { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 8px; background: none; color: var(--text-3); cursor: pointer; transition: background-color 120ms ease, color 120ms ease; } +.incog-toggle { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 8px; background: none; color: var(--text-2); cursor: pointer; transition: background-color 120ms ease, color 120ms ease; } .incog-toggle:hover { background: var(--hover); color: var(--text-2); } .incog-toggle.on { color: var(--text); background: var(--hover); } .composer.incog { background: color-mix(in srgb, var(--surface) 82%, var(--ink) 18%); } @@ -596,7 +604,7 @@ a.btn { text-decoration: none; } .routine { display: grid; grid-template-columns: 32px 1fr auto; gap: 12px; align-items: start; padding: 13px 14px; } .routine + .routine { border-top: 0.5px solid var(--line); } .routine .ri { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); } -.routine .ri.quiet { background: var(--pending-soft); color: var(--pending); } +.routine .ri.quiet { background: var(--hover); color: var(--text-2); } .routine .rt { font-size: 13.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; } .routine .rp { margin-top: 2px; font-size: 13px; color: var(--text-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .routine .rm { margin-top: 6px; display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-3); } @@ -606,7 +614,7 @@ a.btn { text-decoration: none; } .dot.missed { background: none; box-shadow: inset 0 0 0 1.5px var(--pending); } .routine-actions { display: flex; align-items: center; gap: 2px; align-self: center; } .routine-actions .switch { margin-right: 6px; } -.routine-actions .btn-ghost { color: var(--text-3); } +.routine-actions .btn-ghost { color: var(--text-2); } .routine-actions .btn-ghost:hover { color: var(--text); } .routine.paused .ri, .routine.paused .rt, .routine.paused .rp { opacity: .5; } .routine-inline { padding: 16px; } @@ -621,7 +629,7 @@ a.btn { text-decoration: none; } .presets.four { grid-template-columns: repeat(3, 1fr); } .presets .preset { align-content: start; } .preset.new .icon { color: var(--text-2); } -.presets.four .preset.new { grid-row: span 2; align-content: center; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } +.presets.four .preset.new { grid-row: span 2; align-content: start; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } .presets.four .preset.new:hover { background: var(--hover); box-shadow: none; } .routine-form { display: grid; gap: 14px; } .routine-form .segmented { justify-self: start; } @@ -674,7 +682,7 @@ a.btn { text-decoration: none; } .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .provider-cards:has(> :nth-child(8):last-child) { grid-template-columns: repeat(4, 1fr); } -.provider-card { text-align: left; padding: 10px 12px; border-radius: 10px; border: 0.5px solid var(--line-strong); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } +.provider-card { text-align: left; align-content: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 11.5px; color: var(--text-3); line-height: 1.35; } .templates .provider-card { align-content: start; } @@ -998,6 +1006,9 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .skill-diff .add { color: var(--ok, #2a8a4a); } .skill-diff .del { color: var(--danger); text-decoration: line-through; opacity: 0.8; } .teach { margin: 0 0 12px; } +.teach-empty { display: flex; align-items: center; gap: 16px; padding: 14px 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); } +.teach-empty > span { flex: 1; display: grid; gap: 2px; font-size: 12.5px; color: var(--text-3); } +.teach-empty b { font-size: 13.5px; font-weight: 600; color: var(--text); } .teach .er { display: flex; gap: 8px; align-items: center; } .teach .er .input { flex: 1; width: auto; } .standing { display: flex; align-items: center; gap: 10px; margin: 2px 0 12px; } @@ -1048,7 +1059,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .ctx-menu button { display: flex; align-items: center; gap: 8px; padding: 6px 9px; border: 0; border-radius: 6px; background: none; color: var(--text); font: inherit; font-size: 13px; text-align: left; cursor: default; } .ctx-menu button:hover, .ctx-menu button:focus-visible { background: var(--hover); outline: none; } .ctx-menu button svg { color: var(--text-2); } -.bot-settings { display: grid; place-items: center; width: 28px; height: 28px; flex: none; border: 0; border-radius: 8px; background: none; color: var(--text-3, var(--text-2)); } +.bot-settings { display: grid; place-items: center; width: 30px; height: 30px; flex: none; border: 0; border-radius: 8px; background: none; color: var(--text-2); } .bot-settings:hover, .bot-settings:focus-visible { background: var(--hover); color: var(--text); } .guarded.trouble { background: color-mix(in srgb, var(--danger-soft) 100%, var(--bg)); color: var(--text); } .guarded.trouble .icon { color: var(--danger); } diff --git a/ui/src/ui.tsx b/ui/src/ui.tsx index f306a27e..f397f7cc 100644 --- a/ui/src/ui.tsx +++ b/ui/src/ui.tsx @@ -50,6 +50,7 @@ const PATHS: Record = { incognito: 'M3 10.5h18M6 10.5l1.5-5.5h9l1.5 5.5M7.5 18.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm9 0a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM10 16h4', copy: 'M8.5 8.5V5A1.5 1.5 0 0 1 10 3.5h9A1.5 1.5 0 0 1 20.5 5v9a1.5 1.5 0 0 1-1.5 1.5h-3.5M5 8.5h9a1.5 1.5 0 0 1 1.5 1.5v9a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19v-9A1.5 1.5 0 0 1 5 8.5Z', pause: 'M9 5.5v13m6-13v13', + more: 'M6 12h.01M12 12h.01M18 12h.01', play: 'M7.5 5.5v13l11-6.5-11-6.5Z', retry:'M4.5 12a7.5 7.5 0 1 0 2.2-5.3L4.5 9m0-4.5V9H9', branch: 'M6.5 3.5v11m0 0a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Zm11-6a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm0 0c0 4.5-11 3-11 6', diff --git a/ui/src/views/bot.tsx b/ui/src/views/bot.tsx index 4a75699f..c5a473c3 100644 --- a/ui/src/views/bot.tsx +++ b/ui/src/views/bot.tsx @@ -244,7 +244,7 @@ export function BotPage({ bot, status, canDelete, onSaved, onDeleted, onChat, on {(b.provider === 'openai' || b.provider === 'local') && ( -
diff --git a/ui/src/views/chat.tsx b/ui/src/views/chat.tsx index d5d16fde..4c604176 100644 --- a/ui/src/views/chat.tsx +++ b/ui/src/views/chat.tsx @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom' import { createContext, Fragment, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react' import { copyText, inDesktop, invoke, platform } from '../api' import { moodOf, type Mood } from '../face' @@ -291,10 +292,12 @@ export function Menu({ at, name, items, onClose }: { at: { x: number; y: number else if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length]?.focus() } else if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length]?.focus() } } - return ( + // On the body: a view's entrance animation would otherwise make "fixed" mean fixed to that view. + return createPortal(
e.preventDefault()}> {items.map(([icon, label, run]) => )} -
+
, + document.body, ) } diff --git a/ui/src/views/routines.tsx b/ui/src/views/routines.tsx index 0619382b..9df0d5c6 100644 --- a/ui/src/views/routines.tsx +++ b/ui/src/views/routines.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react' import { inDesktop, invoke, remoteServer } from '../api' import { ahead, fullDay, fullTime, IDLE, schedule, tok, untilText, usd } from '../format' import { ask, Avatar, Empty, Icon, Segmented, Switch } from '../ui' -import { Paused, pausedText } from './chat' +import { Menu, Paused, pausedText } from './chat' import { RUN_DOT, skillSlug, useLoad, Bots, when, RehearsalResult } from '../shared' import type { Run, Delivery, Bot, Skill, Routine, Rehearsed } from '../types' @@ -207,6 +207,8 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] }) const [editing, setEditing] = useState<{ id: number; draft: RoutineDraft } | null>(null) const [runningId, setRunningId] = useState(null) const [rehearsal, setRehearsal] = useState<{ id: number; r: Rehearsed | null } | null>(null) + // A routine's less-used actions (rehearse, edit, delete), behind its ⋯ button. + const [more, setMore] = useState<{ x: number; y: number; id: number } | null>(null) const rehearse = (id: number) => { setRehearsal({ id, r: null }); invoke('rehearse', { chatId, routineId: id }).then(r => setRehearsal({ id, r }), e => { setRehearsal(null); setErr(String(e)) }) } const skillRows = useLoad(() => invoke('skills', { chatId }), [chatId]) const list = rows.data ?? [] @@ -253,7 +255,7 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] })
-
{when(r)}{r.followup && Follow-up}{r.for_goal && For the goal}{!!r.ends && Until {untilText(r.ends)}}{r.quiet && Quiet}{(r.paused || held !== undefined) ? Paused : r.goal_held && Paused (goal)}{r.delivery === 'digest' && In digest}{r.delivery === 'log' && Log only}{r.delivery === 'card' && On a card}{r.urgent && Urgent if…}{r.quiet && r.watch && Watching {r.watch === 'mail' ? 'mail' : (() => { try { return new URL(r.watch).host } catch { return r.watch } })()}}
+
{when(r)}{r.followup && Follow-up}{r.for_goal && For the goal}{!!r.ends && Until {untilText(r.ends)}}{r.quiet && Quiet}{(r.paused || held !== undefined) ? Paused : r.goal_held && Paused (goal)}{r.delivery === 'digest' && In digest}{r.delivery === 'log' && Log only}{r.delivery === 'card' && On a card}{r.urgent && Urgent if…}{r.quiet && r.watch && Watching {r.watch === 'mail' ? 'mail' : (() => { try { return new URL(r.watch).host } catch { return r.watch } })()}}
{r.prompt}
{b.name}·{r.paused ? 'Won’t run until you turn it back on' : held !== undefined ? `${pausedText(held)} with ${b.name}; ${r.once ? 'it runs once then' : 'runs in between are skipped, not caught up'}` : r.goal_held ? 'Waits while its goal is paused' : r.once && !r.cron && r.next_fire >= IDLE ? 'Running, or waits for you in Needs you' : r.next_fire >= IDLE ? `${r.cron === 'webhook' ? 'Waits for a call' : r.cron.startsWith('github:') ? 'Waits for a GitHub event' : r.cron.startsWith('mail:') ? 'Waits for a matching email' : r.cron.startsWith('slack:') ? 'Waits for a matching Slack message' : 'No matching event on the calendar this week'}${r.once ? ', then it’s gone' : ''}` : `${r.once ? 'Runs' : 'Next run'} ${ahead(r.next_fire)}${r.once ? ', then it’s gone' : ''}`}
{(r.cron === 'webhook' || r.cron.startsWith('github:')) && } @@ -262,15 +264,12 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] })
update(r, { paused: !on })} /> - - - {/* A one-off has no schedule to edit: change its words by asking the bot, or delete it. */} - {!r.once && } - +
) @@ -279,6 +278,12 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] }) )} + {more && (() => { const r = list.find(x => x.id === more.id); return r && setMore(null)} items={[ + ['play', 'Rehearse', () => { setMore(null); rehearse(r.id) }], + // A one-off has no schedule to edit: change its words by asking the bot, or delete it. + ...(r.once ? [] : [['pencil', 'Edit', () => { setMore(null); setEditing({ id: r.id, draft: { prompt: r.prompt, quiet: r.quiet, botId: r.bot_id, when: toWhen(r.cron), delivery: r.delivery ?? 'now', watch: r.watch ?? '', urgent: r.urgent ?? '', until: dayOf(r.ends) } }) }] as [string, string, () => void]]), + ['trash', 'Delete…', () => { setMore(null); void ask('Delete this routine?', 'Delete', true).then(ok => { if (ok) void invoke('delete_routine', { chatId, id: r.id }).then(rows.reload, e => setErr(String(e))) }) }], + ]} /> })()}

Add a routine

Anything a bot should do on a schedule. Quiet ones only message you when something needs your attention.

{draft ? setDraft(null)} onSave={add} /> : ( diff --git a/ui/src/views/skills.tsx b/ui/src/views/skills.tsx index 6400b49c..9c5500b6 100644 --- a/ui/src/views/skills.tsx +++ b/ui/src/views/skills.tsx @@ -136,7 +136,8 @@ function Skills({ chatId }: { chatId: number }) { } return ( <> -
+
+ {!active.length && !teaching && No skills of your own yetTeach one from a link or a few lines of text, or add one from the library below.} {teaching ?
{ e.preventDefault(); teach() }}> {bots.length > 1 && } setTeaching({ ...teaching, source: e.target.value })} /> From c7555e05e0945aba86073534b84fba8ea4a43c59 Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:40:54 +0100 Subject: [PATCH 05/38] UI polish: stable header across tabs, one button edge, 12px minimum metadata, even template grid Co-Authored-By: Claude Opus 5.5 --- ui/src/App.tsx | 3 ++- ui/src/index.css | 51 ++++++++++++++++++++------------------- ui/src/views/memory.tsx | 4 +-- ui/src/views/routines.tsx | 6 ++--- ui/src/views/skills.tsx | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 271e5411..6b44e50a 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -185,7 +185,8 @@ function Main() { // Clicking a bot opens a chat with it (on this computer); its settings are a right-click (or the gear by the // composer's bot picker) away. const talkTo = (id: number) => { setChatBot(id); setChatId(LOCAL); setIncognito(false); setViaBot(true); setView('chat'); setMenu(null) } - const botChat = viaBot && view === 'chat' && chat.chat_id === LOCAL + // Opened from a bot: it stays that bot's chat across the section tabs, so the header and sidebar don't jump. + const botChat = viaBot && inChat && chat.chat_id === LOCAL // A bot's right-click menu, or pause choices (for one bot, or all of them: id null). const [menu, setMenu] = useState<{ x: number; y: number; id: number | null; pick?: boolean } | null>(null) // A bot file being read before it's added (from a file, or a bot's own for Duplicate). diff --git a/ui/src/index.css b/ui/src/index.css index 7bb7ce58..0a3af7eb 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -89,7 +89,7 @@ button, input, select, textarea { font: inherit; color: inherit; } button { cursor: default; } :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 6px; } ::selection { background: color-mix(in srgb, var(--focus) 70%, transparent); } -code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 10%, transparent); white-space: nowrap; } +code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; } a { color: inherit; } a.btn { text-decoration: none; } .icon { flex: none; display: block; } @@ -145,7 +145,7 @@ a.btn { text-decoration: none; } .rail-item.on .ri-name { font-weight: 600; } .rail-item .ri-text { flex: 1; min-width: 0; display: grid; } .rail-item .ri-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rail-item .ri-sub { font-size: 11.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rail-item .ri-sub { font-size: 12px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } .rail-item .ri-count { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .rail-item .ri-glyph { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); background: var(--hover); flex: none; } @@ -167,7 +167,7 @@ a.btn { text-decoration: none; } .toolbar.bare { border-bottom-color: transparent; background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; } .toolbar-title .tt { display: grid; min-width: 0; } .toolbar-title b { font-size: 13px; font-weight: 650; letter-spacing: -0.005em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.toolbar-title small { font-size: 11.5px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.toolbar-title small { font-size: 12px; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .page { flex: 1; min-height: 0; overflow: auto; } .col { max-width: 760px; margin: 0 auto; padding: 28px 32px 64px; } @@ -187,8 +187,8 @@ a.btn { text-decoration: none; } .btn { height: 30px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; flex: none; - border-radius: 7px; border: 0.5px solid var(--line-strong); background: var(--surface); color: var(--text); - font-size: 13px; font-weight: 500; box-shadow: var(--shadow-sm); white-space: nowrap; + border-radius: 8px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--text); + font-size: 13px; font-weight: 500; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); white-space: nowrap; transition: transform 90ms ease, background-color 120ms ease, opacity 120ms ease; } .btn:hover { background: color-mix(in srgb, var(--surface) 92%, var(--text)); } @@ -220,7 +220,7 @@ a.btn { text-decoration: none; } .mono { font-family: var(--mono); } .field-label { display: grid; gap: 6px; } .field-label > span:first-child { font-size: 12px; font-weight: 600; color: var(--text-2); } -.hint { font-size: 12.5px; color: var(--text-3); line-height: 1.45; } +.hint { font-size: 13px; color: var(--text-3); line-height: 1.5; } .hint.warn { color: var(--pending); display: flex; gap: 6px; align-items: flex-start; } .connect-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .connect-wait { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-2); } @@ -229,7 +229,7 @@ a.btn { text-decoration: none; } .switch { appearance: none; -webkit-appearance: none; width: 32px; height: 19px; border-radius: 99px; position: relative; flex: none; - background: color-mix(in srgb, var(--text) 20%, transparent); transition: background-color 160ms ease; margin: 0; + background: color-mix(in srgb, var(--text) 24%, transparent); transition: background-color 160ms ease; margin: 0; } .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; @@ -283,13 +283,13 @@ a.btn { text-decoration: none; } .group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 0 8px; } .group-head h3 { margin: 0; font: 600 15px/1.25 var(--serif); letter-spacing: -0.005em; color: var(--text); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } -.group-note { margin: 8px 0 0; max-width: 600px; font-size: 12.5px; color: var(--text-3); line-height: 1.5; } +.group-note { margin: 8px 0 0; font-size: 13px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } .row-title { font-size: 13px; font-weight: 500; } -.row-desc { font-size: 12.5px; color: var(--text-3); margin-top: 1px; line-height: 1.4; } +.row-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.45; } .row-ctl { display: flex; align-items: center; gap: 8px; flex: none; } .row-block { padding: 12px 14px; display: grid; gap: 10px; } @@ -348,21 +348,21 @@ a.btn { text-decoration: none; } .thread { flex: 1; max-width: 760px; width: 100%; margin: 0 auto; padding: 28px 32px 12px; display: flex; flex-direction: column; gap: 18px; } .chat-search { position: sticky; top: 0; z-index: 3; max-width: 760px; width: 100%; margin: 0 auto; padding: 10px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } .chat-search .search { display: block; max-width: 240px; margin-left: auto; } -.chat-search .input { height: 28px; font-size: 12.5px; background: transparent; border-color: transparent; box-shadow: none; } +.chat-search .input { height: 28px; font-size: 12.5px; background: transparent; border-color: var(--line); box-shadow: none; } .chat-search .input:hover { background: var(--hover); } .chat-search .input:focus { background: var(--field); } .chat-search .transcript { margin: 6px 0 0; max-height: 320px; box-shadow: var(--shadow-md); } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: center; max-width: 520px; padding: 8px 14px; border-radius: 10px; border: 1px dashed var(--line-strong); display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far { margin: 0; align-self: center; max-width: 520px; padding: 8px 14px; border-radius: 10px; background: var(--hover); border: 1px solid var(--line); display: flex; flex-direction: column; align-items: center; gap: 2px; text-align: center; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } .so-far b { color: var(--text-2); font-weight: 600; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } .msg-bot { display: grid; grid-template-columns: 26px 1fr; gap: 11px; } .msg-head { display: flex; align-items: baseline; gap: 8px; margin: 3px 0 3px; } .msg-head b { font-size: 12.5px; font-weight: 650; color: var(--pig, var(--text)); } -.msg-head time { font-size: 11px; color: var(--text-3); } +.msg-head time { font-size: 12px; color: var(--text-3); } .msg-body { font-size: 14px; line-height: 1.6; word-wrap: break-word; } .sent-photo { display: block; max-width: min(100%, 420px); margin-top: 8px; border-radius: 8px; border: 1px solid var(--line); } .sent-file { display: inline-flex; align-items: center; gap: 6px; margin: 8px 6px 0 0; } @@ -503,7 +503,7 @@ a.btn { text-decoration: none; } .composer textarea { flex: 1; border: 0; background: none; resize: none; padding: 5px 4px; font-size: 14px; line-height: 1.45; max-height: 160px; outline: none; } .composer textarea::placeholder { color: var(--text-3); } .composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--ink); color: var(--on-ink); flex: none; transition: transform 90ms ease, opacity 120ms; } -.composer .send:disabled { opacity: 0.25; } +.composer .send:disabled { opacity: 0.32; } .composer .send:active { transform: scale(0.92); } .bot-pick { position: relative; flex: none; } .bot-pick select { position: absolute; inset: 0; opacity: 0; } @@ -543,16 +543,16 @@ a.btn { text-decoration: none; } .mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 3px 0 10px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } -.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } +.mem-body { margin: 3px 0 12px; font-size: 14px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } +.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .gone { color: var(--danger); } .mem-meta .warn { color: var(--pending); } @@ -601,18 +601,18 @@ a.btn { text-decoration: none; } /* ---------------- routines ---------------- */ -.routine { display: grid; grid-template-columns: 32px 1fr auto; gap: 12px; align-items: start; padding: 13px 14px; } +.routine { display: grid; grid-template-columns: 32px 1fr auto; gap: 12px 14px; align-items: start; padding: 13px 14px; } .routine + .routine { border-top: 0.5px solid var(--line); } .routine .ri { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); } .routine .ri.quiet { background: var(--hover); color: var(--text-2); } .routine .rt { font-size: 13.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; } -.routine .rp { margin-top: 2px; font-size: 13px; color: var(--text-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.routine .rm { margin-top: 6px; display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-3); } +.routine .rp { margin-top: 2px; font-size: 13.5px; color: var(--text-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); } .routine .rh { margin-top: 5px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: 11.5px; color: var(--text-3); } .routine .rh .bad { color: var(--danger); } .rh-dots { display: inline-flex; gap: 3px; } .dot.missed { background: none; box-shadow: inset 0 0 0 1.5px var(--pending); } -.routine-actions { display: flex; align-items: center; gap: 2px; align-self: center; } +.routine-actions { display: flex; align-items: center; gap: 2px; align-self: center; margin-left: 10px; } .routine-actions .switch { margin-right: 6px; } .routine-actions .btn-ghost { color: var(--text-2); } .routine-actions .btn-ghost:hover { color: var(--text); } @@ -626,7 +626,7 @@ a.btn { text-decoration: none; } .preset .icon { color: var(--text-2); margin-bottom: 4px; } .preset b { font-size: 13px; } .preset span { font-size: 12px; color: var(--text-3); } -.presets.four { grid-template-columns: repeat(3, 1fr); } +.presets.four { grid-template-columns: repeat(4, 1fr); } .presets .preset { align-content: start; } .preset.new .icon { color: var(--text-2); } .presets.four .preset.new { grid-row: span 2; align-content: start; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } @@ -660,7 +660,7 @@ a.btn { text-decoration: none; } .runrow + .run-detail, .run + .run { border-top: 0.5px solid var(--line); } .run-t { display: grid; min-width: 0; } .run-t b { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.run-t small { font-size: 11.5px; color: var(--text-3); } +.run-t small { font-size: 12px; color: var(--text-3); } .run-bot { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-2); } .run-bot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .num { font-size: 12px; color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; } @@ -682,7 +682,7 @@ a.btn { text-decoration: none; } .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .provider-cards:has(> :nth-child(8):last-child) { grid-template-columns: repeat(4, 1fr); } -.provider-card { text-align: left; align-content: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line); background: var(--field); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } +.provider-card { text-align: left; align-content: start; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line); background: transparent; display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 11.5px; color: var(--text-3); line-height: 1.35; } .templates .provider-card { align-content: start; } @@ -1053,6 +1053,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .link-real { opacity: 0.7; font-size: 0.86em; } .why.warn-line { color: var(--pending); display: flex; gap: 5px; align-items: center; } .clear-history { display: flex; gap: 10px; align-items: center; margin-top: 18px; } +.clear-history > .btn-ghost:first-child { margin-left: -10px; } /* A bot's right-click menu in the sidebar, and the gear by the composer's bot picker. */ .ctx-menu { position: fixed; z-index: 60; display: grid; gap: 2px; min-width: 190px; padding: 5px; border-radius: 10px; background: var(--surface); border: 0.5px solid var(--line-strong); box-shadow: var(--shadow-md, var(--shadow-sm)); animation: fade-in 120ms var(--ease-out) both; } @@ -1067,7 +1068,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .library { margin-top: 28px; } .library .section-title .select-inline { margin-left: auto; } .library .hint { margin: 0 0 6px; } -.lib-cat h3 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); margin: 22px 2px 8px; } +.lib-cat h3 { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 22px 0 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ .library .mem-actions.lib-add { opacity: 1; transform: none; } .file-text { white-space: pre-wrap; font: 12px var(--mono); margin: 0; } diff --git a/ui/src/views/memory.tsx b/ui/src/views/memory.tsx index 0a09130b..ed2cc208 100644 --- a/ui/src/views/memory.tsx +++ b/ui/src/views/memory.tsx @@ -72,7 +72,7 @@ export function MemoryView({ chatId, onChange }: { chatId: number; onChange: () options={[{ id: 'now', label: 'Current' }, { id: 'all', label: `History${inactive ? ` · ${inactive}` : ''}` }, { id: 'timeline', label: 'Timeline' }]} /> {/* The tidy-up reads a chat's conversations; the household has none. */} {chatId !== HOUSEHOLD && } - +
{(err || rows.error) &&

{err || rows.error}

} @@ -126,7 +126,7 @@ function TidyButton({ chatId, onDone }: { chatId: number; onDone: () => void }) ) } return ( - ) diff --git a/ui/src/views/routines.tsx b/ui/src/views/routines.tsx index 9df0d5c6..74e3ea08 100644 --- a/ui/src/views/routines.tsx +++ b/ui/src/views/routines.tsx @@ -284,13 +284,11 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] }) ...(r.once ? [] : [['pencil', 'Edit', () => { setMore(null); setEditing({ id: r.id, draft: { prompt: r.prompt, quiet: r.quiet, botId: r.bot_id, when: toWhen(r.cron), delivery: r.delivery ?? 'now', watch: r.watch ?? '', urgent: r.urgent ?? '', until: dayOf(r.ends) } }) }] as [string, string, () => void]]), ['trash', 'Delete…', () => { setMore(null); void ask('Delete this routine?', 'Delete', true).then(ok => { if (ok) void invoke('delete_routine', { chatId, id: r.id }).then(rows.reload, e => setErr(String(e))) }) }], ]} /> })()} -

Add a routine

+

Add a routine

+ {!draft && }

Anything a bot should do on a schedule. Quiet ones only message you when something needs your attention.

{draft ? setDraft(null)} onSave={add} /> : (
- {CHECKINS.map(c => ( - +
{(err || rows.error) &&

{err || rows.error}

} {standing.data && standing.data.count > 0 && (() => { @@ -489,7 +489,7 @@ function MemoryCard({ r, versions, act, chatId, showKind }: { r: EntryRow; versi {showVersions ? 'hide' : versions.length === 1 ? 'earlier version' : `${versions.length} earlier versions`}} {r.status === 'superseded' && replaced by #{r.superseded_by}} {r.status === 'forgotten' && forgotten} - #{r.id} + #{r.id}
{check.busy &&

Replaying past corrections…

} {check.note && ( diff --git a/ui/src/views/routines.tsx b/ui/src/views/routines.tsx index 74e3ea08..d2d57e04 100644 --- a/ui/src/views/routines.tsx +++ b/ui/src/views/routines.tsx @@ -257,7 +257,7 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] })
{when(r)}{r.followup && Follow-up}{r.for_goal && For the goal}{!!r.ends && Until {untilText(r.ends)}}{r.quiet && Quiet}{(r.paused || held !== undefined) ? Paused : r.goal_held && Paused (goal)}{r.delivery === 'digest' && In digest}{r.delivery === 'log' && Log only}{r.delivery === 'card' && On a card}{r.urgent && Urgent if…}{r.quiet && r.watch && Watching {r.watch === 'mail' ? 'mail' : (() => { try { return new URL(r.watch).host } catch { return r.watch } })()}}
{r.prompt}
-
{b.name}·{r.paused ? 'Won’t run until you turn it back on' : held !== undefined ? `${pausedText(held)} with ${b.name}; ${r.once ? 'it runs once then' : 'runs in between are skipped, not caught up'}` : r.goal_held ? 'Waits while its goal is paused' : r.once && !r.cron && r.next_fire >= IDLE ? 'Running, or waits for you in Needs you' : r.next_fire >= IDLE ? `${r.cron === 'webhook' ? 'Waits for a call' : r.cron.startsWith('github:') ? 'Waits for a GitHub event' : r.cron.startsWith('mail:') ? 'Waits for a matching email' : r.cron.startsWith('slack:') ? 'Waits for a matching Slack message' : 'No matching event on the calendar this week'}${r.once ? ', then it’s gone' : ''}` : `${r.once ? 'Runs' : 'Next run'} ${ahead(r.next_fire)}${r.once ? ', then it’s gone' : ''}`}
+
{b.name}·{r.paused ? 'Won’t run until you turn it back on' : held !== undefined ? `${pausedText(held)} with ${b.name}; ${r.once ? 'it runs once then' : 'runs in between are skipped, not caught up'}` : r.goal_held ? 'Waits while its goal is paused' : r.once && !r.cron && r.next_fire >= IDLE ? 'Running, or waits for you in Needs you' : r.next_fire >= IDLE ? `${r.cron === 'webhook' ? 'Waits for a call' : r.cron.startsWith('github:') ? 'Waits for a GitHub event' : r.cron.startsWith('mail:') ? 'Waits for a matching email' : r.cron.startsWith('slack:') ? 'Waits for a matching Slack message' : 'No matching event on the calendar this week'}${r.once ? ', then it’s gone' : ''}` : `${r.once ? 'Runs' : 'Next run'} ${ahead(r.next_fire)}${r.once ? ', then it’s gone' : ''}`}
{(r.cron === 'webhook' || r.cron.startsWith('github:')) && } x.id === r.id)} onReset={() => invoke('reset_watch_noise', { chatId, id: r.id }).then(stats.reload, e => setErr(String(e)))} /> {rehearsal?.id === r.id && (rehearsal.r ? setRehearsal(null)} /> :

Rehearsing… it runs for real, but anything it would send or change is only described.

)} @@ -285,7 +285,7 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] }) ['trash', 'Delete…', () => { setMore(null); void ask('Delete this routine?', 'Delete', true).then(ok => { if (ok) void invoke('delete_routine', { chatId, id: r.id }).then(rows.reload, e => setErr(String(e))) }) }], ]} /> })()}

Add a routine

- {!draft && }
+ {!draft && }

Anything a bot should do on a schedule. Quiet ones only message you when something needs your attention.

{draft ? setDraft(null)} onSave={add} /> : (
diff --git a/ui/src/views/skills.tsx b/ui/src/views/skills.tsx index 8b5b6c3b..29383ce3 100644 --- a/ui/src/views/skills.tsx +++ b/ui/src/views/skills.tsx @@ -93,8 +93,8 @@ function SkillLibrary({ chatId, onAdded }: { chatId: number; onAdded: () => void

{k.description}

{open === k.slug &&
}
- - {missing.length > 0 && needs {[...new Set(missing.map(t => NEED[t] ?? t))].join(', ')}} + + {missing.length > 0 && Needs {[...new Set(missing.map(t => NEED[t] ?? t))].join(', ')}} {/* A habit: once added, run it on a schedule in one click (a routine whose prompt is the skill's command). */} {has && k.schedule && (scheduled.includes(k.slug) ? scheduled {k.schedule.label.toLowerCase()} · change it in Routines From b9e7ab49e0436b053c85f0b0e38a9661d36b084e Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:46:09 +0100 Subject: [PATCH 07/38] UI polish: firmer dark cards, footnote helper text, accent send, key chip in settings search Co-Authored-By: Claude Opus 5.5 --- ui/src/App.tsx | 2 +- ui/src/index.css | 49 +++++++++++++++++++++------------------ ui/src/views/activity.tsx | 2 +- ui/src/views/memory.tsx | 4 ++-- ui/src/views/settings.tsx | 3 ++- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6b44e50a..dfc81fac 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -352,7 +352,7 @@ function Main() {
{botChat ? : } - {botChat ? botOf(chatBot ?? botList[0]?.id ?? 1).name : chatName(chat)}{ch.name} · {chat.memories} {chat.memories === 1 ? 'memory' : 'memories'}{household ? '' : ', shared by all your bots'}{chat.household && !household ? ' · reads the household' : ''}{!household && } + {botChat ? botOf(chatBot ?? botList[0]?.id ?? 1).name : chatName(chat)}{ch.name} · {chat.memories} {chat.memories === 1 ? 'memory' : 'memories'}{chat.household && !household ? ' · reads the household' : ''}{!household && } {!botChat && chat.chat_id !== LOCAL && !household && !chat.family && ( +
{(err || rows.error) &&

{err || rows.error}

} @@ -126,7 +126,7 @@ function TidyButton({ chatId, onDone }: { chatId: number; onDone: () => void }) ) } return ( - ) diff --git a/ui/src/views/settings.tsx b/ui/src/views/settings.tsx index 706ce4c4..2456410e 100644 --- a/ui/src/views/settings.tsx +++ b/ui/src/views/settings.tsx @@ -88,7 +88,8 @@ export function SettingsView({ onComputer, find = '' }: { onComputer: () => void <>

Settings

How your bots run, what they may do, and where they live.

{none && A bot's name, role, AI account, tools and its Telegram, Slack or Discord connection are on its own page: right-click the bot in the sidebar.} {!none && /telegram|slack|discord|role|persona|pair|bot token|model/i.test(q) &&

Each bot's Telegram, Slack and Discord connection, role and AI account are on its own page: right-click the bot in the sidebar.

} From 9305ddf3b53903b30f24970a6e1ea4ed6540a351 Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:49:35 +0100 Subject: [PATCH 08/38] UI polish: accent primaries, one button height, hairline account tiles, one paragraph measure Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 54 ++++++++++++++++++++++------------------- ui/src/views/memory.tsx | 2 +- ui/src/views/skills.tsx | 2 +- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index da79b71e..dabb98dd 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -31,6 +31,7 @@ --pending-soft: rgba(201, 134, 19, 0.12); --danger-soft: rgba(207, 62, 54, 0.09); --accent: #4469D6; + --accent-fill: #4469D6; --accent-soft: rgba(68, 105, 214, 0.1); --focus: rgba(68, 105, 214, 0.42); --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05), 0 0 0 1px rgba(20, 20, 30, 0.075); @@ -44,9 +45,9 @@ --bg: #171719; --rail: #1E1E21; --rail-glass: rgba(34, 34, 37, 0.7); - --surface: #25252A; - --sunken: #303036; - --field: #1A1A1D; + --surface: #27272C; + --sunken: #323238; + --field: #1B1B1E; --hover: rgba(255, 255, 255, 0.04); --selected: rgba(255, 255, 255, 0.085); --line: rgba(255, 255, 255, 0.075); @@ -56,16 +57,17 @@ --text-3: #9C9CA4; --ink: #EDEDEF; --on-ink: #1A1A1C; - --live: #42B97A; + --live: #3DAA72; --pending: #E0A23A; --danger: #EE6158; --live-soft: rgba(66, 185, 122, 0.14); --pending-soft: rgba(224, 162, 58, 0.14); --danger-soft: rgba(238, 97, 88, 0.13); --accent: #809FF2; + --accent-fill: #4D72DE; --accent-soft: rgba(128, 159, 242, 0.13); --focus: rgba(128, 159, 242, 0.5); - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.075), inset 0 1px 0 rgba(255, 255, 255, 0.03); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.085), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-lg: 0 22px 50px -12px rgba(0, 0, 0, 0.65), 0 0 0 0.5px rgba(255, 255, 255, 0.08); } @@ -89,7 +91,7 @@ button, input, select, textarea { font: inherit; color: inherit; } button { cursor: default; } :focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 6px; } ::selection { background: color-mix(in srgb, var(--focus) 70%, transparent); } -code { font-family: var(--mono); font-size: 0.92em; padding: 1px 5px; border-radius: 5px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; } +code { font-family: var(--mono); font-size: 0.88em; padding: 0 4px; border-radius: 5px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; } a { color: inherit; } a.btn { text-decoration: none; } .icon { flex: none; display: block; } @@ -157,7 +159,7 @@ a.btn { text-decoration: none; } .main { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--bg); position: relative; } .toolbar { - height: 52px; flex: none; display: flex; align-items: center; gap: 12px; padding: 0 16px 0 20px; + height: 52px; flex: none; display: flex; align-items: center; gap: 12px; padding: 0 20px; border-bottom: 0.5px solid var(--line); background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px); @@ -177,7 +179,7 @@ a.btn { text-decoration: none; } .page-head { display: flex; align-items: center; gap: 16px; margin: 4px 0 26px; } .display { margin: 0; font: 600 26px/1.15 var(--serif); letter-spacing: -0.018em; } -.lead { margin: 3px 0 0; color: var(--text-2); font-size: 13px; } +.lead { margin: 5px 0 0; max-width: 600px; color: var(--text-2); font-size: 13.5px; line-height: 1.5; } .section-title { display: flex; align-items: baseline; gap: 8px; margin: 28px 0 10px; } .section-title h2 { margin: 0; font: 600 16px/1.2 var(--serif); letter-spacing: -0.01em; } .section-title .count { font-size: 12px; font-weight: 500; color: var(--text-3); font-variant-numeric: tabular-nums; } @@ -195,17 +197,17 @@ a.btn { text-decoration: none; } .btn:active { transform: scale(0.97); } .btn:disabled { opacity: 0.45; transform: none; } .btn-primary:disabled { opacity: 1; background: var(--selected); color: var(--text-3); box-shadow: none; } -.btn-primary { background: var(--ink); color: var(--on-ink); border-color: transparent; } -.btn-primary:hover { background: color-mix(in srgb, var(--ink) 86%, var(--bg)); } +.btn-primary { background: var(--accent-fill); color: #fff; border-color: transparent; } +.btn-primary:hover { background: color-mix(in srgb, var(--accent-fill) 88%, #000); } .btn-ghost { background: none; border-color: transparent; box-shadow: none; color: var(--text-2); } .btn-ghost:hover { background: var(--hover); color: var(--text); } .btn-danger { color: var(--danger); } .btn-ghost.btn-danger { color: var(--text-3); } .btn-ghost.btn-danger:hover { background: var(--danger-soft); color: var(--danger); } -.btn-sm { height: 26px; padding: 0 10px; font-size: 12.5px; border-radius: 7px; } +.btn-sm { height: 30px; padding: 0 11px; font-size: 13px; border-radius: 8px; } .btn-lg { height: 34px; padding: 0 16px; font-size: 13.5px; border-radius: 9px; } .icon-btn { width: 30px; padding: 0; justify-content: center; } -.btn-sm.icon-btn { width: 26px; } +.btn-sm.icon-btn { width: 30px; } .input, .textarea, .select { width: 100%; border: 0.5px solid var(--line-strong); background: var(--field); border-radius: 8px; @@ -235,7 +237,7 @@ a.btn { text-decoration: none; } content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); transition: transform 180ms cubic-bezier(.3, .7, .3, 1); } -.switch:checked { background: var(--accent); } +.switch:checked { background: var(--accent-fill); } @media (prefers-color-scheme: dark) { .switch:not(:checked) { background: rgba(255, 255, 255, 0.24); } } .switch:checked::after { transform: translateX(13px); } .switch:disabled { opacity: 0.5; } @@ -255,7 +257,8 @@ a.btn { text-decoration: none; } .badge.live { background: var(--live-soft); color: var(--live); } .badge.pending { background: var(--pending-soft); color: var(--pending); } .badge.danger { background: var(--danger-soft); color: var(--danger); } -.badge.count { min-width: 18px; justify-content: center; background: #F2A33A; color: #2B1800; padding: 0 5px; } +.badge.needs::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--pending); } +.badge.count { min-width: 18px; justify-content: center; background: var(--accent-fill); color: #fff; padding: 0 5px; } .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-3); flex: none; } .dot.live { background: var(--live); } @@ -290,7 +293,7 @@ a.btn { text-decoration: none; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } -.row-title { font-size: 13px; font-weight: 500; } +.row-title { font-size: 13.5px; font-weight: 500; } .row-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.45; } .row-ctl { display: flex; align-items: center; gap: 8px; flex: none; } .row-block { padding: 12px 14px; display: grid; gap: 10px; } @@ -349,7 +352,7 @@ a.btn { text-decoration: none; } .chat { display: flex; flex-direction: column; min-height: 100%; } .thread { flex: 1; max-width: 760px; width: 100%; margin: 0 auto; padding: 18px 32px 12px; display: flex; flex-direction: column; gap: 18px; } -.chat-search { position: sticky; top: 0; z-index: 3; max-width: 760px; width: 100%; margin: 0 auto; padding: 10px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } +.chat-search { position: sticky; top: 0; z-index: 3; max-width: 760px; width: 100%; margin: 0 auto; padding: 16px 32px 0; background: linear-gradient(to top, transparent, var(--bg) 30%); } .chat-search .search { display: block; max-width: 240px; margin-left: auto; } .chat-search .input { height: 28px; font-size: 12.5px; background: transparent; border-color: var(--line); box-shadow: none; } .chat-search .input:hover { background: var(--hover); } @@ -505,12 +508,12 @@ a.btn { text-decoration: none; } .composer:focus-within { border-color: color-mix(in srgb, var(--text) 22%, transparent); } .composer textarea { flex: 1; border: 0; background: none; resize: none; padding: 5px 4px; font-size: 14px; line-height: 1.45; max-height: 160px; outline: none; } .composer textarea::placeholder { color: var(--text-3); } -.composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--accent); color: #fff; flex: none; transition: transform 90ms ease, opacity 120ms; } +.composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--accent-fill); color: #fff; flex: none; transition: transform 90ms ease, opacity 120ms; } .composer .send:disabled { opacity: 1; background: var(--selected); color: var(--text-3); } .composer .send:active { transform: scale(0.92); } .bot-pick { position: relative; flex: none; } .bot-pick select { position: absolute; inset: 0; opacity: 0; } -.bot-pick .avatar { margin: 1px; } +.bot-pick .avatar { margin: 3px 1px -1px; } .attach-row { max-width: 696px; margin: 0 auto 8px; display: flex; gap: 8px; } .attach { position: relative; width: 60px; height: 60px; border-radius: 12px; overflow: hidden; box-shadow: var(--shadow-sm); animation: pop 340ms var(--spring-soft) both; } .attach img { width: 100%; height: 100%; object-fit: cover; display: block; } @@ -547,15 +550,15 @@ a.btn { text-decoration: none; } .mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 4px 0 12px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } +.mem-body { margin: 4px 0 8px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } .mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .mem-id { opacity: .7; } @@ -610,7 +613,7 @@ a.btn { text-decoration: none; } .routine + .routine { border-top: 0.5px solid var(--line); } .routine .ri { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); } .routine .ri.quiet { background: var(--hover); color: var(--text-2); } -.routine .rt { font-size: 13.5px; font-weight: 600; display: flex; align-items: center; gap: 8px; } +.routine .rt { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } .routine .rp { margin-top: 2px; font-size: 13.5px; color: var(--text-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); } .routine .rh { margin-top: 5px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: 11.5px; color: var(--text-3); } @@ -635,6 +638,7 @@ a.btn { text-decoration: none; } .preset span { font-size: 12px; color: var(--text-3); } .presets.four { grid-template-columns: repeat(4, 1fr); } .presets .preset { align-content: start; } +.presets.four .preset { align-content: center; } .preset.new .icon { color: var(--text-2); } .presets.four .preset.new { grid-row: span 2; align-content: start; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } .presets.four .preset.new:hover { background: var(--hover); box-shadow: none; } @@ -689,7 +693,7 @@ a.btn { text-decoration: none; } .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .provider-cards:has(> :nth-child(8):last-child) { grid-template-columns: repeat(4, 1fr); } -.provider-card { text-align: left; align-content: start; padding: 11px 12px 12px; border-radius: 10px; border: 1px solid transparent; background: var(--hover); display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } +.provider-card { text-align: left; align-content: start; padding: 11px 12px 12px; border-radius: 10px; border: 1px solid var(--line); background: none; display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 12px; color: var(--text-2); line-height: 1.4; text-wrap: pretty; } .templates .provider-card { align-content: start; } @@ -700,7 +704,7 @@ a.btn { text-decoration: none; } .starter-routine .textarea { margin-top: 6px; min-height: 0; } .starter-routine .textarea:disabled { opacity: .5; } .provider-card.on { border-color: color-mix(in srgb, var(--accent) 75%, transparent); background: var(--accent-soft); box-shadow: none; } -.provider-card:hover:not(.on) { background: var(--selected); } +.provider-card:hover:not(.on) { background: var(--hover); } .chan-row .row-icon { color: var(--text); } .account-test { display: flex; align-items: center; gap: 10px; min-height: 24px; } .account-test .typing { padding: 0; } @@ -1074,7 +1078,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .guarded.trouble .icon { color: var(--danger); } .page-head .head-action { margin-left: auto; align-self: center; display: inline-flex; align-items: center; gap: 6px; } .library { margin-top: 28px; } -.library .section-title .select-inline { margin-left: auto; } +.library .section-title .select { margin-left: auto; } .library .hint { margin: 0 0 6px; max-width: 600px; font-size: 12.5px; } .lib-cat h3 { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 22px 0 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ diff --git a/ui/src/views/memory.tsx b/ui/src/views/memory.tsx index 6653f985..2b90977e 100644 --- a/ui/src/views/memory.tsx +++ b/ui/src/views/memory.tsx @@ -72,7 +72,7 @@ export function MemoryView({ chatId, onChange }: { chatId: number; onChange: () options={[{ id: 'now', label: 'Current' }, { id: 'all', label: `History${inactive ? ` · ${inactive}` : ''}` }, { id: 'timeline', label: 'Timeline' }]} /> {/* The tidy-up reads a chat's conversations; the household has none. */} {chatId !== HOUSEHOLD && } - +
{(err || rows.error) &&

{err || rows.error}

} diff --git a/ui/src/views/skills.tsx b/ui/src/views/skills.tsx index 29383ce3..af09bbf3 100644 --- a/ui/src/views/skills.tsx +++ b/ui/src/views/skills.tsx @@ -94,7 +94,7 @@ function SkillLibrary({ chatId, onAdded }: { chatId: number; onAdded: () => void {open === k.slug &&
}
- {missing.length > 0 && Needs {[...new Set(missing.map(t => NEED[t] ?? t))].join(', ')}} + {missing.length > 0 && Needs {[...new Set(missing.map(t => NEED[t] ?? t))].join(', ')}} {/* A habit: once added, run it on a schedule in one click (a routine whose prompt is the skill's command). */} {has && k.schedule && (scheduled.includes(k.slug) ? scheduled {k.schedule.label.toLowerCase()} · change it in Routines From 7fc56f4134be5f2ddc88fbc917abd1424e7b8c0b Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:52:59 +0100 Subject: [PATCH 09/38] UI polish: visible dark card edges, one secondary button style, task-first routine rows, growing text boxes Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 50 +++++++++++++++++++++------------------ ui/src/views/memory.tsx | 4 ++-- ui/src/views/routines.tsx | 2 +- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index dabb98dd..da12eb78 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -20,8 +20,8 @@ --line: rgba(24, 24, 32, 0.085); --line-strong: rgba(24, 24, 32, 0.19); --text: #1C1C1E; - --text-2: #4F4F56; - --text-3: #74747C; + --text-2: #4C4C53; + --text-3: #6B6B73; --ink: #1C1C1E; --on-ink: #FFFFFF; --live: #2E9D62; @@ -45,16 +45,16 @@ --bg: #171719; --rail: #1E1E21; --rail-glass: rgba(34, 34, 37, 0.7); - --surface: #27272C; - --sunken: #323238; - --field: #1B1B1E; + --surface: #222226; + --sunken: #2F2F35; + --field: #1A1A1D; --hover: rgba(255, 255, 255, 0.04); --selected: rgba(255, 255, 255, 0.085); --line: rgba(255, 255, 255, 0.075); --line-strong: rgba(255, 255, 255, 0.14); --text: #EDEDEF; - --text-2: #C0C0C8; - --text-3: #9C9CA4; + --text-2: #C2C2CA; + --text-3: #A3A3AB; --ink: #EDEDEF; --on-ink: #1A1A1C; --live: #3DAA72; @@ -67,7 +67,7 @@ --accent-fill: #4D72DE; --accent-soft: rgba(128, 159, 242, 0.13); --focus: rgba(128, 159, 242, 0.5); - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.085), inset 0 1px 0 rgba(255, 255, 255, 0.03); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.11), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-md: 0 8px 24px -8px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.07), inset 0 1px 0 rgba(255, 255, 255, 0.03); --shadow-lg: 0 22px 50px -12px rgba(0, 0, 0, 0.65), 0 0 0 0.5px rgba(255, 255, 255, 0.08); } @@ -149,9 +149,9 @@ a.btn { text-decoration: none; } .rail-item .ri-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub { font-size: 12px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } -.rail-item .ri-count { font-size: 11.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } +.rail-item .ri-count { font-size: 11px; color: var(--text-3); opacity: .8; font-variant-numeric: tabular-nums; } .rail-item .ri-glyph { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); background: var(--hover); flex: none; } -.rail-hint { padding: 6px 8px; font-size: 12px; color: var(--text-3); line-height: 1.4; } +.rail-hint { padding: 6px 8px; text-wrap: pretty; font-size: 12px; color: var(--text-3); line-height: 1.4; } .rail-foot { padding: 8px 10px 12px; border-top: 0.5px solid var(--line); } .rail-foot .rail-item { color: var(--text-2); } .rail-foot .badge { margin-left: auto; } @@ -238,7 +238,7 @@ a.btn { text-decoration: none; } background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); transition: transform 180ms cubic-bezier(.3, .7, .3, 1); } .switch:checked { background: var(--accent-fill); } -@media (prefers-color-scheme: dark) { .switch:not(:checked) { background: rgba(255, 255, 255, 0.24); } } +@media (prefers-color-scheme: dark) { .switch:not(:checked) { background: rgba(255, 255, 255, 0.3); } } .switch:checked::after { transform: translateX(13px); } .switch:disabled { opacity: 0.5; } @@ -291,7 +291,7 @@ a.btn { text-decoration: none; } .group-note { margin: 8px 0 0; max-width: 600px; font-size: 12.5px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } -.row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); flex: none; } +.row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--selected); color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } .row-title { font-size: 13.5px; font-weight: 500; } .row-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.45; } @@ -361,7 +361,7 @@ a.btn { text-decoration: none; } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: stretch; padding: 9px 14px; border-radius: 10px; background: var(--hover); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far { margin: 0; align-self: stretch; padding: 9px 14px; border-radius: 10px; background: var(--hover); box-shadow: inset 0 0 0 1px var(--line); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } .so-far b { color: var(--text-2); font-weight: 600; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } @@ -509,7 +509,7 @@ a.btn { text-decoration: none; } .composer textarea { flex: 1; border: 0; background: none; resize: none; padding: 5px 4px; font-size: 14px; line-height: 1.45; max-height: 160px; outline: none; } .composer textarea::placeholder { color: var(--text-3); } .composer .send { width: 30px; height: 30px; border-radius: 50%; border: 0; display: grid; place-items: center; background: var(--accent-fill); color: #fff; flex: none; transition: transform 90ms ease, opacity 120ms; } -.composer .send:disabled { opacity: 1; background: var(--selected); color: var(--text-3); } +.composer .send:disabled { opacity: 1; background: var(--line-strong); color: var(--text-3); } .composer .send:active { transform: scale(0.92); } .bot-pick { position: relative; flex: none; } .bot-pick select { position: absolute; inset: 0; opacity: 0; } @@ -530,7 +530,7 @@ a.btn { text-decoration: none; } .search { position: relative; flex: 1; } .search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-3); } .search .input { padding-left: 32px; border-radius: 9px; } -.settings-search { display: block; margin: -6px 0 22px; } +.settings-search { display: block; margin: -6px 0 32px; } .search-kbd { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); pointer-events: none; padding: 1px 6px; font-size: 11px; } .group-wrap[hidden], .group > [hidden] { display: none !important; } @@ -550,8 +550,8 @@ a.btn { text-decoration: none; } .mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } @@ -561,7 +561,7 @@ a.btn { text-decoration: none; } .mem-body { margin: 4px 0 8px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } .mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } -.mem-meta .mem-id { opacity: .7; } +.mem-meta .mem-id { opacity: .6; } .mem-meta .gone { color: var(--danger); } .mem-meta .warn { color: var(--pending); } .mem-meta .inline-date { font: inherit; color: inherit; background: none; border: 0; padding: 0; } @@ -613,8 +613,8 @@ a.btn { text-decoration: none; } .routine + .routine { border-top: 0.5px solid var(--line); } .routine .ri { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); } .routine .ri.quiet { background: var(--hover); color: var(--text-2); } -.routine .rt { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; } -.routine .rp { margin-top: 2px; font-size: 13.5px; color: var(--text-2); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.routine .rt { font-size: 13px; font-weight: 600; color: var(--text-2); display: flex; align-items: center; gap: 8px; } +.routine .rp { margin-top: 2px; font-size: 14px; color: var(--text); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); } .routine .rh { margin-top: 5px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: 11.5px; color: var(--text-3); } .routine .rh .bad { color: var(--danger); } @@ -624,7 +624,7 @@ a.btn { text-decoration: none; } .routine-actions .switch { margin-right: 6px; } .routine-actions .btn-ghost { color: var(--text-2); } .routine-actions .icon-btn { color: var(--text); } -.routine-actions .icon-btn svg { stroke-width: 2.8; } +.routine-actions .icon-btn svg { stroke-width: 3; } .routine-actions .btn-ghost:hover { color: var(--text); } .routine.paused .ri, .routine.paused .rt, .routine.paused .rp { opacity: .5; } .routine-inline { padding: 16px; } @@ -638,7 +638,7 @@ a.btn { text-decoration: none; } .preset span { font-size: 12px; color: var(--text-3); } .presets.four { grid-template-columns: repeat(4, 1fr); } .presets .preset { align-content: start; } -.presets.four .preset { align-content: center; } + .preset.new .icon { color: var(--text-2); } .presets.four .preset.new { grid-row: span 2; align-content: start; background: none; box-shadow: none; border: 1px dashed var(--line-strong); } .presets.four .preset.new:hover { background: var(--hover); box-shadow: none; } @@ -660,7 +660,7 @@ a.btn { text-decoration: none; } .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 22px; } .stat { padding: 14px 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); } .stat dt { font-size: 12px; color: var(--text-2); font-weight: 500; } -.stat dd { margin: 6px 0 0; font: 600 24px/1.1 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } +.stat dd { margin: 6px 0 0; font: 600 28px/1.1 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .stat dd small { font-size: 18px; font-weight: 500; color: var(--text-3); letter-spacing: -0.01em; } .stat dd small.bad { color: var(--danger); } .stat p { margin: 4px 0 0; font-size: 11px; color: var(--text-3); line-height: 1.35; } @@ -1083,6 +1083,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .lib-cat h3 { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 22px 0 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ .library .mem-actions.lib-add { opacity: 1; transform: none; } +.library .mem .mem-body { padding-right: 72px; } .file-text { white-space: pre-wrap; font: 12px var(--mono); margin: 0; } .toolbar .guest-on { color: var(--pending); background: var(--pending-soft); } @@ -1119,3 +1120,6 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p /* The settings search is focused on arrival: a quiet edge, not the full glow (that's for typing elsewhere). */ .settings-search .input:focus { border-color: var(--line-strong); box-shadow: none; } + +/* Where the engine can, a text box grows with what's in it (no resize grip). */ +@supports (field-sizing: content) { .textarea { field-sizing: content; resize: none; } } diff --git a/ui/src/views/memory.tsx b/ui/src/views/memory.tsx index 2b90977e..87c325ba 100644 --- a/ui/src/views/memory.tsx +++ b/ui/src/views/memory.tsx @@ -72,7 +72,7 @@ export function MemoryView({ chatId, onChange }: { chatId: number; onChange: () options={[{ id: 'now', label: 'Current' }, { id: 'all', label: `History${inactive ? ` · ${inactive}` : ''}` }, { id: 'timeline', label: 'Timeline' }]} /> {/* The tidy-up reads a chat's conversations; the household has none. */} {chatId !== HOUSEHOLD && } - +
{(err || rows.error) &&

{err || rows.error}

} @@ -126,7 +126,7 @@ function TidyButton({ chatId, onDone }: { chatId: number; onDone: () => void }) ) } return ( - ) diff --git a/ui/src/views/routines.tsx b/ui/src/views/routines.tsx index d2d57e04..28046b87 100644 --- a/ui/src/views/routines.tsx +++ b/ui/src/views/routines.tsx @@ -264,7 +264,7 @@ export function RoutinesView({ chatId, bots }: { chatId: number; bots: Bot[] })
update(r, { paused: !on })} /> - From 1b5057e23cb9438a4ba54332ab8f59de37eac49f Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 20:56:05 +0100 Subject: [PATCH 10/38] UI polish: account tiles on the page, flat templates, dotted memory meta, summary as a strip Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index da12eb78..f3f65e51 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -34,7 +34,7 @@ --accent-fill: #4469D6; --accent-soft: rgba(68, 105, 214, 0.1); --focus: rgba(68, 105, 214, 0.42); - --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05), 0 0 0 1px rgba(20, 20, 30, 0.075); + --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05), 0 0 0 1px rgba(20, 20, 30, 0.09); --shadow-md: 0 6px 20px -8px rgba(20, 20, 40, 0.14), 0 0 0 1px rgba(20, 20, 30, 0.06); --shadow-lg: 0 18px 44px -12px rgba(20, 20, 40, 0.22), 0 0 0 0.5px rgba(20, 20, 30, 0.09); --radius: 10px; @@ -128,7 +128,7 @@ a.btn { text-decoration: none; } .brand .where { margin-left: auto; font: 500 10.5px var(--mono); color: var(--text-2); background: var(--hover); padding: 1px 6px; border-radius: 5px; } :root[data-shell="mac"] .brand { display: none; } -.rail-label { display: flex; align-items: center; justify-content: space-between; padding: 14px 8px 6px; font-size: 11.5px; font-weight: 600; color: var(--text-3); } +.rail-label { display: flex; align-items: center; justify-content: space-between; padding: 20px 8px 6px; font-size: 11.5px; font-weight: 600; color: var(--text-3); } .rail-label button { border: 0; background: none; color: var(--text-3); width: 20px; height: 20px; border-radius: 5px; display: grid; place-items: center; } .rail-label button:hover { background: var(--hover); color: var(--text); } .rail-label span { display: flex; gap: 2px; } @@ -146,12 +146,12 @@ a.btn { text-decoration: none; } .rail-item.on { background: var(--selected); } .rail-item.on .ri-name { font-weight: 600; } .rail-item .ri-text { flex: 1; min-width: 0; display: grid; } -.rail-item .ri-name { font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.rail-item .ri-sub { font-size: 12px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rail-item .ri-name { font-size: 13.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rail-item .ri-sub { font-size: 12.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } .rail-item .ri-count { font-size: 11px; color: var(--text-3); opacity: .8; font-variant-numeric: tabular-nums; } .rail-item .ri-glyph { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); background: var(--hover); flex: none; } -.rail-hint { padding: 6px 8px; text-wrap: pretty; font-size: 12px; color: var(--text-3); line-height: 1.4; } +.rail-hint { padding: 6px 8px; text-wrap: pretty; font-size: 12px; opacity: .8; color: var(--text-3); line-height: 1.4; } .rail-foot { padding: 8px 10px 12px; border-top: 0.5px solid var(--line); } .rail-foot .rail-item { color: var(--text-2); } .rail-foot .badge { margin-left: auto; } @@ -215,7 +215,7 @@ a.btn { text-decoration: none; } transition: box-shadow 120ms ease, border-color 120ms ease; } .input { height: 30px; } -.textarea { min-height: 72px; resize: vertical; line-height: 1.5; } +.textarea { min-height: 60px; resize: vertical; line-height: 1.5; } .input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .input::placeholder, .textarea::placeholder { color: var(--text-3); } .select { height: 30px; appearance: none; padding-right: 26px; background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%); background-position: calc(100% - 13px) 13px, calc(100% - 9px) 13px; background-size: 4px 4px; background-repeat: no-repeat; } @@ -291,7 +291,7 @@ a.btn { text-decoration: none; } .group-note { margin: 8px 0 0; max-width: 600px; font-size: 12.5px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } -.row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: var(--selected); color: var(--text-2); flex: none; } +.row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: none; color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } .row-title { font-size: 13.5px; font-weight: 500; } .row-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.45; } @@ -361,7 +361,7 @@ a.btn { text-decoration: none; } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: stretch; padding: 9px 14px; border-radius: 10px; background: var(--hover); box-shadow: inset 0 0 0 1px var(--line); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far { margin: 0; align-self: stretch; padding: 2px 0 2px 12px; border-left: 2px solid var(--line-strong); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } .so-far b { color: var(--text-2); font-weight: 600; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } @@ -527,6 +527,7 @@ a.btn { text-decoration: none; } /* ---------------- memory ---------------- */ .searchbar { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } +.searchbar .segmented { margin-right: 8px; } .search { position: relative; flex: 1; } .search .icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-3); } .search .input { padding-left: 32px; border-radius: 9px; } @@ -559,7 +560,7 @@ a.btn { text-decoration: none; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } .mem-body { margin: 4px 0 8px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } -.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12px; color: var(--text-3); font-variant-numeric: tabular-nums; } +.mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .mem-id { opacity: .6; } .mem-meta .gone { color: var(--danger); } @@ -615,7 +616,7 @@ a.btn { text-decoration: none; } .routine .ri.quiet { background: var(--hover); color: var(--text-2); } .routine .rt { font-size: 13px; font-weight: 600; color: var(--text-2); display: flex; align-items: center; gap: 8px; } .routine .rp { margin-top: 2px; font-size: 14px; color: var(--text); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-3); } +.routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-3); } .routine .rh { margin-top: 5px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: 11.5px; color: var(--text-3); } .routine .rh .bad { color: var(--danger); } .rh-dots { display: inline-flex; gap: 3px; } @@ -630,8 +631,8 @@ a.btn { text-decoration: none; } .routine-inline { padding: 16px; } .routine + .routine-inline, .routine-inline + .routine { border-top: 0.5px solid var(--line); } .presets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } -.preset { text-align: left; padding: 14px; border-radius: 12px; border: 0; background: var(--surface); box-shadow: var(--shadow-sm); display: grid; gap: 4px; transition: box-shadow 160ms ease, transform 90ms ease; } -.preset:hover { box-shadow: var(--shadow-md); } +.preset { text-align: left; padding: 14px; border-radius: 12px; border: 0; background: none; box-shadow: inset 0 0 0 1px var(--line-strong); display: grid; gap: 4px; transition: box-shadow 160ms ease, transform 90ms ease; } +.preset:hover { background: var(--hover); } .preset:active { transform: scale(0.985); } .preset .icon { color: var(--text-2); margin-bottom: 4px; } .preset b { font-size: 13px; } @@ -671,7 +672,7 @@ a.btn { text-decoration: none; } .runrow + .run-detail, .run + .run { border-top: 0.5px solid var(--line); } .run-t { display: grid; min-width: 0; } .run-t b { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.run-t small { font-size: 12px; color: var(--text-3); } +.run-t small { font-size: 12.5px; color: var(--text-3); } .run-bot { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-2); } .run-bot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .num { font-size: 12px; color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; } @@ -693,6 +694,8 @@ a.btn { text-decoration: none; } .provider-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .provider-cards:has(> :nth-child(8):last-child) { grid-template-columns: repeat(4, 1fr); } +.group:has(> .row-block > .provider-cards:not(.templates)) { background: none; box-shadow: none; overflow: visible; } +.group:has(> .row-block > .provider-cards:not(.templates)) > .row-block { padding: 0; gap: 14px; } .provider-card { text-align: left; align-content: start; padding: 11px 12px 12px; border-radius: 10px; border: 1px solid var(--line); background: none; display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } .provider-card b { font-size: 13px; } .provider-card span { font-size: 12px; color: var(--text-2); line-height: 1.4; text-wrap: pretty; } @@ -703,7 +706,7 @@ a.btn { text-decoration: none; } .starter-routine .row-title { display: flex; align-items: center; gap: 6px; } .starter-routine .textarea { margin-top: 6px; min-height: 0; } .starter-routine .textarea:disabled { opacity: .5; } -.provider-card.on { border-color: color-mix(in srgb, var(--accent) 75%, transparent); background: var(--accent-soft); box-shadow: none; } +.provider-card.on { border-color: color-mix(in srgb, var(--accent) 75%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent); box-shadow: none; } .provider-card:hover:not(.on) { background: var(--hover); } .chan-row .row-icon { color: var(--text); } .account-test { display: flex; align-items: center; gap: 10px; min-height: 24px; } @@ -897,7 +900,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .preset, .account, .provider-card, .starter, .chip, .mem, .stat { transition: transform 380ms var(--spring), box-shadow 240ms ease, border-color 200ms ease, color 160ms ease; } -.preset:hover, .account:hover, .starter:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); } +.account:hover, .starter:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); } .provider-card:hover { transform: translateY(-1px); } .preset:active, .account:active, .provider-card:active, .starter:active, .chip:active { transform: scale(0.975); transition-duration: 100ms; } .rail-item { transition: background-color 160ms ease, transform 120ms ease; } @@ -1079,7 +1082,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .page-head .head-action { margin-left: auto; align-self: center; display: inline-flex; align-items: center; gap: 6px; } .library { margin-top: 28px; } .library .section-title .select { margin-left: auto; } -.library .hint { margin: 0 0 6px; max-width: 600px; font-size: 12.5px; } +.library .hint { margin: 0 0 6px; max-width: 600px; font-size: 13.5px; color: var(--text-2); } .lib-cat h3 { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 22px 0 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ .library .mem-actions.lib-add { opacity: 1; transform: none; } @@ -1123,3 +1126,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p /* Where the engine can, a text box grows with what's in it (no resize grip). */ @supports (field-sizing: content) { .textarea { field-sizing: content; resize: none; } } + +/* Memory's meta line: one dot between facts, as on routines and activity. */ +.mem-meta > * + :not(.badge)::before { content: "·"; margin: 0 7px; color: var(--text-3); opacity: .7; display: inline-block; text-decoration: none; } +.mem-meta > * + .badge { margin-left: 10px; } From 3cf5ed2e43002f29c9bf280d2a208f4ca5db80cb Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 21:00:46 +0100 Subject: [PATCH 11/38] UI polish: a comfortable reading scale (14px text, 32px controls), one icon column, compact memory rows Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index f3f65e51..a276fd32 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -9,7 +9,7 @@ --serif: ui-serif, "New York", "Iowan Old Style", "Charter", Cambria, "Noto Serif", Georgia, serif; --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, "Cascadia Mono", Consolas, "JetBrains Mono", "DejaVu Sans Mono", monospace; - --bg: #FBFBFA; + --bg: #F8F8F6; --rail: #F0F0EE; --rail-glass: rgba(236, 236, 234, 0.74); --surface: #FFFFFF; @@ -78,7 +78,7 @@ html, body, #root { height: 100%; margin: 0; } body { background: var(--bg); color: var(--text); - font: 13px/1.45 var(--sans); + font: 14px/1.45 var(--sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; user-select: none; @@ -145,8 +145,8 @@ a.btn { text-decoration: none; } .rail-item:hover { background: var(--hover); } .rail-item.on { background: var(--selected); } .rail-item.on .ri-name { font-weight: 600; } -.rail-item .ri-text { flex: 1; min-width: 0; display: grid; } -.rail-item .ri-name { font-size: 13.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rail-item .ri-text { flex: 1; min-width: 0; display: grid; gap: 1px; } +.rail-item .ri-name { font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub { font-size: 12.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rail-item .ri-sub.busy { color: var(--text-2); } .rail-item .ri-count { font-size: 11px; color: var(--text-3); opacity: .8; font-variant-numeric: tabular-nums; } @@ -179,7 +179,7 @@ a.btn { text-decoration: none; } .page-head { display: flex; align-items: center; gap: 16px; margin: 4px 0 26px; } .display { margin: 0; font: 600 26px/1.15 var(--serif); letter-spacing: -0.018em; } -.lead { margin: 5px 0 0; max-width: 600px; color: var(--text-2); font-size: 13.5px; line-height: 1.5; } +.lead { margin: 6px 0 0; max-width: 600px; color: var(--text-2); font-size: 14px; line-height: 1.5; } .section-title { display: flex; align-items: baseline; gap: 8px; margin: 28px 0 10px; } .section-title h2 { margin: 0; font: 600 16px/1.2 var(--serif); letter-spacing: -0.01em; } .section-title .count { font-size: 12px; font-weight: 500; color: var(--text-3); font-variant-numeric: tabular-nums; } @@ -188,9 +188,9 @@ a.btn { text-decoration: none; } /* ---------------- primitives ---------------- */ .btn { - height: 30px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; flex: none; + height: 32px; padding: 0 13px; display: inline-flex; align-items: center; gap: 6px; flex: none; border-radius: 8px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--text); - font-size: 13px; font-weight: 500; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); white-space: nowrap; + font-size: 13.5px; font-weight: 500; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); white-space: nowrap; transition: transform 90ms ease, background-color 120ms ease, opacity 120ms ease; } .btn:hover { background: color-mix(in srgb, var(--surface) 92%, var(--text)); } @@ -206,19 +206,19 @@ a.btn { text-decoration: none; } .btn-ghost.btn-danger:hover { background: var(--danger-soft); color: var(--danger); } .btn-sm { height: 30px; padding: 0 11px; font-size: 13px; border-radius: 8px; } .btn-lg { height: 34px; padding: 0 16px; font-size: 13.5px; border-radius: 9px; } -.icon-btn { width: 30px; padding: 0; justify-content: center; } +.icon-btn { width: 32px; padding: 0; justify-content: center; } .btn-sm.icon-btn { width: 30px; } .input, .textarea, .select { width: 100%; border: 0.5px solid var(--line-strong); background: var(--field); border-radius: 8px; - padding: 6px 10px; font-size: 13px; box-shadow: inset 0 0.5px 1px rgba(0, 0, 0, 0.04); + padding: 6px 11px; font-size: 13.5px; box-shadow: inset 0 0.5px 1px rgba(0, 0, 0, 0.04); transition: box-shadow 120ms ease, border-color 120ms ease; } -.input { height: 30px; } +.input { height: 32px; } .textarea { min-height: 60px; resize: vertical; line-height: 1.5; } .input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .input::placeholder, .textarea::placeholder { color: var(--text-3); } -.select { height: 30px; appearance: none; padding-right: 26px; background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%); background-position: calc(100% - 13px) 13px, calc(100% - 9px) 13px; background-size: 4px 4px; background-repeat: no-repeat; } +.select { height: 32px; appearance: none; padding-right: 26px; background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%); background-position: calc(100% - 13px) 14px, calc(100% - 9px) 14px; background-size: 4px 4px; background-repeat: no-repeat; } .mono { font-family: var(--mono); } .field-label { display: grid; gap: 6px; } .field-label > span:first-child { font-size: 12px; font-weight: 600; color: var(--text-2); } @@ -245,13 +245,13 @@ a.btn { text-decoration: none; } .segmented { display: inline-flex; gap: 2px; padding: 2px; border-radius: 8px; background: var(--hover); flex: none; } .toolbar .segmented { background: color-mix(in srgb, var(--text) 3%, transparent); } .segmented button { - height: 26px; padding: 0 12px; border: 0; border-radius: 6px; background: none; display: inline-flex; align-items: center; gap: 6px; - font-size: 12.5px; font-weight: 500; color: var(--text-2); transition: background-color 120ms ease, color 120ms ease; + height: 28px; padding: 0 12px; border: 0; border-radius: 6px; background: none; display: inline-flex; align-items: center; gap: 6px; + font-size: 13px; font-weight: 500; color: var(--text-2); transition: background-color 120ms ease, color 120ms ease; } .segmented button:hover { color: var(--text); } .segmented button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); } @media (prefers-color-scheme: dark) { .segmented button.on { background: #3D3D44; } } -.segmented.sm button { height: 26px; padding: 0 10px; font-size: 12.5px; } +.segmented.sm button { height: 28px; padding: 0 11px; font-size: 13px; } .badge { display: inline-flex; align-items: center; gap: 4px; height: 18px; padding: 0 7px; border-radius: 99px; font-size: 11px; font-weight: 600; background: var(--hover); color: var(--text-2); white-space: nowrap; } .badge.live { background: var(--live-soft); color: var(--live); } @@ -284,16 +284,16 @@ a.btn { text-decoration: none; } } /* Inset grouped list, as in System Settings. */ -.group-wrap { margin: 0 0 36px; } +.group-wrap { margin: 0 0 40px; } .group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 0 8px; } .group-head h3 { margin: 0; font: 600 15px/1.25 var(--serif); letter-spacing: -0.005em; color: var(--text); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } -.group-note { margin: 8px 0 0; max-width: 600px; font-size: 12.5px; color: var(--text-3); line-height: 1.5; } +.group-note { margin: 8px 0 0; max-width: 600px; font-size: 13px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: none; color: var(--text-2); flex: none; } .row-main { flex: 1; min-width: 0; } -.row-title { font-size: 13.5px; font-weight: 500; } +.row-title { font-size: 14px; font-weight: 500; } .row-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; line-height: 1.45; } .row-ctl { display: flex; align-items: center; gap: 8px; flex: none; } .row-block { padding: 12px 14px; display: grid; gap: 10px; } @@ -334,7 +334,7 @@ a.btn { text-decoration: none; } .approval .learned { margin-top: 3px; font-size: 11.5px; color: var(--text-2); font-weight: 500; } .inbox-list { display: grid; gap: 10px; } .inbox-item .badge.count { margin-left: auto; animation: pop 360ms var(--spring-soft) both; } -.inbox-row { display: grid; grid-template-columns: 28px 1fr auto; gap: 12px; align-items: start; padding: 12px 14px; } +.inbox-row { display: grid; grid-template-columns: 26px 1fr auto; gap: 12px; align-items: start; padding: 12px 14px; } .inbox-row + .inbox-row { border-top: 0.5px solid var(--line); } .inbox-row.leaving { animation: fade-out 240ms var(--ease-out) both; } .inbox-title { font-size: 13.5px; font-weight: 600; } @@ -361,13 +361,13 @@ a.btn { text-decoration: none; } .chat-search .tl { grid-template-columns: 84px 86px 1fr; } .chat-search .tl .mono { white-space: nowrap; } .show-earlier { align-self: center; } -.so-far { margin: 0; align-self: stretch; padding: 2px 0 2px 12px; border-left: 2px solid var(--line-strong); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } -.so-far b { color: var(--text-2); font-weight: 600; } +.so-far { margin: 0; align-self: stretch; padding: 2px 0 2px 12px; border-left: 2px solid var(--line); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; font-size: 12.5px; line-height: 1.45; color: var(--text-3); } +.so-far b { color: var(--text-2); font-weight: 500; } .msg-hit { outline: 2px solid var(--pending); outline-offset: 4px; border-radius: 10px; transition: outline-color 600ms; } .msg-user { align-self: flex-end; max-width: 78%; padding: 8px 13px; border-radius: 16px 16px 5px 16px; background: var(--sunken); font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } .msg-bot { display: grid; grid-template-columns: 26px 1fr; gap: 11px; } .msg-head { display: flex; align-items: baseline; gap: 8px; margin: 3px 0 1px; } -.msg-head b { font-size: 12.5px; font-weight: 650; color: var(--pig, var(--text)); } +.msg-head b { font-size: 13px; font-weight: 650; color: color-mix(in srgb, var(--pig, var(--text)) 78%, var(--text)); } .msg-head time { font-size: 12px; color: var(--text-3); } .msg-body { font-size: 14px; line-height: 1.6; word-wrap: break-word; } .sent-photo { display: block; max-width: min(100%, 420px); margin-top: 8px; border-radius: 8px; border: 1px solid var(--line); } @@ -541,28 +541,29 @@ a.btn { text-decoration: none; } .mem-list > .mem:hover { box-shadow: none; background: color-mix(in srgb, var(--text) 2%, transparent); } .mem-list > .mem.superseded, .mem-list > .mem.forgotten { box-shadow: none; } .mem { - position: relative; padding: 14px 16px 12px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); + position: relative; padding: 12px 16px 11px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); transition: box-shadow 160ms ease; } .mem:hover { box-shadow: var(--shadow-md); } .mem.superseded, .mem.forgotten { background: none; box-shadow: inset 0 0 0 0.5px var(--line-strong); } .mem.superseded .mem-title, .mem.superseded .mem-body, .mem.forgotten .mem-title, .mem.forgotten .mem-body { color: var(--text-3); } .mem.forgotten .mem-title { text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--danger) 60%, transparent); } -.mem-title { font: 600 14px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } +.mem-title { font: 600 14.5px/1.35 var(--sans); letter-spacing: -0.006em; padding-right: 92px; } .mem-title .icon { display: inline-block; margin-right: 5px; color: var(--text-3); vertical-align: -1px; } .mem-actions .icon-btn.on { color: var(--text); } -.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--accent); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } -.mem-meta .linkish:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } +.mem-meta .linkish { font: inherit; font-weight: 500; color: var(--text-2); background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none; } +.mem-meta .linkish:hover { color: var(--text); text-decoration: underline; text-underline-offset: 2px; } .versions { list-style: none; margin: 10px 0 0; padding: 8px 0 0 12px; border-top: 0.5px solid var(--line); display: grid; gap: 6px; } .versions li { display: grid; gap: 1px; font-size: 12.5px; color: var(--text-2); border-left: 2px solid var(--line-strong); padding-left: 10px; } .versions small { font-size: 11px; color: var(--text-3); } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 4px 0 8px; font-size: 13.5px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } +.mem-body { margin: 5px 0 8px; font-size: 14px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } .mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } -.mem-meta .mem-id { opacity: .6; } +.mem-meta .mem-id { opacity: 0; transition: opacity 160ms ease; } +.mem:hover .mem-meta .mem-id, .mem:focus-within .mem-meta .mem-id { opacity: .6; } .mem-meta .gone { color: var(--danger); } .mem-meta .warn { color: var(--pending); } .mem-meta .inline-date { font: inherit; color: inherit; background: none; border: 0; padding: 0; } @@ -610,10 +611,10 @@ a.btn { text-decoration: none; } /* ---------------- routines ---------------- */ -.routine { display: grid; grid-template-columns: 32px 1fr auto; gap: 12px 14px; align-items: start; padding: 13px 14px; } +.routine { display: grid; grid-template-columns: 26px 1fr auto; gap: 12px; align-items: start; padding: 14px 14px; } .routine + .routine { border-top: 0.5px solid var(--line); } -.routine .ri { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--hover); color: var(--text-2); } -.routine .ri.quiet { background: var(--hover); color: var(--text-2); } +.routine .ri { width: 26px; height: 22px; display: grid; place-items: center; color: var(--text-2); } +.routine .ri.quiet { color: var(--text-2); } .routine .rt { font-size: 13px; font-weight: 600; color: var(--text-2); display: flex; align-items: center; gap: 8px; } .routine .rp { margin-top: 2px; font-size: 14px; color: var(--text); line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .routine .rm { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-3); } @@ -658,14 +659,14 @@ a.btn { text-decoration: none; } /* ---------------- activity ---------------- */ -.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 22px; } +.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 12px; } .stat { padding: 14px 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); } .stat dt { font-size: 12px; color: var(--text-2); font-weight: 500; } .stat dd { margin: 6px 0 0; font: 600 28px/1.1 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .stat dd small { font-size: 18px; font-weight: 500; color: var(--text-3); letter-spacing: -0.01em; } .stat dd small.bad { color: var(--danger); } .stat p { margin: 4px 0 0; font-size: 11px; color: var(--text-3); line-height: 1.35; } -.runrow { width: 100%; display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; align-items: center; padding: 10px 14px; border: 0; background: none; text-align: left; transition: background-color 120ms ease; } +.runrow { width: 100%; display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; align-items: center; padding: 8px 14px; border: 0; background: none; text-align: left; transition: background-color 120ms ease; } .runrow:hover { background: var(--hover); } .runhead { display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; padding: 9px 14px 8px; border-bottom: 1px solid var(--line); font-size: 11px; font-weight: 600; color: var(--text-3); } .runhead .num { font-size: 11px; color: var(--text-3); } @@ -697,7 +698,7 @@ a.btn { text-decoration: none; } .group:has(> .row-block > .provider-cards:not(.templates)) { background: none; box-shadow: none; overflow: visible; } .group:has(> .row-block > .provider-cards:not(.templates)) > .row-block { padding: 0; gap: 14px; } .provider-card { text-align: left; align-content: start; padding: 11px 12px 12px; border-radius: 10px; border: 1px solid var(--line); background: none; display: grid; gap: 3px; transition: box-shadow 120ms ease, border-color 120ms ease; } -.provider-card b { font-size: 13px; } +.provider-card b { font-size: 13.5px; margin-bottom: 2px; } .provider-card span { font-size: 12px; color: var(--text-2); line-height: 1.4; text-wrap: pretty; } .templates .provider-card { align-content: start; } .templates .provider-card b { display: flex; align-items: center; gap: 6px; } @@ -706,7 +707,7 @@ a.btn { text-decoration: none; } .starter-routine .row-title { display: flex; align-items: center; gap: 6px; } .starter-routine .textarea { margin-top: 6px; min-height: 0; } .starter-routine .textarea:disabled { opacity: .5; } -.provider-card.on { border-color: color-mix(in srgb, var(--accent) 75%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent); box-shadow: none; } +.provider-card.on { border-color: color-mix(in srgb, var(--accent) 75%, transparent); background: var(--accent-soft); box-shadow: none; } .provider-card:hover:not(.on) { background: var(--hover); } .chan-row .row-icon { color: var(--text); } .account-test { display: flex; align-items: center; gap: 10px; min-height: 24px; } @@ -1083,7 +1084,7 @@ kbd, .keys { font: 500 11.5px var(--sans); color: var(--text-2); padding: 3px 7p .library { margin-top: 28px; } .library .section-title .select { margin-left: auto; } .library .hint { margin: 0 0 6px; max-width: 600px; font-size: 13.5px; color: var(--text-2); } -.lib-cat h3 { font-size: 13px; font-weight: 600; color: var(--text-2); margin: 22px 0 8px; } +.lib-cat h3 { font-size: 12.5px; font-weight: 600; color: var(--text-3); margin: 22px 0 8px; } /* Adding is the point of a library card: always shown, not only on hover. */ .library .mem-actions.lib-add { opacity: 1; transform: none; } .library .mem .mem-body { padding-right: 72px; } From 9aee52f6524f4e1bd2d96169be5d0a21799fbaf6 Mon Sep 17 00:00:00 2001 From: Willem Date: Thu, 5 Nov 2026 21:04:38 +0100 Subject: [PATCH 12/38] UI polish: one-line run details, quieter token arrow, fading day divider Co-Authored-By: Claude Opus 5.5 --- ui/src/index.css | 22 ++++++++++++---------- ui/src/views/activity.tsx | 2 +- ui/src/views/chat.tsx | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ui/src/index.css b/ui/src/index.css index a276fd32..7ad38c50 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -53,8 +53,8 @@ --line: rgba(255, 255, 255, 0.075); --line-strong: rgba(255, 255, 255, 0.14); --text: #EDEDEF; - --text-2: #C2C2CA; - --text-3: #A3A3AB; + --text-2: #C4C4CC; + --text-3: #A8A8B0; --ink: #EDEDEF; --on-ink: #1A1A1C; --live: #3DAA72; @@ -238,7 +238,8 @@ a.btn { text-decoration: none; } background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); transition: transform 180ms cubic-bezier(.3, .7, .3, 1); } .switch:checked { background: var(--accent-fill); } -@media (prefers-color-scheme: dark) { .switch:not(:checked) { background: rgba(255, 255, 255, 0.3); } } +@media (prefers-color-scheme: dark) { .switch:not(:checked) { background: rgba(255, 255, 255, 0.34); } } +@media (prefers-color-scheme: dark) { .rail { border-right-color: var(--line); } } .switch:checked::after { transform: translateX(13px); } .switch:disabled { opacity: 0.5; } @@ -288,7 +289,7 @@ a.btn { text-decoration: none; } .group-head { display: flex; align-items: center; justify-content: space-between; margin: 0 0 8px; } .group-head h3 { margin: 0; font: 600 15px/1.25 var(--serif); letter-spacing: -0.005em; color: var(--text); } .group { background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-sm); overflow: hidden; } -.group-note { margin: 8px 0 0; max-width: 600px; font-size: 13px; color: var(--text-3); line-height: 1.5; } +.group-note { margin: 7px 0 0; max-width: 600px; font-size: 13px; color: var(--text-3); line-height: 1.5; } .row { display: flex; align-items: center; gap: 12px; padding: 10px 14px; min-height: 44px; } .row + .row, .row + .row-block, .row-block + .row, .row-block + .row-block { border-top: 0.5px solid var(--line); } .row-icon { width: 26px; height: 26px; border-radius: 7px; display: grid; place-items: center; background: none; color: var(--text-2); flex: none; } @@ -466,7 +467,8 @@ a.btn { text-decoration: none; } .composer .send.stop { background: var(--text); } .composer .send.stop .stop-square { width: 10px; height: 10px; border-radius: 2.5px; background: var(--bg); } .day-sep { display: flex; align-items: center; gap: 12px; margin: 6px 0 -4px; font-size: 11.5px; font-weight: 600; color: var(--text-3); } -.day-sep::before, .day-sep::after { content: ""; flex: 1; height: 0.5px; background: var(--line); } +.day-sep::before, .day-sep::after { content: ""; flex: 1; height: 1px; background: linear-gradient(to right, transparent, var(--line)); } +.day-sep::after { background: linear-gradient(to left, transparent, var(--line)); } .msg-copy { margin-left: auto; width: 22px; height: 22px; border: 0; border-radius: 6px; background: none; color: var(--text-3); display: grid; place-items: center; opacity: 0; transform: scale(0.9); transition: opacity 160ms ease, transform 300ms var(--spring), background-color 120ms ease, color 120ms ease; } .msg-bot:hover .msg-copy, .msg-copy:focus-visible, .msg-copy.done { opacity: 1; transform: none; } .msg-copy:hover { background: var(--hover); color: var(--text); } @@ -499,7 +501,7 @@ a.btn { text-decoration: none; } @keyframes typing { 0%, 60%, 100% { opacity: .3; transform: none; } 30% { opacity: 1; transform: translateY(-2px); } } @media (prefers-reduced-motion: reduce) { .typing i { animation: none; opacity: .6; } .fade-in, .approval { animation: none; } } -.composer-wrap { position: sticky; bottom: 0; padding: 10px 32px 20px; background: linear-gradient(to bottom, transparent, var(--bg) 26%); } +.composer-wrap { position: sticky; bottom: 0; padding: 10px 32px 24px; background: linear-gradient(to bottom, transparent, var(--bg) 26%); } .composer { max-width: 696px; margin: 0 auto; display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 8px; border-radius: 16px; background: var(--surface); border: 1px solid var(--line-strong); @@ -559,7 +561,7 @@ a.btn { text-decoration: none; } .editor-card.import { display: grid; gap: 10px; } .editor-card.import .er { display: flex; align-items: center; gap: 10px; } .editor-card.import .grow { flex: 1; } -.mem-body { margin: 5px 0 8px; font-size: 14px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } +.mem-body { margin: 6px 0 6px; font-size: 14px; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; } .mem-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: 12.5px; color: var(--text-3); font-variant-numeric: tabular-nums; } .mem-meta .lineage { color: var(--pending); } .mem-meta .mem-id { opacity: 0; transition: opacity 160ms ease; } @@ -659,11 +661,11 @@ a.btn { text-decoration: none; } /* ---------------- activity ---------------- */ -.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 12px; } +.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } .stat { padding: 14px 16px; border-radius: 12px; background: var(--surface); box-shadow: var(--shadow-sm); } .stat dt { font-size: 12px; color: var(--text-2); font-weight: 500; } .stat dd { margin: 6px 0 0; font: 600 28px/1.1 var(--sans); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } -.stat dd small { font-size: 18px; font-weight: 500; color: var(--text-3); letter-spacing: -0.01em; } +.stat dd small { font-size: 20px; font-weight: 400; color: var(--text-3); margin: 0 2px; } .stat dd small.bad { color: var(--danger); } .stat p { margin: 4px 0 0; font-size: 11px; color: var(--text-3); line-height: 1.35; } .runrow { width: 100%; display: grid; grid-template-columns: 8px minmax(0, 1.5fr) minmax(0, 1fr) 96px 64px 70px 14px; gap: 14px; align-items: center; padding: 8px 14px; border: 0; background: none; text-align: left; transition: background-color 120ms ease; } @@ -673,7 +675,7 @@ a.btn { text-decoration: none; } .runrow + .run-detail, .run + .run { border-top: 0.5px solid var(--line); } .run-t { display: grid; min-width: 0; } .run-t b { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.run-t small { font-size: 12.5px; color: var(--text-3); } +.run-t small { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; color: var(--text-3); } .run-bot { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-2); } .run-bot span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .num { font-size: 12px; color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; } diff --git a/ui/src/views/activity.tsx b/ui/src/views/activity.tsx index dd36a69b..4417586b 100644 --- a/ui/src/views/activity.tsx +++ b/ui/src/views/activity.tsx @@ -32,7 +32,7 @@ export function ActivityView({ chatId }: { chatId: number }) { {t && (
Runs · 30 days
{t.runs}{t.errors ? · {t.errors} failed : null}
-
Tokens in → out
{tok(t.tokens_in)} → {tok(t.tokens_out)}
+
Tokens in → out
{tok(t.tokens_in)} {tok(t.tokens_out)}
API-equivalent cost
{usd(t.cost_usd)}
)} diff --git a/ui/src/views/chat.tsx b/ui/src/views/chat.tsx index 4c604176..40d9a627 100644 --- a/ui/src/views/chat.tsx +++ b/ui/src/views/chat.tsx @@ -757,7 +757,7 @@ export function ChatView({ chat, bots, onTurn, incognito = false, setIncognito, {openBot && !incognito && ( )}