mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): each tile header names its harness and model
The tile header is now `● [logo] name · model ..... ⋯ ⤢ ×` (owner request): - The logo is PR #532's `run-mode-dot <cliId>` slot, so the logos, the skins and the plain dot of an id without a logo stay single-sourced in styles.css. The id is data (a class and a catalog lookup), never a branch; the frontend id-branching guard now also scans constants.js, terminal-split.js and tile-grid.js (the one existing shell branch in tile-grid.js, the attach route, is allowlisted with its reason). - The model is the session's displayModel, as text in a data-i18n-skip span inside a box whose tooltip may translate. Unknown means the logo alone. - The logo's tooltip and accessible name say "<harness> · <model>", plus where a model the CLI did not report came from ("set at launch", "custom endpoint"; zh-CN patterns for both, the names pass through). The model's box is aria-hidden so a screen reader hears the model once. - One painter, _paintSessionHarness (terminal-split.js, shared with the split panes next), diffs against what it last wrote, never the DOM: an unchanged session writes nothing on a tab render. - On a narrow header the model gives way first, then the name: the name does not shrink at all and is capped at its box, since any shrink factor takes a subpixel from a name that fits and ellipsizes it. The chrome and zoom tests found header parts by child position; they now look them up by class, with every assertion kept (the rename tests had been passing against the new logo node by position). The i18n harvester files the logo's labels as harness and model names that must stay as they are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2134,7 +2134,56 @@ function sessionIdFromFragment(hash) {
|
||||
return id && id.trim() ? id.trim() : null;
|
||||
}
|
||||
|
||||
/** Longest model name a session header shows (the server caps it as well). */
|
||||
const SESSION_MODEL_MAX_CHARS = 64;
|
||||
/** A CLI registry id (src/config/cli-registry/schema.ts); anything else is not a class name. */
|
||||
const CLI_ID_PATTERN = /^[a-z][a-z0-9-]{0,23}$/;
|
||||
|
||||
/**
|
||||
* What a session's header says about its harness: the CLI id (the
|
||||
* `run-mode-dot <id>` logo class), the registry's label for it, the model the
|
||||
* session runs when the server knows it (`SessionState.displayModel`), and the
|
||||
* tooltip naming both.
|
||||
*
|
||||
* The id is data: the label comes from the injected CLI catalog and falls back
|
||||
* to the id, so a CLI added through clis.json still gets a name. The model is
|
||||
* untrusted text (read off a pane, or a CLI's own report): control characters
|
||||
* are dropped and the length capped here too, and callers render it with
|
||||
* textContent. The tooltip says where a model that is not the CLI's own report
|
||||
* came from, so it never claims more than the server knows: one the session
|
||||
* was launched with may have been switched since, and a custom endpoint's
|
||||
* model is the endpoint's, whatever the CLI calls it.
|
||||
*
|
||||
* @param {object} session - a session from app.sessions
|
||||
* @param {Array<{id: string, label?: string}>} [catalog] - window.__codemanCliCatalog
|
||||
* @returns {{id: string, label: string, model: string, title: string}}
|
||||
*/
|
||||
function describeSessionHarness(session, catalog) {
|
||||
const id = typeof session?.mode === 'string' && CLI_ID_PATTERN.test(session.mode) ? session.mode : '';
|
||||
const entry = id && Array.isArray(catalog) ? catalog.find((cli) => cli?.id === id) : null;
|
||||
const label = (typeof entry?.label === 'string' && entry.label.trim()) || id;
|
||||
const raw = session?.displayModel?.model;
|
||||
const model =
|
||||
typeof raw === 'string'
|
||||
? raw
|
||||
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
||||
.trim()
|
||||
.slice(0, SESSION_MODEL_MAX_CHARS)
|
||||
: '';
|
||||
const source = session?.displayModel?.source;
|
||||
const qualifier = !model
|
||||
? ''
|
||||
: source === 'launch'
|
||||
? ' (set at launch)'
|
||||
: source === 'custom-endpoint'
|
||||
? ' (custom endpoint)'
|
||||
: '';
|
||||
const title = [label, model].filter(Boolean).join(' \u00B7 ') + qualifier;
|
||||
return { id, label, model, title };
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.CodemanSessionHarness = { describeSessionHarness, SESSION_MODEL_MAX_CHARS };
|
||||
window.CodemanHistoryFormat = { formatHistoryBytes, computeHistoryTruncationNotice, computeRewriteScrollLine };
|
||||
window.CodemanFilePaths = { absoluteFilePathPattern, previewsInFileViewer, FILE_PREVIEW_EXTENSIONS };
|
||||
window.CodemanTerminalLines = { terminalLogicalLine };
|
||||
|
||||
@@ -1058,6 +1058,11 @@
|
||||
],
|
||||
[/^The agent exited \((-?\d+)\)$/, (_m, code) => `智能体已退出(${code})`],
|
||||
[/^The agent exited \(signal (\d+)\)$/, (_m, signal) => `智能体已退出(信号 ${signal})`],
|
||||
// A session header's harness logo (tile grid, split pane): "<harness> · <model>",
|
||||
// and where the model came from when the CLI did not report it. The harness
|
||||
// and model names pass through untranslated.
|
||||
[/^(.+) \(set at launch\)$/, (_m, names) => `${names}(启动时设定)`],
|
||||
[/^(.+) \(custom endpoint\)$/, (_m, names) => `${names}(自定义端点)`],
|
||||
// The Run button's mode codes ("Run CC", "Run SH", "Run OC" ...; a registry
|
||||
// CLI's shortBadge too). Exact entries win first ("Run Shell", "Run OMP").
|
||||
[/^Run ([A-Z][A-Z0-9]{1,5})$/, (_m, code) => `运行 ${code}`],
|
||||
|
||||
@@ -19477,7 +19477,7 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Tile header: `● name ......... ⋯ ⤢ ×`. FIXED height (#464): the body below
|
||||
/* Tile header: `● [logo] name · model ..... ⋯ ⤢ ×`. FIXED height (#464): the body below
|
||||
holds the xterm, so a header that grew (a wrapped name, the rename input)
|
||||
would shrink the body with nothing refitting the xterm or its PTY. */
|
||||
.tile-header {
|
||||
@@ -19496,14 +19496,51 @@ html[data-session-list="sidebar"][data-sidebar="collapsed"] .btn-sidebar-toggle
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tile-name {
|
||||
/* The harness logo: PR #532's run-mode-dot slot (1.15em of this 13px, the
|
||||
size of the header's text), with the logos, the skins and the plain dot of an
|
||||
id without one all coming from the run-mode-dot rules. */
|
||||
.tile-harness.run-mode-dot {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Name and model. On a narrow header the model gives way first, then the name;
|
||||
the logo and the buttons keep their size. The name does not shrink at all (a
|
||||
shrink factor, however small, also takes a subpixel from a name that fits and
|
||||
ellipsizes it) and is capped at this box's width instead. */
|
||||
.tile-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tile-name {
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tile-model {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-muted, #888);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tile-model::before {
|
||||
content: '\00B7';
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.tile-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -261,6 +261,39 @@ Object.assign(CodemanApp.prototype, {
|
||||
// nothing on the button said which. `.split-open` + aria-pressed give it
|
||||
// the same active-state language as the codebase's other toggle buttons
|
||||
// (keyboard-accessory's Ctrl key, the voice-input mic).
|
||||
/**
|
||||
* Paints a session header's harness logo and model: a grid tile's, and the
|
||||
* split panes'. The logo is PR #532's `run-mode-dot <cliId>` slot (the id is
|
||||
* data, never a branch), the model is text (describeSessionHarness,
|
||||
* constants.js). Diffs against the values it last wrote, kept on `parts`,
|
||||
* never against the DOM, which the translator may have rewritten: an
|
||||
* unchanged session writes nothing, and this runs on every tab render.
|
||||
*
|
||||
* @param {{harness: HTMLElement, model: HTMLElement, modelName: HTMLElement}} parts - the
|
||||
* header's nodes (the model's box and the name inside it); the memo lives here too
|
||||
* @param {object} session - the session the header shows
|
||||
* @param {string} logoClass - the header's own class for its logo
|
||||
*/
|
||||
_paintSessionHarness(parts, session, logoClass) {
|
||||
const harness = window.CodemanSessionHarness.describeSessionHarness(session, window.__codemanCliCatalog);
|
||||
const cls = `${logoClass} run-mode-dot${harness.id ? ` ${harness.id}` : ''}`;
|
||||
if (parts.harnessClass !== cls) {
|
||||
parts.harnessClass = cls;
|
||||
parts.harness.className = cls;
|
||||
}
|
||||
if (parts.harnessTitle !== harness.title) {
|
||||
parts.harnessTitle = harness.title;
|
||||
parts.harness.title = harness.title;
|
||||
parts.harness.setAttribute('aria-label', harness.title);
|
||||
parts.model.title = harness.title;
|
||||
}
|
||||
if (parts.modelValue !== harness.model) {
|
||||
parts.modelValue = harness.model;
|
||||
parts.modelName.textContent = harness.model;
|
||||
parts.model.hidden = !harness.model;
|
||||
}
|
||||
},
|
||||
|
||||
_updateSplitButtonState(open) {
|
||||
const btn = document.querySelector('.btn-split');
|
||||
if (!btn) return;
|
||||
|
||||
@@ -732,7 +732,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
overlay: null,
|
||||
header: header.el,
|
||||
dot: header.dot,
|
||||
harness: header.harness,
|
||||
name: header.name,
|
||||
model: header.model,
|
||||
modelName: header.modelName,
|
||||
zoomBtn: header.zoomBtn,
|
||||
renaming: false,
|
||||
// The pid this tile last saw, so a pane that starts later is noticed.
|
||||
@@ -1075,9 +1078,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
/**
|
||||
* `● name ......... ⋯ ⤢ ×`: the status dot (the six-state classifier the tab
|
||||
* rows and both home screens share), the session name (double-click
|
||||
* renames), the session menu (the tab rail's own), zoom and remove-tile. Its
|
||||
* `● [logo] name · model ..... ⋯ ⤢ ×`: the status dot (the six-state
|
||||
* classifier the tab rows and both home screens share), the harness logo, the
|
||||
* session name (double-click renames), the model it runs when known, the
|
||||
* session menu (the tab rail's own), zoom and remove-tile. Its
|
||||
* buttons stop pointerdown, so acting on a tile that is not focused does not
|
||||
* also focus it (and spend its idle alert).
|
||||
*/
|
||||
@@ -1087,6 +1091,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'tile-dot home-sessions-dot home-sessions-dot--idle';
|
||||
dot.setAttribute('aria-hidden', 'true');
|
||||
// The harness's logo: PR #532's `run-mode-dot <cliId>` slot, so the logos,
|
||||
// the skins and the plain dot for an id without one stay in styles.css. Its
|
||||
// tooltip and accessible name carry the harness and the model in full
|
||||
// (_paintSessionHarness).
|
||||
const harness = document.createElement('span');
|
||||
harness.className = 'tile-harness run-mode-dot';
|
||||
harness.setAttribute('role', 'img');
|
||||
const title = document.createElement('span');
|
||||
title.className = 'tile-title';
|
||||
const name = document.createElement('span');
|
||||
name.className = 'tile-name';
|
||||
// A session literally named like a UI string ("Sessions") must not be translated.
|
||||
@@ -1095,6 +1108,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
e.stopPropagation();
|
||||
this.startTileRename(sessionId);
|
||||
});
|
||||
// The model the session runs, when the server knows it: the name itself is
|
||||
// never translated (data-i18n-skip on the inner span), the tooltip on the
|
||||
// outer one may be. Hidden from screen readers: the logo's accessible name
|
||||
// already says it.
|
||||
const model = document.createElement('span');
|
||||
model.className = 'tile-model';
|
||||
model.setAttribute('aria-hidden', 'true');
|
||||
model.hidden = true;
|
||||
const modelName = document.createElement('span');
|
||||
modelName.setAttribute('data-i18n-skip', '');
|
||||
model.appendChild(modelName);
|
||||
title.append(name, model);
|
||||
const actions = document.createElement('span');
|
||||
actions.className = 'tile-actions';
|
||||
const button = (cls, label, glyph, onClick) => {
|
||||
@@ -1122,13 +1147,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
// behind the menu's Close session and its confirm.
|
||||
button('tile-remove', 'Remove tile (the session keeps running)', '\u00D7', () => this.removeTile(sessionId))
|
||||
);
|
||||
el.append(dot, name, actions);
|
||||
return { el, dot, name, zoomBtn };
|
||||
el.append(dot, harness, title, actions);
|
||||
return { el, dot, harness, name, model, modelName, zoomBtn };
|
||||
},
|
||||
|
||||
/**
|
||||
* Refreshes one tile's header from the session: dot state, name, the hover
|
||||
* label ("working 3m") and the `needs` border. Diffs on existing nodes only,
|
||||
* Refreshes one tile's header from the session: dot state, harness logo,
|
||||
* name, model, the hover label ("working 3m") and the `needs` border. Diffs on existing nodes only,
|
||||
* and cheap: it runs on every tab render (every status change).
|
||||
*/
|
||||
_renderTileHeader(sessionId) {
|
||||
@@ -1152,6 +1177,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const name =
|
||||
this._inlineRenamePending?.get(sessionId) || this.getSessionName?.(session) || session.name || 'Session';
|
||||
if (entry.name.textContent !== name) entry.name.textContent = name;
|
||||
this._paintSessionHarness(entry, session, 'tile-harness');
|
||||
// A permission prompt is visible across the room.
|
||||
entry.el.classList.toggle('tile--needs', state === 'needs');
|
||||
this._renderTileOverlay(sessionId);
|
||||
|
||||
Reference in New Issue
Block a user