v0.8.4: WebUI sessions, diff, settings, slash commands and extensions

Sessions: rename, delete and search; /api/retry regenerates the last
answer, editing a prompt resends from there; /api/export downloads the
transcript as Markdown (cst_session_export_md). Session messages carry
note/tool_result markers, so a reload rebuilds the tool steps with
their arguments, output and exit status; subagent progress is shown.

Diff and settings: /api/diff shows the real diff; /api/settings covers
protocol, max_steps, tool switches and confirm; /api/permissions edits
the rules; sandbox status is reported. Session rename no longer reads
past a short title.

Commands: slash commands with completion in the composer (/model,
/provider, /new, /retry, /undo, /compact, /export, /diff, /init,
/skill and more), a Ctrl+P palette, Shift+Tab cycles the approval
mode, F1 or ? shows the keys. "Thinking expanded" is saved in the
settings (show_thinking), "expand all tool output" in the browser.

Extensions: GET /api/extensions lists MCP servers, plugins, skills,
agent roles and subagent runs; POST /api/extensions/reload and
/api/extensions/trust; a workspace switch reloads them. /api/init and
/api/skill run on the server (cst_init_prompt shared with the TUI).

Errors, statuses and [cst] notes are shown in Chinese with a readable
reason instead of rc=; a failed turn's error survives the reload.

Tests: scripts/e2e_webui_sessions.py, e2e_webui_workspace.py and
e2e_webui_ext.py, run from e2e_webui.sh.
This commit is contained in:
WHC
2026-10-10 13:57:17 +08:00
parent 2d1052fd5f
commit 7e1dbd9afa
17 changed files with 2978 additions and 146 deletions
+1064 -64
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -69,6 +69,7 @@
<svg class="i"><use href="#i-refresh"/></svg>
</button>
</div>
<input id="sess-search" class="sess-search" type="search" placeholder="搜索会话" autocomplete="off">
<div id="sessions-list" class="sessions"></div>
<div class="side-foot">
@@ -133,6 +134,11 @@
<footer id="composer">
<div id="pending" class="pending" hidden></div>
<div id="edit-bar" class="edit-bar" hidden>
<span>编辑上一条消息:发送后替换它并重新回答(文件改动保留,需要时先撤销)</span>
<span class="grow"></span>
<button id="btn-edit-cancel" class="link">取消</button>
</div>
<!-- permission gate: inline, the turn really is waiting on it -->
<div id="gate" class="gate" hidden>
@@ -146,8 +152,9 @@
<div class="gate-row" id="gate-buttons"></div>
</div>
<div id="slash" class="slash" role="listbox" hidden></div>
<div class="box">
<textarea id="input" rows="1" placeholder="给 CST-Agent 派个任务…"></textarea>
<textarea id="input" rows="1" placeholder="给 CST-Agent 派个任务…(/ 打开命令,F1 看快捷键)"></textarea>
<div class="box-row">
<button id="btn-plus" class="icon att" title="添加图片或计划模式">
<svg class="i"><use href="#i-plus"/></svg>
@@ -191,7 +198,8 @@
</section>
<section class="card" id="card-files">
<h4>文件改动 <span class="badge" id="files-count">0</span></h4>
<h4>文件改动 <span class="badge" id="files-count">0</span>
<span class="grow"></span><button id="btn-diff" class="link" title="git status + git diff">查看 diff</button></h4>
<div id="files-body" class="muted">本轮还没有改动文件。</div>
</section>
+136 -3
View File
@@ -339,7 +339,7 @@ body.no-side #side { pointer-events: none; }
background: var(--bg-lift); border-radius: 18px 18px 6px 18px;
padding: 9px 15px; max-width: 76%; white-space: pre-wrap; word-break: break-word;
}
.msg.error .body { color: var(--danger); }
.msg.error .body { color: var(--danger); white-space: pre-wrap; }
.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; }
@@ -380,7 +380,8 @@ body.no-side #side { pointer-events: none; }
.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-actions { display: flex; gap: 2px; padding: 4px 0 0 36px; opacity: 0; transition: opacity var(--t) var(--ease); }
.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); }
.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;
@@ -425,6 +426,19 @@ body.no-side #side { pointer-events: none; }
}
.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;
font-family: var(--mono); font-size: 11.5px; color: var(--fg-faint);
}
.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);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.note-line::before { content: "· "; }
.step-body { padding: 0 0 6px; animation: fade-in var(--t) var(--ease) both; }
.step-out, .step-args-full {
font-family: var(--mono); font-size: 12px; background: var(--bg-code);
@@ -448,6 +462,71 @@ body.no-side #side { pointer-events: none; }
#composer > * { max-width: 760px; margin: 0 auto; }
.pending { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 0 8px; }
.pending .chip { background: var(--bg-soft); border-color: var(--line); }
.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);
color: var(--fg-dim); font-size: 12.5px;
}
.edit-bar[hidden] { display: none; }
.msg.user .msg-actions { justify-content: flex-end; padding-left: 0; }
.sess-search {
width: calc(100% - 8px); margin: 0 4px 8px; height: 30px; padding: 0 10px;
border-radius: 9px; border: 1px solid var(--line); background: var(--bg-soft);
color: var(--fg); font-size: 13px;
}
.sess-item { cursor: pointer; }
#modal-box .sess-list { max-height: min(56vh, 520px); overflow-y: auto; margin: 4px 0 10px; }
#modal-box .sess button { flex: 0 0 auto; }
.sess-item .more {
flex: 0 0 auto; align-self: center; width: 22px; height: 22px; border-radius: 6px;
display: grid; place-items: center; color: var(--fg-faint); opacity: 0;
transition: opacity var(--t-fast), background var(--t-fast);
}
.sess-item:hover .more, .sess-item .more:focus-visible { opacity: 1; }
.sess-item .more:hover { background: var(--bg-hover); color: var(--fg); }
.sess-item .more .i { width: 16px; height: 16px; }
#modal-box .sess button.danger { color: var(--danger, #e5484d); }
button.fstat { width: 100%; text-align: left; border-radius: 7px; transition: background var(--t-fast); }
button.fstat:hover { background: var(--bg-hover); }
.danger-text { color: var(--danger); }
#modal-box.wide { width: min(980px, 100%); }
#modal-box h4.sub { margin: 16px 0 6px; font-size: 13px; color: var(--fg-dim); font-weight: 650; }
#modal-box .field select, #modal-box .field input[type=number] {
flex: 1 1 auto; min-width: 150px; background: var(--bg-lift); color: var(--fg);
border: 1px solid var(--line); border-radius: 10px; padding: 7px 10px; outline: none;
}
#modal-box .field > span { width: 84px; }
#modal-box .checks { display: flex; flex-wrap: wrap; gap: 14px; }
#modal-box .check { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
#modal-box .tool-list { max-height: 34vh; overflow-y: auto; border: 1px solid var(--line); border-radius: 10px; padding: 4px; }
#modal-box .tool-row {
display: flex; align-items: center; gap: 8px; padding: 5px 6px; border-radius: 7px;
font-size: 12.5px; cursor: pointer;
}
#modal-box .tool-row:hover { background: var(--bg-hover); }
#modal-box .tool-row.dim code { opacity: .55; }
#modal-box .tool-row code { font-family: var(--mono); font-size: 12px; flex: 0 0 auto; }
#modal-box .tool-row .perm { flex: 0 0 auto; color: var(--fg-faint); font-size: 11px; border: 1px solid var(--line); border-radius: 6px; padding: 0 5px; }
#modal-box .tool-row .desc { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg-dim); }
#modal-box .diff-files { display: flex; flex-direction: column; gap: 1px; max-height: 24vh; overflow-y: auto; margin-bottom: 10px; }
#modal-box .diff-file {
display: flex; gap: 10px; align-items: center; text-align: left; padding: 5px 8px;
border-radius: 7px; font-size: 12.5px;
}
#modal-box .diff-file:hover { background: var(--bg-hover); }
#modal-box .diff-file.on { background: var(--accent-weak); }
#modal-box .diff-file .st { flex: 0 0 52px; color: var(--fg-faint); font-size: 11.5px; }
#modal-box .diff-file .p { font-family: var(--mono); overflow-wrap: anywhere; }
pre.diff {
margin: 0; padding: 10px 0; max-height: 52vh; overflow: auto; border-radius: 10px;
background: var(--bg-soft); border: 1px solid var(--line);
font-family: var(--mono); font-size: 12px; line-height: 1.5;
}
pre.diff span { display: block; padding: 0 12px; white-space: pre; min-height: 1.5em; }
pre.diff .add { color: var(--text-success); background: color-mix(in srgb, var(--text-success) 10%, transparent); }
pre.diff .del { color: var(--text-error); background: color-mix(in srgb, var(--text-error) 10%, transparent); }
pre.diff .hunk { color: var(--accent); }
pre.diff .h { color: var(--fg-faint); font-weight: 600; }
.box {
border: 1px solid var(--border-medium); border-radius: var(--radius-lg);
@@ -659,6 +738,60 @@ body.no-panel #panel { pointer-events: none; }
#modal-box .sess .id { font-family: var(--mono); font-size: 11.5px; color: var(--fg-faint); }
#modal-box .sess .t { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; }
/* --- commands: the list over the composer, the palette, the help sheet ---- */
#composer { position: relative; }
.slash {
position: absolute; left: 20px; right: 20px; bottom: calc(100% - 6px); z-index: 20;
max-height: min(46vh, 360px); overflow-y: auto; padding: 5px;
background: var(--main-surface-tertiary); border: 1px solid var(--border-medium);
border-radius: 14px; box-shadow: var(--shadow); animation: pop-in 140ms var(--ease) both;
}
.slash[hidden] { display: none; }
.slash-item, .palette-item {
display: flex; align-items: baseline; gap: 10px; width: 100%; padding: 7px 10px;
border-radius: 9px; text-align: left; font-size: 13px;
}
.slash-item.on, .palette-item.on { background: var(--accent-weak); }
.slash-item:hover, .palette-item:hover { background: var(--bg-hover); }
.slash-item code, .palette-item code { font-family: var(--mono); font-size: 12.5px; color: var(--fg); flex: 0 0 auto; }
.slash-item.on code, .palette-item.on code { color: var(--accent); }
.slash-item .hint { font-family: var(--mono); font-size: 11.5px; color: var(--fg-faint); flex: 0 0 auto; }
.slash-item .desc, .palette-item .desc {
flex: 1 1 auto; min-width: 0; color: var(--fg-dim); font-size: 12.5px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#modal-box .palette-list { max-height: min(52vh, 460px); overflow-y: auto; margin-top: 8px; }
kbd {
font-family: var(--mono); font-size: 11px; color: var(--fg-dim); white-space: nowrap;
border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 6px; padding: 1px 6px;
background: var(--bg-soft);
}
.palette-item kbd { flex: 0 0 auto; }
#modal-box .keys-grid {
display: grid; grid-template-columns: max-content 1fr; gap: 7px 14px; align-items: baseline;
font-size: 13px;
}
#modal-box .keys-grid kbd { justify-self: start; }
#modal-box .keys-grid.cmds code { font-family: var(--mono); font-size: 12.5px; }
#modal-box .keys-grid span { color: var(--fg-dim); }
#modal-box .ext-list { display: flex; flex-direction: column; gap: 2px; }
#modal-box .ext-row {
display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: 8px;
border-bottom: 1px solid var(--line);
}
#modal-box .ext-row .pm { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
#modal-box .ext-row .pm > div:last-child:not(.ext-title) { overflow-wrap: anywhere; }
#modal-box .ext-title { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-size: 13.5px; }
#modal-box .ext-badge {
font-size: 11px; border: 1px solid var(--line-strong); border-radius: 6px; padding: 0 6px; color: var(--fg-dim);
}
#modal-box .ext-badge.ok { color: var(--accent); border-color: var(--accent-line); }
#modal-box .ext-badge.bad { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 55%, transparent); }
#modal-box .ext-row > button {
flex: 0 0 auto; height: 30px; padding: 0 12px; border-radius: 9px; font-weight: 600; font-size: 13px;
border: 1px solid var(--line-strong); background: var(--bg-lift); white-space: nowrap;
}
/* --- narrow screens: rail and panel become drawers ------------------------ */
#backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, .45); z-index: 30; display: none; animation: fade-in var(--t) var(--ease) both; }
body.drawer-open #backdrop { display: block; }
@@ -677,7 +810,7 @@ body.drawer-open #backdrop { display: block; }
#to-bottom { bottom: 150px; right: 16px; }
.hero h1 { font-size: 21px; }
.msg.user .body { max-width: 88%; }
.turn-meta, .msg-actions, .turn-steps { padding-left: 0; }
.turn-meta, .msg-actions, .turn-steps, .note-line { padding-left: 0; }
}
@media (min-width: 901px) { #backdrop { display: none !important; } }