The activity panel, and continuing an agent chat in a terminal
The activity panel beside a chat, from /api/v2's activity routes: Context
(used share, tokens against the window, a segmented bar and a row per
category), Outputs (the files written, lines added and removed, each opening
in the viewer), Background tasks (a team's members as a tree, helpers and
commands: state, elapsed time counting on between reads, model, latest step,
what it is writing, its whole conversation read-only, Stop -- a member after
a confirmation, as it takes those under it), the team's room (who to whom,
reports marked, the words through the page's own markdown) and the plan.
The chat follows GET /chats/{chat}/activity/events while it is open: the strip
above the message box sums the agents up and opens the panel; a new version
reads the panel again while it is open, the costly context part on opening,
after a reply and at most every 15 s. The stream is reopened when the server
closes it, and more slowly while it fails. Beside the chat on a wide window
(the viewer then goes over the chat), over it below --bp-wide, a full-height
sheet at --bp-narrow. The header's Activity button, the palette and Escape
open and close it.
An agent chat with a device session (device_session_id) can be continued in a
terminal: the chat's menu and a header button show 'cd <folder> && lembas -s
<id>' to copy, with lembas the CLI as it starts here (on PATH, else the app's
own copy by its path). Shown rather than run: no terminal opens the same way
on every desktop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+1072
-40
No files matched your search
+195
-8
@@ -1,6 +1,7 @@
|
||||
/* LLeMbas Desktop's page. Every colour, size, space and font is a token from tokens.css (the
|
||||
server's, copied by scripts/fetch-tokens.sh and pinned); the only values of this page's own are
|
||||
the title bar's height and the setup card's width, named below. Media queries cannot read a
|
||||
the title bar's height, the setup card's width and the activity panel's number columns, named
|
||||
below. Media queries cannot read a
|
||||
custom property, so the two breakpoints are written out and named after the tokens they are:
|
||||
48rem is --bp-narrow (the sidebar becomes a drawer), 64rem is --bp-wide (the viewer goes over
|
||||
the chat instead of beside it).
|
||||
@@ -18,6 +19,10 @@
|
||||
--card-max: 40rem;
|
||||
/* The sidebar toggle's track in the title bar: none until the sidebar is a drawer. */
|
||||
--side-btn-w: 0px;
|
||||
/* The activity panel's number columns: a token count or a share ("180k", "63.0%"), and an
|
||||
elapsed time ("2h 03m"), in the panel's monospaced figures. */
|
||||
--num-col: 4.5em;
|
||||
--time-col: 5.5em;
|
||||
}
|
||||
|
||||
html, body { margin: 0; height: 100%; overflow: hidden; }
|
||||
@@ -376,7 +381,7 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
|
||||
/* --- The chat ------------------------------------------------------------------------------ */
|
||||
|
||||
.main, .settings { grid-area: main; min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; }
|
||||
.main, .settings { grid-area: main; min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto auto; }
|
||||
.settings { grid-template-rows: auto minmax(0, 1fr); }
|
||||
|
||||
.head {
|
||||
@@ -495,23 +500,20 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
its word before them -- fixed tracks, so nothing moves when a number grows a digit. */
|
||||
.agents {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-areas: "min max";
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: var(--control-h);
|
||||
}
|
||||
.agents__dash { display: none; }
|
||||
.agents__dash { color: var(--ink-muted); }
|
||||
.agents__input { width: 100%; min-width: 0; height: var(--control-h); padding: 0 var(--control-px-sm); }
|
||||
.agents--compact {
|
||||
grid-template-columns: auto calc(var(--w-field-xs) / 2) auto calc(var(--w-field-xs) / 2);
|
||||
grid-template-areas: none;
|
||||
width: auto;
|
||||
height: var(--control-h-sm);
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
}
|
||||
.agents--compact .agents__dash { display: inline; color: var(--ink-muted); }
|
||||
.agents--compact .agents__input { height: var(--control-h-sm); }
|
||||
.agents__label { color: var(--ink-muted); white-space: nowrap; }
|
||||
/* The model picker: a button the height of a control, avatar · name · chevron. */
|
||||
@@ -783,6 +785,173 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
.viewer__note { padding: var(--sp-4); font-size: var(--text-sm); color: var(--ink-muted); }
|
||||
.viewer__note.is-error { color: var(--warning); }
|
||||
|
||||
/* --- The activity panel -------------------------------------------------------------------------
|
||||
Beside the chat, the inspector's width; while the viewer is open too, the viewer goes over the
|
||||
chat (it was opened from here, and three columns of text beside the sidebar do not fit). Over
|
||||
the chat from --bp-wide down, as the viewer is; at --bp-narrow that is a full-height sheet. */
|
||||
|
||||
.shell[data-activity="true"],
|
||||
.shell[data-activity="true"][data-viewer="true"] {
|
||||
grid-template-columns: var(--sidebar-width) minmax(0, 1fr) var(--inspector-width);
|
||||
grid-template-areas: "side main activity";
|
||||
}
|
||||
.shell[data-activity="true"] .viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
|
||||
.activity {
|
||||
grid-area: activity;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
border-left: var(--border-w) solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
/* Title and close on two known tracks; with an agent's conversation open, Back before them. */
|
||||
.activity__head { grid-template-columns: minmax(0, 1fr) var(--control-h-sm); }
|
||||
.activity[data-view="agent"] .activity__head { grid-template-columns: var(--control-h-sm) minmax(0, 1fr) var(--control-h-sm); }
|
||||
.activity__body { overflow: auto; min-height: 0; display: grid; align-content: start; font-size: var(--text-sm); }
|
||||
.activity__section { padding: var(--sp-3); border-bottom: var(--border-w) solid var(--border); min-width: 0; }
|
||||
.activity__section:last-child { border-bottom: 0; }
|
||||
.activity__heading, .activity__subheading {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font: 600 var(--text-sm) / var(--leading-tight) var(--font-ui);
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
.activity__heading::-webkit-details-marker { display: none; }
|
||||
.activity__section[open] > .activity__heading { margin-bottom: var(--sp-3); }
|
||||
.activity__subheading { margin: var(--sp-4) 0 var(--sp-2); cursor: default; }
|
||||
.activity__count { font: var(--text-xs) var(--font-mono); color: var(--ink-muted); }
|
||||
.activity__empty { margin: 0; padding: var(--sp-1) 0; color: var(--ink-faint); font-size: var(--text-sm); }
|
||||
.activity__empty.is-error { color: var(--warning); padding: var(--sp-3); }
|
||||
.activity__plan { min-width: 0; }
|
||||
|
||||
/* Context: the line, the bar, then one row per category on four fixed tracks -- swatch, the name
|
||||
that gives, and two number columns -- so the numbers stand in columns in any language. The
|
||||
categories' colours are tokens, one each. */
|
||||
.ctx { display: grid; gap: var(--sp-2); }
|
||||
.ctx__used { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-2); margin: 0; color: var(--ink); }
|
||||
.ctx__of { font-family: var(--font-mono); color: var(--ink-muted); }
|
||||
.ctx__bar { display: flex; height: var(--sp-2); overflow: hidden; background: var(--surface-active); border-radius: var(--radius-full); }
|
||||
.ctx__seg { flex: none; width: var(--share, 0); background: var(--ctx, var(--ink-faint)); }
|
||||
.ctx__rows { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||
.ctx__row {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sp-3) minmax(0, 1fr) var(--num-col) var(--num-col);
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--row-h);
|
||||
}
|
||||
.ctx__key { width: var(--sp-3); height: var(--sp-3); border-radius: var(--radius-sm); background: var(--ctx, var(--ink-faint)); }
|
||||
.ctx--free.ctx__key { background: transparent; border: var(--border-w) solid var(--border-strong); }
|
||||
.ctx__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--ink-muted); }
|
||||
.ctx__num { font: var(--text-xs) var(--font-mono); text-align: right; color: var(--ink-muted); }
|
||||
.ctx__note { margin: 0; font-size: var(--text-xs); color: var(--ink-faint); }
|
||||
.ctx--messages { --ctx: var(--accent); }
|
||||
.ctx--system { --ctx: var(--ink-muted); }
|
||||
.ctx--memory { --ctx: var(--warning); }
|
||||
.ctx--instructions { --ctx: var(--success); }
|
||||
.ctx--tools { --ctx: var(--ink); }
|
||||
.ctx--mcp_tools { --ctx: var(--danger); }
|
||||
.ctx--since { --ctx: var(--ink-faint); }
|
||||
.ctx--buffer { --ctx: var(--border-strong); }
|
||||
.ctx--free { --ctx: transparent; }
|
||||
|
||||
/* Outputs: icon, the name (cut, never wrapped), the lines added and removed. */
|
||||
.outs { display: grid; margin: 0; padding: 0; list-style: none; }
|
||||
.outs__row { display: grid; grid-template-columns: var(--sp-4) minmax(0, 1fr) auto; gap: var(--sp-2); align-items: center; min-height: var(--row-h); color: var(--ink-muted); }
|
||||
.outs__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0; font: var(--text-sm) var(--font-ui); color: var(--ink); text-align: left; background: none; border: 0; cursor: pointer; }
|
||||
.outs__name:hover { color: var(--accent); text-decoration: underline; }
|
||||
.outs__diff { display: grid; grid-auto-flow: column; gap: var(--sp-2); font: var(--text-xs) var(--font-mono); }
|
||||
.outs__add { color: var(--success); }
|
||||
.outs__del { color: var(--danger); }
|
||||
|
||||
/* Background tasks. An agent's row is one grid of known tracks: state dot, what it is (the one
|
||||
track that gives, cut), elapsed time in a column of its own width, the stop button at its size
|
||||
(an empty box where there is none) -- so rows line up whatever each says. */
|
||||
.tasks { display: grid; gap: var(--sp-2); min-width: 0; }
|
||||
.tasks__summary, .tasks__team { margin: 0; color: var(--ink-muted); }
|
||||
.tasks__goal { margin: 0; color: var(--ink); }
|
||||
.tasks__list { display: grid; gap: var(--sp-1); }
|
||||
.tasks__more > summary { cursor: pointer; padding: var(--sp-1) 0; color: var(--ink-muted); }
|
||||
.tasks__more[open] > summary { margin-bottom: var(--sp-1); }
|
||||
.agent { margin-left: calc(var(--depth, 0) * var(--sp-4)); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius); min-width: 0; }
|
||||
.agent__row {
|
||||
display: grid;
|
||||
grid-template-columns: var(--dot-size) minmax(0, 1fr) var(--time-col) var(--control-h-sm);
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 var(--sp-1) 0 var(--sp-3);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.agent__row::-webkit-details-marker { display: none; }
|
||||
.agent__dot { width: var(--dot-size); height: var(--dot-size); border-radius: var(--radius-full); background: var(--ink-faint); }
|
||||
.agent.is-working .agent__dot { background: var(--accent); }
|
||||
.agent.is-queued .agent__dot, .agent.is-waiting .agent__dot { background: var(--warning); }
|
||||
.agent.is-done .agent__dot { background: var(--leaf); }
|
||||
.agent.is-error .agent__dot { background: var(--danger); }
|
||||
.agent__what { display: grid; min-width: 0; }
|
||||
.agent__name, .agent__role { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.agent__name { color: var(--ink); }
|
||||
.agent__role { font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
.agent__time { font: var(--text-xs) var(--font-mono); text-align: right; color: var(--ink-muted); }
|
||||
.agent__stop { width: var(--control-h-sm); height: var(--control-h-sm); }
|
||||
.agent__body { display: grid; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) var(--sp-3); border-top: var(--border-w) solid var(--border); min-width: 0; }
|
||||
.agent__body > .btn { justify-self: start; }
|
||||
.agent__facts { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin: 0; font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
.agent__state { color: var(--ink); }
|
||||
.agent__model { font-family: var(--font-mono); overflow-wrap: anywhere; }
|
||||
.agent__brief { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); }
|
||||
.agent__step { display: grid; grid-template-columns: var(--dot-size) auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; margin: 0; }
|
||||
.agent__verb { color: var(--ink); white-space: nowrap; }
|
||||
.agent__title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: var(--text-xs) var(--font-mono); color: var(--ink-muted); }
|
||||
.agent__text { margin: 0; max-height: calc(6 * var(--row-h)); overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
|
||||
/* The team's room: who to whom, a report marked, the words as markdown. */
|
||||
.room { display: grid; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; }
|
||||
.room__entry { display: grid; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius); min-width: 0; }
|
||||
.room__entry.is-report { border-left: var(--border-w-thick) solid var(--leaf); }
|
||||
.room__meta { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--sp-2); align-items: center; font-size: var(--text-xs); color: var(--ink-muted); }
|
||||
.room__n { font-family: var(--font-mono); }
|
||||
.room__route { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.room__badge { color: var(--leaf); border-color: var(--leaf); }
|
||||
|
||||
/* One agent's conversation, read-only. */
|
||||
.convo__head { display: grid; gap: var(--sp-2); padding: var(--sp-3); border-bottom: var(--border-w) solid var(--border); }
|
||||
.convo__head > .btn { justify-self: start; }
|
||||
.convo { display: grid; gap: var(--sp-3); padding: var(--sp-3); min-width: 0; }
|
||||
.convo__turn { font-size: var(--text-sm); }
|
||||
.convo__tools { display: grid; gap: var(--sp-1); margin: var(--sp-2) 0 0; padding: 0; list-style: none; }
|
||||
.convo__tool { display: grid; grid-template-columns: var(--dot-size) auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; font-size: var(--text-xs); }
|
||||
|
||||
/* The strip above the message box: what the agents are doing, one line, and the way into the panel. */
|
||||
.strip { padding: var(--sp-2) var(--sp-4) 0; }
|
||||
.strip__btn {
|
||||
display: grid;
|
||||
grid-template-columns: var(--dot-size) minmax(0, 1fr) auto;
|
||||
gap: var(--sp-2);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: var(--thread-max-width);
|
||||
min-height: var(--control-h);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--control-px-sm);
|
||||
font: var(--text-sm) var(--font-ui);
|
||||
color: var(--ink-muted);
|
||||
text-align: left;
|
||||
background: var(--surface);
|
||||
border: var(--border-w) solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
.strip__btn:hover { color: var(--ink); background: var(--surface-hover); }
|
||||
.strip__text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
|
||||
/* --- Menus, dialogs and the palette ----------------------------------------------------------- */
|
||||
|
||||
.menu {
|
||||
@@ -836,6 +1005,19 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
.dialog::backdrop { background: var(--scrim); }
|
||||
.dialog__body { display: grid; gap: var(--sp-3); padding: var(--sp-4); }
|
||||
.dialog__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-ui); }
|
||||
/* A line to copy: as it is, on one line that scrolls on its own rather than the dialog. */
|
||||
.dialog__code {
|
||||
margin: 0;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
font: var(--text-sm) var(--font-mono);
|
||||
color: var(--ink);
|
||||
background: var(--code-bg);
|
||||
border: var(--border-w) solid var(--code-border);
|
||||
border-radius: var(--radius);
|
||||
user-select: all;
|
||||
}
|
||||
.dialog__input { width: 100%; }
|
||||
.dialog__actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2); }
|
||||
|
||||
@@ -866,6 +1048,9 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
@media (max-width: 64rem) {
|
||||
.shell[data-viewer="true"] { grid-template-columns: var(--sidebar-width) minmax(0, 1fr); grid-template-areas: "side main"; }
|
||||
.viewer { grid-area: main; z-index: var(--z-panel); border-left: 0; }
|
||||
.shell[data-activity="true"],
|
||||
.shell[data-activity="true"][data-viewer="true"] { grid-template-columns: var(--sidebar-width) minmax(0, 1fr); grid-template-areas: "side main"; }
|
||||
.activity { grid-area: main; z-index: var(--z-panel); border-left: 0; }
|
||||
}
|
||||
|
||||
/* 48rem (--bp-narrow): the sidebar is a drawer, opened from the title bar. */
|
||||
@@ -873,7 +1058,8 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
:root { --side-btn-w: var(--titlebar-h); }
|
||||
.bar__side { visibility: visible; }
|
||||
.bar__host { display: none; }
|
||||
.shell, .shell[data-viewer="true"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "main"; }
|
||||
.shell, .shell[data-viewer="true"],
|
||||
.shell[data-activity="true"], .shell[data-activity="true"][data-viewer="true"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "main"; }
|
||||
.side {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
@@ -893,6 +1079,7 @@ label, .label { font: var(--text-sm) / var(--leading-tight) var(--font-ui); colo
|
||||
.head__tools:empty { display: none; }
|
||||
.thread { padding: var(--sp-3); }
|
||||
.composer { padding: var(--sp-2) var(--sp-3) var(--sp-3); }
|
||||
.strip { padding: var(--sp-2) var(--sp-3) 0; }
|
||||
.cards { padding: var(--sp-2) var(--sp-3) 0; }
|
||||
.card { padding: var(--sp-4); gap: var(--sp-4); }
|
||||
.msg--user { max-width: 100%; }
|
||||
|
||||
Reference in new issue
Block a user