mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
The Project chat's work pointer labelled its button with the root card's title verbatim; for a finished task that is the last progress headline, and a plan_task DEGRADED verdict runs to ~400 characters. The base .btn wraps text and the pointer grew to the full status bar width, so the button became a 12-line block covering the reading area. The label now names the card (projectWorkLabel: coined name first, else the title, whitespace-normalized and capped) inside a label span that the CSS ellipsizes to one line; the bar keeps its original 180px row policy (a default desktop panel renders one row, a narrower panel or a phone wraps to a second row, never a third). Without a represented root card the pointer and its coverage note are hidden instead of a disabled placeholder button. The three DOM test stubs' prepend() accept several nodes like the real DOM. Docs updated (DESIGN, ARCHITECTURE, DEVELOPMENT). The nano context-mode segment's dangling var(--purple) now uses the existing --project / --project-18 pair, which was the only red CI test on the base (test_web_typography_static).
73 lines
3.2 KiB
JavaScript
73 lines
3.2 KiB
JavaScript
/** A navigation pointer to existing Project cards, never another work dashboard. */
|
|
import { compareHistoryPosition } from './chat_history_replay.js';
|
|
|
|
// The pointer names the card; it never restates the card's latest status line
|
|
// (a plan_task verdict runs to ~400 characters, and the card itself clamps its
|
|
// title to two lines). One line of text is the whole budget: the CSS ellipsizes
|
|
// the rest, and this cap keeps the accessible name short on every surface. The
|
|
// complete text stays on the card, one click away — no mouse-only tooltip.
|
|
export const POINTER_NAME_CHARS = 120;
|
|
|
|
export function projectWorkTarget(records = []) {
|
|
const roots = records.filter(record => !record.isSubagent && record.root?.isConnected)
|
|
.sort((a, b) => Number(a.root.dataset?.ts || 0) - Number(b.root.dataset?.ts || 0)
|
|
|| compareHistoryPosition(a.historyPosition, b.historyPosition));
|
|
return roots.filter(record => !record.finished).at(-1) || roots.at(-1) || null;
|
|
}
|
|
|
|
/** `Working · <name>` / `Latest task · <name>`: the coined name first, else the
|
|
* card's own title, whitespace-normalized and capped to one line's worth. */
|
|
export function projectWorkLabel(target) {
|
|
if (!target) return '';
|
|
const name = String(target.suggestedName || target.titleEl?.textContent || '')
|
|
.replace(/\s+/g, ' ').trim() || 'Task';
|
|
const short = name.length > POINTER_NAME_CHARS
|
|
? `${name.slice(0, POINTER_NAME_CHARS - 1).trimEnd()}…`
|
|
: name;
|
|
return `${target.finished ? 'Latest task' : 'Working'} · ${short}`;
|
|
}
|
|
|
|
export function bindProjectWorkPointer(host, { records, getWindow, onNavigate }) {
|
|
const doc = host.ownerDocument;
|
|
const button = doc.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'btn btn-ghost project-work-pointer';
|
|
const label = doc.createElement('span');
|
|
label.className = 'project-work-pointer-label';
|
|
button.append(label);
|
|
const note = doc.createElement('span');
|
|
note.className = 'project-work-coverage';
|
|
let target = null;
|
|
let disposed = false;
|
|
const navigate = () => {
|
|
update();
|
|
if (target?.root?.isConnected) onNavigate(target.root);
|
|
};
|
|
function update() {
|
|
if (disposed) return;
|
|
target = projectWorkTarget([...records.values()]);
|
|
const text = projectWorkLabel(target);
|
|
if (label.textContent !== text) label.textContent = text;
|
|
// Without a represented card the pointer leads nowhere and takes no
|
|
// space; an absent card is not a claim that the Project has no work.
|
|
button.hidden = !target;
|
|
button.disabled = !target;
|
|
// A represented card is not a claim that all Project work/history is loaded.
|
|
const coverage = target && getWindow()?.complete !== true ? 'Loaded messages only' : '';
|
|
if (note.textContent !== coverage) note.textContent = coverage;
|
|
note.hidden = !coverage;
|
|
}
|
|
button.addEventListener('click', navigate);
|
|
host.prepend(button, note);
|
|
update();
|
|
return {
|
|
update,
|
|
destroy() {
|
|
disposed = true;
|
|
button.removeEventListener('click', navigate);
|
|
button.remove();
|
|
note.remove();
|
|
target = null;
|
|
},
|
|
};
|
|
}
|