v0.8.5: WebUI process timeline, Gemini-style images, aligned layout
Process timeline: a run of consecutive thinking and tool calls is one
block that folds to a single line ("思考 4 次 · 工具 6 次"; while running
it shows the current action) and opens into a compact timeline, one row
per thought or call. Visible text, a note or the next prompt closes the
block; history and the live stream follow the same rule. show_thinking
now also applies while streaming instead of forcing thoughts open.
Layout: the avatar gutter and the flex-wrap that dropped it under long
answers are gone, so answer, process blocks, notes and composer share one
left edge. The text column and composer are both 720px (--content); the
rail and run panel grow with clamp() on wide screens. A rebuild after
each turn no longer replays every entrance animation, and the turn time
survives it. Message actions are icon buttons and only appear under text.
Images: a prompt's images sit above the bubble, right-aligned (one keeps
its shape, several are 152px rounded tiles) and open in a lightbox (Esc,
arrow keys). Images uploaded from the page show as thumbnails over the
composer and appear on the prompt as soon as it is sent.
Also: ?theme= is no longer overridden by app.js, the user bubble is
visible in the light theme, and the narrow top bar no longer wraps.
This commit is contained in:
+158
-62
@@ -35,6 +35,7 @@
|
||||
--composer-surface-secondary: #202026;
|
||||
|
||||
--bg-code: #08080a;
|
||||
--bubble-user: var(--main-surface-tertiary);
|
||||
|
||||
/* every hover/active tint is alpha, so it lands correctly on any surface */
|
||||
--hover: color-mix(in srgb, var(--text-primary) 7%, transparent);
|
||||
@@ -68,8 +69,11 @@
|
||||
--accent: var(--accent-primary);
|
||||
--danger: var(--text-error);
|
||||
|
||||
--rail: 252px;
|
||||
--panel: 296px;
|
||||
/* the side columns grow with the screen so a wide window does not turn into
|
||||
* one wide middle: the text stays near the run panel it is read against */
|
||||
--rail: clamp(240px, 14vw, 280px);
|
||||
--panel: clamp(296px, 20vw, 380px);
|
||||
--content: 720px; /* text column and composer share this edge */
|
||||
--radius: 12px;
|
||||
--radius-lg: 22px;
|
||||
--shadow: 0 16px 40px rgba(0, 0, 0, .45);
|
||||
@@ -103,6 +107,7 @@
|
||||
--composer-surface-secondary: #f4f4f6;
|
||||
|
||||
--bg-code: #f5f5f7;
|
||||
--bubble-user: #eef0f4;
|
||||
|
||||
--accent-primary: #0f9d70;
|
||||
--accent-fg: #ffffff;
|
||||
@@ -306,7 +311,7 @@ body.no-side #side { pointer-events: none; }
|
||||
border-radius: 99px; padding: 2px; position: relative;
|
||||
}
|
||||
.seg button {
|
||||
height: 25px; padding: 0 12px; border-radius: 99px; color: var(--fg-dim); font-size: 12.5px;
|
||||
height: 25px; padding: 0 12px; white-space: nowrap; border-radius: 99px; color: var(--fg-dim); font-size: 12.5px;
|
||||
transition: color var(--t-fast), background var(--t);
|
||||
}
|
||||
.seg button.on { background: var(--bg-lift); color: var(--fg); font-weight: 600; }
|
||||
@@ -316,7 +321,7 @@ body.no-side #side { pointer-events: none; }
|
||||
flex: 1 1 auto; overflow-y: auto; padding: 22px 0 10px;
|
||||
scroll-behavior: smooth; scrollbar-gutter: stable;
|
||||
}
|
||||
#log { max-width: 760px; margin: 0 auto; padding: 0 20px; }
|
||||
#log { max-width: calc(var(--content) + 40px); margin: 0 auto; padding: 0 20px; }
|
||||
#log:empty { display: none; }
|
||||
|
||||
/* --- empty state ---------------------------------------------------------- */
|
||||
@@ -330,16 +335,22 @@ body.no-side #side { pointer-events: none; }
|
||||
}
|
||||
|
||||
/* --- transcript ----------------------------------------------------------- */
|
||||
.msg { display: flex; gap: 12px; padding: 13px 0 0; animation: fade-up var(--t) var(--ease) both; }
|
||||
.msg .gutter { flex: 0 0 24px; padding-top: 2px; color: var(--fg-faint); font-size: 11px; text-align: center; user-select: none; }
|
||||
.msg .body { min-width: 0; flex: 1 1 auto; }
|
||||
.msg.user { justify-content: flex-end; }
|
||||
.msg.user .gutter { display: none; }
|
||||
/* one left edge for everything (answer, process block, notes), the same edge
|
||||
* as the composer; prompts hang on the right edge with their images above */
|
||||
.msg { display: flex; flex-direction: column; align-items: stretch; padding: 14px 0 0; animation: fade-up var(--t) var(--ease) both; }
|
||||
.msg .body { min-width: 0; max-width: 100%; }
|
||||
.msg.user { align-items: flex-end; padding-top: 24px; }
|
||||
#log > .msg.user:first-child { padding-top: 6px; }
|
||||
.msg.user .body {
|
||||
background: var(--bg-lift); border-radius: 18px 18px 6px 18px;
|
||||
padding: 9px 15px; max-width: 76%; white-space: pre-wrap; word-break: break-word;
|
||||
background: var(--bubble-user); border-radius: 24px;
|
||||
padding: 11px 18px; max-width: min(76%, 560px); white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.msg.error .body { color: var(--danger); white-space: pre-wrap; }
|
||||
.msg.error .body::before {
|
||||
content: "!"; display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 18px; height: 18px; margin-right: 8px; border-radius: 50%; vertical-align: 1px;
|
||||
background: color-mix(in srgb, var(--danger) 18%, transparent); font-size: 12px; font-weight: 800;
|
||||
}
|
||||
.body > :first-child, .live-html > :first-child { margin-top: 0; }
|
||||
.body > :last-child, .live-html > :last-child { margin-bottom: 0; }
|
||||
.body h1, .body h2, .body h3, .body h4 { line-height: 1.35; margin: 1.1em 0 .5em; }
|
||||
@@ -370,63 +381,102 @@ body.no-side #side { pointer-events: none; }
|
||||
background: var(--accent); animation: blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
.attach-strip { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0 8px; justify-content: flex-end; }
|
||||
.attach-strip img {
|
||||
width: 108px; height: 108px; object-fit: cover; border-radius: 12px;
|
||||
border: 1px solid var(--line); animation: rise var(--t) var(--ease) both;
|
||||
/* a prompt's images: above the bubble and outside it, square tiles when there
|
||||
* are several, the picture's own shape when there is one */
|
||||
.attach-strip { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12px; margin: 0 0 10px; max-width: 100%; }
|
||||
.attach-strip .thumb {
|
||||
display: block; width: 152px; height: 152px; padding: 0; overflow: hidden; cursor: zoom-in;
|
||||
border-radius: 20px; border: 1px solid var(--line); background: var(--bg-soft);
|
||||
animation: rise var(--t) var(--ease) both;
|
||||
transition: border-color var(--t-fast), transform 90ms var(--ease);
|
||||
}
|
||||
.attach-strip .thumb:hover { border-color: var(--line-strong); }
|
||||
.attach-strip .thumb:active { transform: scale(.985); }
|
||||
.attach-strip .thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.attach-strip.one .thumb { width: auto; height: auto; }
|
||||
.attach-strip.one .thumb img {
|
||||
width: auto; height: auto; min-width: 72px; min-height: 72px;
|
||||
max-width: min(260px, 70vw); max-height: 260px; object-fit: contain;
|
||||
}
|
||||
|
||||
.turn-meta { display: flex; align-items: center; gap: 10px; padding: 10px 0 2px 36px; }
|
||||
.turn-meta { display: flex; align-items: center; gap: 10px; padding: 8px 0 2px; }
|
||||
.turn-meta details > summary { cursor: pointer; list-style: none; color: var(--fg-faint); font-size: 12px; transition: color var(--t-fast); }
|
||||
.turn-meta details > summary::-webkit-details-marker { display: none; }
|
||||
.turn-meta details > summary:hover { color: var(--fg-dim); }
|
||||
.msg:has(> .msg-actions) { flex-wrap: wrap; row-gap: 0; }
|
||||
.msg-actions { display: flex; flex-basis: 100%; gap: 2px; padding: 4px 0 0 36px; opacity: 0; transition: opacity var(--t) var(--ease); }
|
||||
/* the actions take a row only under messages that have text to act on */
|
||||
.msg-actions { display: flex; gap: 2px; padding-top: 4px; margin-left: -7px; opacity: 0; transition: opacity var(--t) var(--ease); }
|
||||
.msg.user .msg-actions { margin: 0 -7px 0 0; }
|
||||
.msg:hover .msg-actions, .msg-actions:focus-within { opacity: 1; }
|
||||
.msg-actions button {
|
||||
height: 26px; padding: 0 9px; border-radius: 8px; color: var(--fg-faint); font-size: 12px;
|
||||
.act {
|
||||
width: 30px; height: 28px; border-radius: 8px; color: var(--fg-faint);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
transition: background var(--t-fast), color var(--t-fast), transform 90ms var(--ease);
|
||||
}
|
||||
.act svg.i { width: 16px; height: 16px; }
|
||||
.act:hover { background: var(--bg-hover); color: var(--fg); }
|
||||
.act:active { transform: scale(.94); }
|
||||
|
||||
/* --- process block: a run of thinking and tool calls, folded to one line --- */
|
||||
.proc { margin: 10px 0 0; }
|
||||
.proc-sum {
|
||||
display: inline-flex; align-items: center; gap: 7px; max-width: 100%; height: 28px;
|
||||
margin-left: -7px; padding: 0 10px 0 6px; border-radius: 8px;
|
||||
cursor: pointer; list-style: none; color: var(--fg-faint); font-size: 13px; user-select: none;
|
||||
transition: background var(--t-fast), color var(--t-fast);
|
||||
}
|
||||
.msg-actions button:hover { background: var(--bg-hover); color: var(--fg); }
|
||||
|
||||
.think { margin: .1em 0 .5em; }
|
||||
.think summary { cursor: pointer; list-style: none; color: var(--fg-faint); font-size: 12.5px; transition: color var(--t-fast); }
|
||||
.think summary::-webkit-details-marker { display: none; }
|
||||
.think summary::before { content: "▸ "; }
|
||||
.think[open] summary::before { content: "▾ "; }
|
||||
.think summary:hover { color: var(--fg-dim); }
|
||||
.think > div {
|
||||
color: var(--fg-dim); font-size: 13.5px; white-space: pre-wrap;
|
||||
padding: 6px 0 2px 14px; border-left: 2px solid var(--line);
|
||||
animation: fade-in var(--t) var(--ease) both;
|
||||
.proc-sum::-webkit-details-marker { display: none; }
|
||||
.proc-sum:hover { background: var(--bg-hover); color: var(--fg-dim); }
|
||||
.proc-chev { width: 14px; height: 14px; transform: rotate(-90deg); transition: transform var(--t) var(--ease); }
|
||||
.proc[open] > .proc-sum .proc-chev { transform: none; }
|
||||
.proc-lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.proc-meta { flex: 0 0 auto; color: var(--fg-quat); font-size: 12px; white-space: nowrap; }
|
||||
.proc-meta:empty { display: none; }
|
||||
/* the running block's label shimmers instead of spinning */
|
||||
.proc.run .proc-lbl {
|
||||
background: linear-gradient(90deg, var(--fg-faint) 0%, var(--fg) 45%, var(--fg-faint) 90%);
|
||||
background-size: 220% 100%; -webkit-background-clip: text; background-clip: text;
|
||||
color: transparent; animation: shimmer 1.8s linear infinite;
|
||||
}
|
||||
|
||||
/* tool calls: one line, opens on demand (like "已执行 python · <<PY>") */
|
||||
.turn-steps { padding: 6px 0 0 36px; }
|
||||
.turn-steps > summary { cursor: pointer; list-style: none; color: var(--fg-faint); font-size: 12.5px; transition: color var(--t-fast); }
|
||||
.turn-steps > summary::-webkit-details-marker { display: none; }
|
||||
.turn-steps > summary::before { content: "⌘ "; }
|
||||
.turn-steps > summary:hover { color: var(--fg-dim); }
|
||||
.steps-box { padding: 2px 0 0 12px; margin-top: 4px; border-left: 2px solid var(--line); }
|
||||
.step-line { animation: fade-up var(--t) var(--ease) both; }
|
||||
.step-line > summary {
|
||||
display: flex; align-items: baseline; gap: 8px; cursor: pointer; list-style: none;
|
||||
padding: 5px 0; font-size: 13px; color: var(--fg-dim);
|
||||
/* the timeline: one hairline, one dot per entry */
|
||||
.tl { position: relative; margin: 2px 0 0; padding: 2px 0 2px 20px; animation: fade-in var(--t) var(--ease) both; }
|
||||
.tl::before {
|
||||
content: ""; position: absolute; left: 4px; top: 14px; bottom: 14px; width: 1px;
|
||||
background: var(--line);
|
||||
}
|
||||
.tl-item { position: relative; animation: fade-up var(--t) var(--ease) both; }
|
||||
.tl-item > summary {
|
||||
display: flex; align-items: center; gap: 8px; min-height: 28px; padding: 2px 0;
|
||||
cursor: pointer; list-style: none; font-size: 13px; color: var(--fg-dim);
|
||||
transition: color var(--t-fast);
|
||||
}
|
||||
.step-line > summary::-webkit-details-marker { display: none; }
|
||||
.step-line > summary:hover { color: var(--fg); }
|
||||
.step-line .name { font-family: var(--mono); font-size: 12.5px; color: var(--fg); }
|
||||
.step-line .args { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12px; }
|
||||
.step-line .phase { font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.step-line.run .phase { color: var(--warn); }
|
||||
.step-line.run .phase::before {
|
||||
content: ""; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--warn); animation: pulse 1.1s var(--ease) infinite;
|
||||
.tl-item > summary::-webkit-details-marker { display: none; }
|
||||
.tl-item > summary:hover { color: var(--fg); }
|
||||
.tl-dot {
|
||||
position: absolute; left: -20px; top: 9px; width: 9px; height: 9px; border-radius: 50%;
|
||||
background: var(--bg); border: 1.5px solid var(--line-strong);
|
||||
}
|
||||
.tl-item.think .tl-dot { border-color: color-mix(in srgb, var(--violet) 75%, transparent); }
|
||||
.tl-name { flex: 0 0 auto; font-size: 12.5px; color: var(--fg-dim); }
|
||||
.tl-prev {
|
||||
flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
color: var(--fg-faint); font-size: 12.5px;
|
||||
}
|
||||
.tl-item.think[open] .tl-prev { visibility: hidden; }
|
||||
.think-text {
|
||||
color: var(--fg-dim); font-size: 13px; line-height: 1.65; white-space: pre-wrap; word-break: break-word;
|
||||
padding: 2px 0 8px; animation: fade-in var(--t) var(--ease) both;
|
||||
}
|
||||
|
||||
/* tool calls: one line each, open for arguments and output */
|
||||
.step-line .name { flex: 0 0 auto; font-family: var(--mono); font-size: 12.5px; color: var(--fg); }
|
||||
.step-line .args { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12px; color: var(--fg-faint); }
|
||||
.step-line .phase { flex: 0 0 auto; font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; }
|
||||
.step-line.run .phase { color: var(--warn); }
|
||||
.step-line.run .tl-dot { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 30%, var(--bg)); animation: pulse 1.1s var(--ease) infinite; }
|
||||
.step-line.done .tl-dot { border-color: var(--accent-line); background: var(--accent-weak); }
|
||||
.step-line.bad .tl-dot { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 20%, var(--bg)); }
|
||||
.step-line.bad .phase, .step-line.bad .name { color: var(--danger); }
|
||||
.step-line.done .phase::before { content: "✓"; }
|
||||
.step-line.bad .phase::before { content: "✕"; }
|
||||
/* a sub-agent's latest steps, next to its task */
|
||||
.step-line .step-detail {
|
||||
flex: 0 1 40%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
@@ -435,7 +485,7 @@ body.no-side #side { pointer-events: none; }
|
||||
.step-line.done .step-detail { display: none; }
|
||||
/* "[cst] plan mode is now ON" and other engine notes in the history */
|
||||
.note-line {
|
||||
padding: 6px 0 2px 36px; font-size: 12px; color: var(--fg-faint);
|
||||
padding: 8px 0 2px; font-size: 12px; color: var(--fg-faint);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.note-line::before { content: "· "; }
|
||||
@@ -459,9 +509,24 @@ body.no-side #side { pointer-events: none; }
|
||||
|
||||
/* --- composer ------------------------------------------------------------- */
|
||||
#composer { padding: 8px 20px 12px; }
|
||||
#composer > * { max-width: 760px; margin: 0 auto; }
|
||||
.pending { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 0 8px; }
|
||||
#composer > * { max-width: var(--content); margin: 0 auto; }
|
||||
.pending { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 6px 0 10px; }
|
||||
.pending .chip { background: var(--bg-soft); border-color: var(--line); }
|
||||
/* images waiting for the next prompt, the way they will look once sent */
|
||||
.pend-thumb { position: relative; width: 56px; height: 56px; flex: 0 0 auto; animation: rise var(--t) var(--ease) both; }
|
||||
.pend-thumb img {
|
||||
display: block; width: 100%; height: 100%; object-fit: cover; cursor: zoom-in;
|
||||
border-radius: 12px; border: 1px solid var(--line);
|
||||
}
|
||||
.pend-x {
|
||||
position: absolute; top: -7px; right: -7px; width: 20px; height: 20px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--main-surface-tertiary); border: 1px solid var(--line-strong); color: var(--fg-dim);
|
||||
opacity: 0; transition: opacity var(--t-fast), color var(--t-fast);
|
||||
}
|
||||
.pend-x svg.i { width: 12px; height: 12px; stroke-width: 2.2; }
|
||||
.pend-thumb:hover .pend-x, .pend-x:focus-visible { opacity: 1; }
|
||||
.pend-x:hover { color: var(--fg); }
|
||||
.edit-bar {
|
||||
display: flex; align-items: center; gap: 8px; padding: 6px 10px; margin-bottom: 8px;
|
||||
border-radius: 10px; border: 1px solid var(--line); background: var(--bg-soft);
|
||||
@@ -698,9 +763,6 @@ body.no-panel #panel { pointer-events: none; }
|
||||
.todo-list .boxmark { color: var(--fg-faint); }
|
||||
.todo-list li.done { color: var(--fg-faint); text-decoration: line-through; }
|
||||
.todo-list li.doing { color: var(--fg); font-weight: 600; }
|
||||
#steps-body .step-line > summary { font-size: 12.5px; }
|
||||
#steps-body .step-line .phase { font-size: 11px; }
|
||||
|
||||
/* --- modal sheet (session list) ------------------------------------------- */
|
||||
#modal {
|
||||
position: fixed; inset: 0; background: rgba(0, 0, 0, .5); z-index: 40;
|
||||
@@ -808,11 +870,44 @@ body.drawer-open #backdrop { display: block; }
|
||||
body.side-open #side, body.panel-open #panel { transform: none; }
|
||||
.only-narrow { display: inline-flex; }
|
||||
#to-bottom { bottom: 150px; right: 16px; }
|
||||
.hero h1 { font-size: 21px; }
|
||||
.msg.user .body { max-width: 88%; }
|
||||
.turn-meta, .msg-actions, .turn-steps, .note-line { padding-left: 0; }
|
||||
.attach-strip .thumb { width: 120px; height: 120px; }
|
||||
#lightbox { padding: 56px 12px; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
#chip-approval .lbl { display: none; }
|
||||
.seg button { padding: 0 9px; }
|
||||
}
|
||||
@media (min-width: 901px) { #backdrop { display: none !important; } }
|
||||
/* touch screens have no hover: the actions stay visible */
|
||||
@media (hover: none) {
|
||||
.msg-actions, .pend-x { opacity: 1; }
|
||||
}
|
||||
|
||||
/* --- lightbox ------------------------------------------------------------- */
|
||||
#lightbox {
|
||||
position: fixed; inset: 0; z-index: 70; padding: 56px 72px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0, 0, 0, .84); animation: fade-in var(--t) var(--ease) both;
|
||||
}
|
||||
#lb-img {
|
||||
max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, .5); animation: pop-in 180ms var(--ease) both;
|
||||
}
|
||||
.lb-btn {
|
||||
position: absolute; width: 40px; height: 40px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(255, 255, 255, .1); color: #fff;
|
||||
transition: background var(--t-fast), transform 90ms var(--ease);
|
||||
}
|
||||
.lb-btn:hover { background: rgba(255, 255, 255, .2); }
|
||||
#lb-close { top: 14px; right: 14px; }
|
||||
#lb-prev { left: 14px; top: calc(50% - 20px); }
|
||||
#lb-next { right: 14px; top: calc(50% - 20px); }
|
||||
#lb-count {
|
||||
position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, .75); font-size: 13px; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* --- motion --------------------------------------------------------------- */
|
||||
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
@@ -821,6 +916,7 @@ body.drawer-open #backdrop { display: block; }
|
||||
@keyframes pop-in { from { opacity: 0; transform: translateY(-4px) scale(.97); } to { opacity: 1; transform: none; } }
|
||||
@keyframes pulse { 0%, 100% { opacity: .3; transform: scale(.85); } 50% { opacity: 1; transform: scale(1); } }
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: -120% 0; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
|
||||
#scroll { scroll-behavior: auto; }
|
||||
|
||||
Reference in New Issue
Block a user