mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
feat: skill fast-path hardening, lineage retune + colors, per-tab pop-out, reliable tab alerts
- SKILL.md: forbid the standalone preamble check and pre-spawn recon turns (measured: two wasted model turns cost ~12s of a 28s two-worker run; the hardened flow measured 20.2s cold / 12.8s warm end to end) - Lineage lines: dip now hangs from the strip's bottom edge (cap 104 -> 64, no stacked row offsets), fixing the deep bow in wrapped strips and keeping row-1 arcs off row-2 tab labels; per-child color palette (skin blue first, then matrix green, pink, violet, red, turquoise, orange) via an inline --lineage-color custom property - Session Options -> Session: per-TAB pop-out (open-in-window) button override on top of the general showTabDetachButton setting; per-device localStorage map rendered as the tab-show-detach class - Tab alerts: seed the pending-hook state machine from GET /api/approvals regardless of the approvals-inbox setting (reloads used to lose the red tab entirely with the inbox off), clear unconditionally on approval_resolved, and repaint the alert as a steady red/yellow ring + glow + status dot on a ::before overlay so it stays visible on the selected (active) tab until the permission is actually resolved - docs: worker warm-pool design sketch (verified numbers baked in) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3992,7 +3992,7 @@ class CodemanApp {
|
||||
? (session.workingDir ? `${parsedName.prefix} (${session.workingDir})` : parsedName.prefix)
|
||||
: (session.workingDir || '');
|
||||
|
||||
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}${loadState ? ' tab-loading' : ''}" data-id="${id}" data-color="${color}" ${loadState ? `data-load-phase="${escapeHtml(loadState.phase)}"` : ''} onclick="app.handleSessionTabClick(event, ${escapeHtml(JSON.stringify(id))})" oncontextmenu="event.preventDefault(); app.startInlineRename(${escapeHtml(JSON.stringify(id))})" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-busy="${loadState ? 'true' : 'false'}" aria-label="${escapeHtml(name)} session" ${tabTooltip ? `title="${escapeHtml(tabTooltip)}"` : ''}>
|
||||
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}${loadState ? ' tab-loading' : ''}${this.hasTabDetachOverride(id) ? ' tab-show-detach' : ''}" data-id="${id}" data-color="${color}" ${loadState ? `data-load-phase="${escapeHtml(loadState.phase)}"` : ''} onclick="app.handleSessionTabClick(event, ${escapeHtml(JSON.stringify(id))})" oncontextmenu="event.preventDefault(); app.startInlineRename(${escapeHtml(JSON.stringify(id))})" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-busy="${loadState ? 'true' : 'false'}" aria-label="${escapeHtml(name)} session" ${tabTooltip ? `title="${escapeHtml(tabTooltip)}"` : ''}>
|
||||
${_tabIdx < 9 ? '<span class="tab-number">' + (_tabIdx + 1) + '</span>' : ''}
|
||||
${loadState ? '<span class="tab-load-spinner" aria-hidden="true"></span>' : ''}
|
||||
<span class="tab-status ${status}" aria-hidden="true"></span>
|
||||
|
||||
@@ -37,13 +37,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
async seedApprovals() {
|
||||
if (!this.approvals) this.approvals = new Map();
|
||||
this.approvals.clear();
|
||||
if (this.approvalsInboxEnabled()) {
|
||||
const data = await this._apiJson('/api/approvals');
|
||||
for (const item of (data && data.approvals) || []) {
|
||||
this.approvals.set(item.id, item);
|
||||
// Re-arm the tab alert state machine (idempotent set-add).
|
||||
this.setPendingHook(item.sessionId, approvalKindToHook(item.kind));
|
||||
}
|
||||
// ⚠ Fetch and re-arm the tab-alert state machine REGARDLESS of the inbox
|
||||
// setting. The server-side approval store runs unconditionally (only the
|
||||
// inbox SURFACES are opt-in), and the red/yellow tab alert predates the
|
||||
// inbox: gating the seed on the setting meant that with the inbox off, a
|
||||
// reload landed with every alert store empty while a permission dialog sat
|
||||
// blocking a session (owner report 2026-08-15: rail said NEEDS YOU from
|
||||
// the live SSE event, the reloaded-elsewhere tab showed a plain green
|
||||
// dot). Only populating `this.approvals` (bell/drawer/answer strips) stays
|
||||
// behind the setting.
|
||||
const data = await this._apiJson('/api/approvals');
|
||||
const inboxOn = this.approvalsInboxEnabled();
|
||||
for (const item of (data && data.approvals) || []) {
|
||||
if (inboxOn) this.approvals.set(item.id, item);
|
||||
// Re-arm the tab alert state machine (idempotent set-add).
|
||||
this.setPendingHook(item.sessionId, approvalKindToHook(item.kind));
|
||||
}
|
||||
this.renderApprovals();
|
||||
},
|
||||
@@ -68,14 +76,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
_onApprovalResolved(info) {
|
||||
if (!info || !info.id || !this.approvals) return;
|
||||
if (this.approvals.delete(info.id)) {
|
||||
// Clear the matching tab alert: the inbox resolves on more signals than
|
||||
// the hook handlers do (superseded, expired, answered from another
|
||||
// device), and clearPendingHooks is a no-op when nothing is set.
|
||||
this.clearPendingHooks(info.sessionId, approvalKindToHook(info.kind));
|
||||
this.renderApprovals();
|
||||
}
|
||||
if (!info || !info.id) return;
|
||||
// Clear the matching tab alert UNCONDITIONALLY: the inbox resolves on more
|
||||
// signals than the hook handlers do (superseded, expired, answered from
|
||||
// another device), clearPendingHooks is a no-op when nothing is set, and
|
||||
// with the inbox setting OFF the item was never stored in `this.approvals`
|
||||
// even though seedApprovals armed the alert — gating the clear on a map hit
|
||||
// would strand that alert forever.
|
||||
this.clearPendingHooks(info.sessionId, approvalKindToHook(info.kind));
|
||||
if (this.approvals?.delete(info.id)) this.renderApprovals();
|
||||
},
|
||||
|
||||
// ─── Actions ─────────────────────────────────────────────────
|
||||
|
||||
+32
-20
@@ -222,23 +222,35 @@ function computeTabScrollLeft(input) {
|
||||
// endpoint scrolled outside the strip. `.session-tabs` is `overflow-x: auto`, so a
|
||||
// scrolled-out tab still HAS a rect — one lying over the logo or the header
|
||||
// buttons. Skipping is honest; clamping would point at a tab that isn't there.
|
||||
// ⚠ THE DIP IS WHAT MAKES THE ARC AN ARC, and the first shipped numbers were tuned
|
||||
// against two tabs sitting side by side. A worker the agent skill starts is appended
|
||||
// to the END of the strip, so the real span between a lead and its worker is 800-1500px,
|
||||
// not 200, and a 44px cap over 1300px of span is a 33px sag, i.e. a line that reads as
|
||||
// STRAIGHT and crosses the terminal instead of bracketing under the strip. The dip now
|
||||
// keeps growing with the span (0.085/px, ~3x steeper against the old cap) so the bracket
|
||||
// survives the distance the feature is actually used at. The ceiling is what keeps a
|
||||
// full-width pair out of the terminal's fourth line: 104 + the sibling step lands the
|
||||
// deepest sag around y=140 on a 1080 screen, the same proportion two adjacent tabs get.
|
||||
// ⚠ THE DIP IS WHAT MAKES THE ARC AN ARC, and it has now been mis-tuned in BOTH
|
||||
// directions, so treat these numbers as a corridor rather than a dial to crank:
|
||||
// - Too shallow (the first ship, 44px cap): a skill worker is appended to the END of
|
||||
// the strip, so a lead-to-worker span is 800-1500px, and a 44px cap over 1300px is
|
||||
// a 33px sag, a line that reads as STRAIGHT across the terminal (#285).
|
||||
// - Too deep (the 104px cap that replaced it): in the wrapped-strip case the cap and
|
||||
// the FULL row offset stacked, bowing the bracket ~106px into the terminal text
|
||||
// (owner screenshot 2026-08-15, "die Linien machen einen grossen Bogen nach unten").
|
||||
// The dip is measured from the STRIP'S BOTTOM EDGE (falling back to the lower tab
|
||||
// bottom when the strip rect is missing or shorter than its tabs), which buys two
|
||||
// things at once: the bow needs no per-row offsets stacked on top, and a same-row
|
||||
// arc between ROW-1 tabs of a wrapped strip clears row 2's labels instead of being
|
||||
// drawn through them (the retune's own first draft had exactly that regression).
|
||||
const LINEAGE_DIP_BASE_PX = 14;
|
||||
const LINEAGE_DIP_PER_PX = 0.085;
|
||||
const LINEAGE_DIP_PER_PX = 0.06;
|
||||
const LINEAGE_DIP_MIN_PX = 22;
|
||||
const LINEAGE_DIP_MAX_PX = 104;
|
||||
const LINEAGE_DIP_MAX_PX = 64;
|
||||
// Siblings nest by this much. Widened with the stroke: at 2.5px plus its glow, arcs 6px
|
||||
// apart bled into one thick band instead of reading as three separate lines.
|
||||
const LINEAGE_SIBLING_STEP_PX = 8;
|
||||
const LINEAGE_STRIP_TOLERANCE_PX = 4;
|
||||
// Lineage palette, assigned per CHILD in first-seen order and cycled (session-lineage.js).
|
||||
// The empty FIRST entry means "no override": the CSS then falls back to --session-blue,
|
||||
// which every skin block tunes for its own background, so a lone arc keeps the
|
||||
// skin-aware blue that shipped in 1.18.2. The fixed entries are deliberately vivid
|
||||
// (owner call 2026-08-15: matrix green, pinkish, violet, red, turquoise "and so on");
|
||||
// they ride the same double glow as the blue, which is what keeps them legible over
|
||||
// terminal text on every skin.
|
||||
const LINEAGE_COLORS = ['', '#00ff66', '#ff5ea8', '#a78bfa', '#ff5252', '#2dd4bf', '#ffa940'];
|
||||
|
||||
function computeLineagePath(input) {
|
||||
const parent = input?.parent;
|
||||
@@ -269,18 +281,17 @@ function computeLineagePath(input) {
|
||||
const cBottom = cTop + ch;
|
||||
const sameRow = Math.abs(pTop + ph / 2 - (cTop + ch / 2)) <= Math.min(ph, ch) / 2;
|
||||
|
||||
// Both ends anchor on the tab BOTTOM, and the control points hang below whichever
|
||||
// row is lower, so one formula covers a flat strip and a wrapped one.
|
||||
// Both ends anchor on the tab BOTTOM, and the control points hang below the WHOLE
|
||||
// strip, so one formula covers a flat strip, a wrapped pair, and a same-row pair
|
||||
// sitting above further rows (see the corridor note above the constants).
|
||||
const span = Math.abs(cx - px);
|
||||
const rowDrop = Math.abs(cBottom - pBottom);
|
||||
// ⚠ A wrapped pair needs the dip measured from the LOWER row, or the bracket would
|
||||
// only reach the row gap again. Adding the row offset also keeps the curve clear of
|
||||
// the row it crosses instead of grazing its bottom edge.
|
||||
const stripBottom =
|
||||
strip && Number(strip.height) > 0 ? Number(strip.top) + Number(strip.height) : Number.NEGATIVE_INFINITY;
|
||||
const baseline = Math.max(pBottom, cBottom, stripBottom);
|
||||
const dip =
|
||||
Math.min(LINEAGE_DIP_MAX_PX, Math.max(LINEAGE_DIP_MIN_PX, LINEAGE_DIP_BASE_PX + span * LINEAGE_DIP_PER_PX)) +
|
||||
depth * LINEAGE_SIBLING_STEP_PX +
|
||||
rowDrop;
|
||||
const yc = Math.max(pBottom, cBottom) + dip;
|
||||
depth * LINEAGE_SIBLING_STEP_PX;
|
||||
const yc = baseline + dip;
|
||||
const d = `M ${r1(px)} ${r1(pBottom)} C ${r1(px)} ${r1(yc)}, ${r1(cx)} ${r1(yc)}, ${r1(cx)} ${r1(cBottom)}`;
|
||||
return { d, endX: cx, endY: cBottom, sameRow };
|
||||
}
|
||||
@@ -441,6 +452,7 @@ if (typeof window !== 'undefined') {
|
||||
DIP_MIN_PX: LINEAGE_DIP_MIN_PX,
|
||||
DIP_MAX_PX: LINEAGE_DIP_MAX_PX,
|
||||
SIBLING_STEP_PX: LINEAGE_SIBLING_STEP_PX,
|
||||
COLORS: LINEAGE_COLORS,
|
||||
};
|
||||
window.CodemanConnectionLoss = {
|
||||
compute: computeConnectionLossUi,
|
||||
|
||||
@@ -1211,6 +1211,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="set-row" data-search="pop out detach tab window this session">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Pop-out button on this tab</span>
|
||||
<span class="set-row-desc">Show the open-in-a-window button on this tab even while the general App Settings toggle is off.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="sessionOptShowTabDetach" onchange="app.onSessionTabDetachToggle(this.checked)"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*
|
||||
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||
* @dependency subagent-windows.js (_updateConnectionLinesImmediate, #connectionLines)
|
||||
* @dependency constants.js (window.CodemanLineage.computePath)
|
||||
* @dependency constants.js (window.CodemanLineage.computePath + .COLORS)
|
||||
* @dependency settings-ui.js (loadAppSettingsFromStorage, getDefaultSettings)
|
||||
* @loadorder 15.6 (after ultracode-windows.js — appended to the same SVG pass)
|
||||
*/
|
||||
@@ -90,6 +90,35 @@ Object.assign(CodemanApp.prototype, {
|
||||
return edges;
|
||||
},
|
||||
|
||||
/**
|
||||
* Colour for one child's arc, from CodemanLineage.COLORS, assigned in FIRST-SEEN
|
||||
* order and remembered per child id. First-seen rather than draw-index keeps a
|
||||
* line's colour stable across re-renders, tab reorders and sibling closes (the
|
||||
* SVG is wiped and rebuilt constantly, so an index-based colour would flicker).
|
||||
* An empty string means "no override": the CSS falls back to --session-blue.
|
||||
*/
|
||||
_lineageColorFor(childId) {
|
||||
const palette = (window.CodemanLineage && window.CodemanLineage.COLORS) || [];
|
||||
if (palette.length === 0) return '';
|
||||
if (!this._lineageColorByChild) {
|
||||
this._lineageColorByChild = new Map();
|
||||
this._lineageColorNext = 0;
|
||||
}
|
||||
let idx = this._lineageColorByChild.get(childId);
|
||||
if (idx === undefined) {
|
||||
idx = this._lineageColorNext++ % palette.length;
|
||||
this._lineageColorByChild.set(childId, idx);
|
||||
// Bounded: entries for long-gone sessions are pruned once the map is clearly
|
||||
// stale, so a day-long dashboard cannot grow it without limit.
|
||||
if (this._lineageColorByChild.size > 200 && this.sessions) {
|
||||
for (const key of this._lineageColorByChild.keys()) {
|
||||
if (!this.sessions.has(key)) this._lineageColorByChild.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return palette[idx] || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Append the lineage layer to the shared SVG pass.
|
||||
*
|
||||
@@ -137,6 +166,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
// the line itself. `status` is the CHILD's, which is the interesting end.
|
||||
const working = edge.status === 'working' ? ' lineage-line--working' : '';
|
||||
line.setAttribute('class', 'connection-line lineage-line' + working);
|
||||
// Per-child colour rides a CSS custom property so the stylesheet keeps owning
|
||||
// opacity, glow and dash; an empty colour leaves the --session-blue fallback.
|
||||
const color = this._lineageColorFor(edge.childId);
|
||||
if (color) line.style.setProperty('--lineage-color', color);
|
||||
// `data-agent-id` is what _applyLineEntrances() queries — see the file header.
|
||||
line.setAttribute('data-agent-id', 'lineage:' + edge.childId);
|
||||
line.setAttribute('data-parent-tab', edge.parentId);
|
||||
@@ -153,6 +186,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
dot.setAttribute('r', '3.5');
|
||||
dot.setAttribute('class', 'lineage-line-dot' + working);
|
||||
dot.setAttribute('data-child-tab', edge.childId);
|
||||
if (color) dot.style.setProperty('--lineage-color', color);
|
||||
svg.appendChild(dot);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1283,12 +1283,65 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Session Options Modal
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Per-TAB pop-out button override (Session Options → Session → Identity). The
|
||||
* general `showTabDetachButton` App Setting stays the per-device default for ALL
|
||||
* tabs; this map whitelists single sessions on top of it, so one tab can carry
|
||||
* the ⧉ button while the general toggle stays off. Per-device on purpose, like
|
||||
* the general setting: it is a display choice, so it lives in localStorage and
|
||||
* never touches the server schema. Rendered as the `tab-show-detach` class on
|
||||
* the tab (see _fullRenderSessionTabs), which styles.css exempts from the
|
||||
* global `display: none` gate; the active-tab reveal rules stay shared, so an
|
||||
* overridden tab behaves exactly like a tab under the general toggle.
|
||||
*/
|
||||
_tabDetachOverrides() {
|
||||
if (this._tabDetachOverrideMap === undefined) {
|
||||
try {
|
||||
this._tabDetachOverrideMap = JSON.parse(localStorage.getItem('codeman:tab-detach-overrides') || '{}') || {};
|
||||
} catch (_e) {
|
||||
this._tabDetachOverrideMap = {};
|
||||
}
|
||||
}
|
||||
return this._tabDetachOverrideMap;
|
||||
},
|
||||
|
||||
hasTabDetachOverride(sessionId) {
|
||||
return !!this._tabDetachOverrides()[sessionId];
|
||||
},
|
||||
|
||||
onSessionTabDetachToggle(on) {
|
||||
const id = this.editingSessionId;
|
||||
if (!id) return;
|
||||
const map = this._tabDetachOverrides();
|
||||
if (on) map[id] = 1;
|
||||
else delete map[id];
|
||||
// Prune ids whose sessions are gone, so closed sessions cannot grow the map.
|
||||
for (const key of Object.keys(map)) {
|
||||
if (key !== id && this.sessions && !this.sessions.has(key)) delete map[key];
|
||||
}
|
||||
try {
|
||||
localStorage.setItem('codeman:tab-detach-overrides', JSON.stringify(map));
|
||||
} catch (_e) {
|
||||
/* storage full/blocked: the in-memory map still applies this page load */
|
||||
}
|
||||
// Apply to the LIVE tab directly: the debounced render may take the
|
||||
// incremental path (same session set), which patches rather than rebuilds,
|
||||
// so the template's class would only land on the next full render. Future
|
||||
// full renders re-emit it from _fullRenderSessionTabs.
|
||||
const tab = document.querySelector(`.session-tab[data-id="${CSS.escape(id)}"]`);
|
||||
if (tab) tab.classList.toggle('tab-show-detach', !!on);
|
||||
},
|
||||
|
||||
openSessionOptions(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
this.editingSessionId = sessionId;
|
||||
|
||||
// Per-tab pop-out override state (see _tabDetachOverrides above).
|
||||
const detachToggle = document.getElementById('sessionOptShowTabDetach');
|
||||
if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId);
|
||||
|
||||
// Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only)
|
||||
const isAltMode = session.mode === 'opencode' || session.mode === 'codex' || session.mode === 'gemini' || session.mode === 'antigravity' || session.mode === 'pi';
|
||||
this.switchOptionsTab(isAltMode ? 'summary' : 'respawn');
|
||||
|
||||
+78
-19
@@ -1459,23 +1459,73 @@ html[data-line-anim="packet"] .connection-line.line-enter {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* Tab alert animations */
|
||||
.session-tab.tab-alert-action {
|
||||
/* Tab alerts: a STEADY red/yellow base with a pulse breathing on top.
|
||||
⚠ The original animation swung background AND border to transparent at its
|
||||
0%/100% keyframes, so for roughly half of every cycle an alerted tab was
|
||||
indistinguishable from a normal one: a glance (or a screenshot, owner report
|
||||
2026-08-15) read "no alert" while the home rail showed a steady NEEDS YOU.
|
||||
A pending permission is BLOCKING the agent, so the tab must look blocked at
|
||||
every instant; only the intensity is allowed to move. The status dot joins
|
||||
in (red/yellow, (0,4,0) so it outranks the skin block's (0,3,1) dot rules),
|
||||
mirroring the phone overview's red-row language. */
|
||||
/* The alert paints on ::before, NEVER on the tab element: .session-tab.active
|
||||
forces background/border/box-shadow with !important, and !important beats
|
||||
even a running animation, so an element-level alert vanished the moment the
|
||||
tab was selected. The permission is still blocking while you look at it, so
|
||||
the red ring must survive selection and clear only on resolution (owner call
|
||||
2026-08-15). Same convention as the entrance styles (see the tab-enter block).
|
||||
(0,3,x) via the strip parent on purpose: the non-OG skin block quiets
|
||||
decorative glows (`.tab-glow { box-shadow: none }` lands at (0,2,1)), and an
|
||||
alert halo is signal, not decor, so it must outrank that on every skin.
|
||||
The overlay paints above the tab's inline content (positioned vs flow), which
|
||||
is fine at these alphas and is exactly what keeps it visible over the active
|
||||
tab's opaque-ish background. */
|
||||
.session-tabs .session-tab.tab-alert-action::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
border: 2px solid var(--red);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
|
||||
animation: tab-blink-red 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.session-tab.tab-alert-idle {
|
||||
.session-tab.tab-alert-action .tab-status.idle,
|
||||
.session-tab.tab-alert-action .tab-status.busy,
|
||||
.session-tab.tab-alert-action .tab-status {
|
||||
background: var(--red);
|
||||
box-shadow: 0 0 6px rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
.session-tabs .session-tab.tab-alert-idle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
border: 2px solid var(--yellow);
|
||||
background: rgba(234, 179, 8, 0.1);
|
||||
box-shadow: 0 0 8px rgba(234, 179, 8, 0.35);
|
||||
animation: tab-blink-yellow 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.session-tab.tab-alert-idle .tab-status.idle,
|
||||
.session-tab.tab-alert-idle .tab-status.busy,
|
||||
.session-tab.tab-alert-idle .tab-status {
|
||||
background: var(--yellow);
|
||||
box-shadow: 0 0 6px rgba(234, 179, 8, 0.6);
|
||||
}
|
||||
|
||||
@keyframes tab-blink-red {
|
||||
0%, 100% { background: transparent; border-color: transparent; }
|
||||
50% { background: rgba(239, 68, 68, 0.12); border-color: var(--red); }
|
||||
0%, 100% { background: rgba(239, 68, 68, 0.12); box-shadow: 0 0 8px rgba(239, 68, 68, 0.4); }
|
||||
50% { background: rgba(239, 68, 68, 0.3); box-shadow: 0 0 16px rgba(239, 68, 68, 0.75); }
|
||||
}
|
||||
|
||||
@keyframes tab-blink-yellow {
|
||||
0%, 100% { background: transparent; border-color: transparent; }
|
||||
50% { background: rgba(234, 179, 8, 0.1); border-color: var(--yellow); }
|
||||
0%, 100% { background: rgba(234, 179, 8, 0.1); box-shadow: 0 0 8px rgba(234, 179, 8, 0.35); }
|
||||
50% { background: rgba(234, 179, 8, 0.24); box-shadow: 0 0 14px rgba(234, 179, 8, 0.65); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
@@ -2081,8 +2131,12 @@ html[data-line-anim="packet"] .connection-line.line-enter {
|
||||
/* Pop-out button is opt-in (App Settings → Tab Bar, default off; per-device).
|
||||
settings-ui.js mirrors the setting as the tabs-show-detach class on <html>.
|
||||
A tab that is ALREADY detached keeps its icon regardless: it is the
|
||||
re-focus affordance for the popped-out window. */
|
||||
html:not(.tabs-show-detach) .session-tab:not(.detached) .tab-detach {
|
||||
re-focus affordance for the popped-out window. A SINGLE tab can also opt in
|
||||
via Session Options → Session (`tab-show-detach` on the tab, per-device map
|
||||
in session-ui.js) while the general toggle stays off; the active-tab reveal
|
||||
rules above are shared, so the overridden tab behaves identically. Phones are
|
||||
unaffected either way: mobile.css hides .tab-detach with !important. */
|
||||
html:not(.tabs-show-detach) .session-tab:not(.detached):not(.tab-show-detach) .tab-detach {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -9329,11 +9383,15 @@ kbd {
|
||||
Deliberately quieter and thinner than the subagent lines above so the two
|
||||
layers read as different things in the same SVG.
|
||||
|
||||
Colour comes from --session-blue, which EVERY skin block already defines and
|
||||
already tunes for its own background, so one rule covers all seven (the four
|
||||
light skins included). Do not add a per-skin `.lineage-line` override inside the
|
||||
html:not([data-skin="og"]) block: a bare class rule in there resolves to (0,2,1)
|
||||
and would outrank this one from a surprising place.
|
||||
Colour: every rule reads --lineage-color, which session-lineage.js sets INLINE
|
||||
per line from the CodemanLineage.COLORS palette (per child, first-seen order,
|
||||
owner call 2026-08-15: several connected tabs must get several colours). The
|
||||
FIRST line gets no override, so it falls through to --session-blue, which EVERY
|
||||
skin block already defines and tunes for its own background; a lone arc therefore
|
||||
still renders the skin-aware blue that shipped in 1.18.2. Do not add a per-skin
|
||||
`.lineage-line` override inside the html:not([data-skin="og"]) block: a bare
|
||||
class rule in there resolves to (0,2,1) and would outrank this one from a
|
||||
surprising place.
|
||||
|
||||
⚠ BLUE, NOT THE VIOLET THIS SHIPPED WITH (owner call, 2026-08-14: "make these
|
||||
lines in blue that they are better visible"). Violet sits close to the terminal's
|
||||
@@ -9352,13 +9410,13 @@ kbd {
|
||||
(4 4 on a 2.5px line reads as a dotted smudge), and `lineage-flow` marches by
|
||||
exactly two dash cycles, so it has to move with them. */
|
||||
.connection-line.lineage-line {
|
||||
stroke: var(--session-blue, #2b8fd9);
|
||||
stroke: var(--lineage-color, var(--session-blue, #2b8fd9));
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 5 5;
|
||||
stroke-linecap: round;
|
||||
opacity: 0.72;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7)) drop-shadow(0 0 5px var(--session-blue, #2b8fd9))
|
||||
drop-shadow(0 0 11px var(--session-blue, #2b8fd9));
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7)) drop-shadow(0 0 5px var(--lineage-color, var(--session-blue, #2b8fd9)))
|
||||
drop-shadow(0 0 11px var(--lineage-color, var(--session-blue, #2b8fd9)));
|
||||
}
|
||||
|
||||
/* ⚠ OUTSIDE the reduced-motion block below on purpose. A working child is the case
|
||||
@@ -9374,9 +9432,10 @@ kbd {
|
||||
}
|
||||
|
||||
.lineage-line-dot {
|
||||
fill: var(--session-blue, #2b8fd9);
|
||||
fill: var(--lineage-color, var(--session-blue, #2b8fd9));
|
||||
opacity: 0.85;
|
||||
filter: drop-shadow(0 0 4px var(--session-blue, #2b8fd9)) drop-shadow(0 0 9px var(--session-blue, #2b8fd9));
|
||||
filter: drop-shadow(0 0 4px var(--lineage-color, var(--session-blue, #2b8fd9)))
|
||||
drop-shadow(0 0 9px var(--lineage-color, var(--session-blue, #2b8fd9)));
|
||||
}
|
||||
|
||||
/* The child end marches while that worker is actually working, so the line
|
||||
|
||||
Reference in New Issue
Block a user