mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat(tabs,header): group tabs by state and add header stats styles (#426)
Two directions from Discussion #426, each a per-device setting and the new default. Tab Grouping (tabGrouping: 'state' | 'none', default 'state', option C): tabs are grouped needs you (red, plus failed sessions), waiting (yellow), working and idle (also ended, exited panes and web tabs), most urgent on top. The desktop header strip draws a row per group with its label and count in a left gutter; the flat vertical rail and the sidebar draw a section per group; tablets keep their scrolling row with inline dividers; phones keep the chip row in group order without headings. Classification is the home screens' own (_mobileOverviewState/_mobileOverviewExit), the fold into four groups is pure in CodemanTabTriage (constants.js). It is flex `order` plus aria-hidden heading/break elements reconciled in place after both render paths, never a DOM reorder, so Alt+N, the keyboard walk and drag keep reading tab order; a drop is refused across groups. Named groups in the vertical rail take precedence. Header Stats Style (headerStatsStyle: 'classic' | 'compact' | 'tiles', default 'tiles', option G): tiles give WS, CPU, MEM and each plan window a label-over-value tile with a bar underneath; compact is one WS/CPU/MEM pill with sparklines plus a plan-ring pill; classic is the header as before. Desktop only (classic below 768px and in solo windows). The clustered styles move #connectionIndicator into #headerSystemStats and WS stays out of a hidden System Stats pill. The extra parts are always rendered and hidden by default in CSS, so classic is unchanged. Tests: test/tab-triage.test.ts, test/header-stats-style.test.ts; three source pins in test/tab-rail-order.test.ts follow renamed lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -382,6 +382,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Header visibility settings
|
||||
document.getElementById('appSettingsShowFontControls').checked = settings.showFontControls ?? defaults.showFontControls ?? false;
|
||||
document.getElementById('appSettingsShowSystemStats').checked = settings.showSystemStats ?? defaults.showSystemStats ?? true;
|
||||
document.getElementById('appSettingsHeaderStatsStyle').value = this.resolveHeaderStatsStyle(settings);
|
||||
document.getElementById('appSettingsShowLifecycleLog').checked = settings.showLifecycleLog ?? defaults.showLifecycleLog ?? false;
|
||||
document.getElementById('appSettingsShowResponseViewer').checked = settings.showResponseViewer ?? defaults.showResponseViewer ?? false;
|
||||
document.getElementById('appSettingsShowFileViewerButton').checked = settings.showFileViewerButton ?? defaults.showFileViewerButton ?? true;
|
||||
@@ -500,6 +501,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
settings.tabRailDetail ?? defaults.tabRailDetail ?? 'rich';
|
||||
document.getElementById('appSettingsTabRailSort').value =
|
||||
settings.tabRailSort ?? defaults.tabRailSort ?? 'activity';
|
||||
document.getElementById('appSettingsTabGrouping').value =
|
||||
(settings.tabGrouping ?? defaults.tabGrouping ?? 'state') === 'none' ? 'none' : 'state';
|
||||
document.getElementById('appSettingsShowTabDetachButton').checked = settings.showTabDetachButton ?? defaults.showTabDetachButton ?? false;
|
||||
document.getElementById('appSettingsSessionListLayout').value =
|
||||
settings.sessionListLayout ?? defaults.sessionListLayout ?? 'header';
|
||||
@@ -2475,6 +2478,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Header visibility settings
|
||||
showFontControls: document.getElementById('appSettingsShowFontControls').checked,
|
||||
showSystemStats: document.getElementById('appSettingsShowSystemStats').checked,
|
||||
headerStatsStyle: document.getElementById('appSettingsHeaderStatsStyle').value,
|
||||
showLifecycleLog: document.getElementById('appSettingsShowLifecycleLog').checked,
|
||||
showResponseViewer: document.getElementById('appSettingsShowResponseViewer').checked,
|
||||
showFileViewerButton: document.getElementById('appSettingsShowFileViewerButton').checked,
|
||||
@@ -2524,6 +2528,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
tabRailWidth: this.readTabRailWidthSetting?.() ?? 256,
|
||||
tabRailDetail: document.getElementById('appSettingsTabRailDetail').value,
|
||||
tabRailSort: document.getElementById('appSettingsTabRailSort').value,
|
||||
tabGrouping: document.getElementById('appSettingsTabGrouping').value,
|
||||
showTabDetachButton: document.getElementById('appSettingsShowTabDetachButton').checked,
|
||||
sessionListLayout: document.getElementById('appSettingsSessionListLayout').value,
|
||||
sessionSidebarFontSize: this.resolveSessionSidebarFontSize(
|
||||
@@ -3448,6 +3453,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
tabRailWidth: 256,
|
||||
tabRailDetail: 'rich',
|
||||
tabRailSort: 'activity',
|
||||
tabGrouping: 'state',
|
||||
sessionListLayout: 'header',
|
||||
sessionSidebarFontSize: 12,
|
||||
cjkInputEnabled: false,
|
||||
@@ -3549,6 +3555,63 @@ Object.assign(CodemanApp.prototype, {
|
||||
return now === before ? undefined : now;
|
||||
},
|
||||
|
||||
/**
|
||||
* The stored header-stats style, or the default. Anything but the three
|
||||
* known values (an absent key, a value from a newer build) reads as 'tiles',
|
||||
* the default (the tile variant of Discussion #426's option G).
|
||||
*/
|
||||
resolveHeaderStatsStyle(settings) {
|
||||
const value = settings?.headerStatsStyle ?? this.getDefaultSettings().headerStatsStyle;
|
||||
return value === 'classic' || value === 'compact' ? value : 'tiles';
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply a header-stats style: the `data-header-stats` attribute every rule in
|
||||
* the "Header stats styles" block of styles.css keys on, plus the two DOM
|
||||
* moves the clustered styles need.
|
||||
*
|
||||
* The template keeps the classic order, where the connection indicator sits
|
||||
* before the font controls and the plan-usage chip near the end of the header.
|
||||
* Compact and Tiles draw them as ONE cluster (WS · CPU · MEM, then the plan
|
||||
* windows), so the indicator moves into #headerSystemStats as its first child
|
||||
* and the chip moves right after it. Comment anchors left at the template
|
||||
* positions are what 'classic' moves them back to, so switching back restores
|
||||
* the header exactly.
|
||||
*
|
||||
* ⚠️ The indicator only joins the pill while System Stats is shown: the pill
|
||||
* is hidden with `display: none`, and the WS readout must not disappear with
|
||||
* it. Both elements keep their ids, so every writer (setConnectionStatus,
|
||||
* updatePlanUsageChip) finds them wherever they sit.
|
||||
*
|
||||
* @param {{style: 'classic'|'compact'|'tiles', showSystemStats: boolean}} opts
|
||||
*/
|
||||
applyHeaderStatsStyle({ style, showSystemStats }) {
|
||||
document.documentElement.dataset.headerStats = style;
|
||||
const stats = document.getElementById('headerSystemStats');
|
||||
const conn = document.getElementById('connectionIndicator');
|
||||
const plan = document.getElementById('planUsageChip');
|
||||
if (!stats || !conn || !plan) return;
|
||||
if (!this._headerStatsAnchors) {
|
||||
const connAnchor = document.createComment(' connection indicator (classic position) ');
|
||||
const planAnchor = document.createComment(' plan usage chip (classic position) ');
|
||||
conn.before(connAnchor);
|
||||
plan.before(planAnchor);
|
||||
this._headerStatsAnchors = { conn: connAnchor, plan: planAnchor };
|
||||
}
|
||||
const anchors = this._headerStatsAnchors;
|
||||
const clustered = style !== 'classic';
|
||||
if (clustered && showSystemStats) {
|
||||
if (stats.firstElementChild !== conn) stats.prepend(conn);
|
||||
} else if (anchors.conn.nextSibling !== conn) {
|
||||
anchors.conn.after(conn);
|
||||
}
|
||||
if (clustered) {
|
||||
if (stats.nextElementSibling !== plan) stats.after(plan);
|
||||
} else if (anchors.plan.nextSibling !== plan) {
|
||||
anchors.plan.after(plan);
|
||||
}
|
||||
},
|
||||
|
||||
applyHeaderVisibilitySettings() {
|
||||
const settings = this.loadAppSettingsFromStorage();
|
||||
const defaults = this.getDefaultSettings();
|
||||
@@ -3579,6 +3642,12 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (tokenCountEl) {
|
||||
tokenCountEl.style.display = showTokenCount ? '' : 'none';
|
||||
}
|
||||
// After the System Stats visibility above: whether WS joins the stats pill
|
||||
// depends on the pill being shown.
|
||||
this.applyHeaderStatsStyle({
|
||||
style: compactHeader || this.isSoloWindow ? 'classic' : this.resolveHeaderStatsStyle(settings),
|
||||
showSystemStats,
|
||||
});
|
||||
|
||||
// Hide lifecycle log button when setting is disabled
|
||||
// Default OFF: the lifecycle-log document icon is opt-in; the default header
|
||||
@@ -3746,6 +3815,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
const sort = (settings.tabRailSort ?? defaults.tabRailSort ?? 'activity') === 'manual' ? 'manual' : 'activity';
|
||||
root.dataset.tabRailSort = sort;
|
||||
|
||||
// Grouping rides on a fourth attribute, for the same reason: it is applied
|
||||
// as inline `order` plus heading elements the render paths emit, so a flip
|
||||
// has to re-render, and `isTabTriage()` (app.js) reads one attribute per
|
||||
// pass instead of re-parsing localStorage. Anything but an explicit 'none'
|
||||
// is 'state', the default (Discussion #426, option C).
|
||||
const previousGrouping = root.dataset.tabGrouping || 'state';
|
||||
const grouping = (settings.tabGrouping ?? defaults.tabGrouping ?? 'state') === 'none' ? 'none' : 'state';
|
||||
root.dataset.tabGrouping = grouping;
|
||||
|
||||
const tabsEl = document.getElementById('sessionTabs');
|
||||
const rail = document.getElementById('tabRail');
|
||||
const headerHost = document.getElementById('sessionTabsHost');
|
||||
@@ -3769,7 +3847,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// the row template, not toggled by CSS — same reasoning as the sidebar's
|
||||
// detail half in applySessionListLayout(). Taller rows also move every
|
||||
// connector anchored to a tab rect.
|
||||
const changed = orientationChanged || previousDetail !== detail || previousSort !== sort;
|
||||
const changed = orientationChanged || previousDetail !== detail || previousSort !== sort || previousGrouping !== grouping;
|
||||
if (orientationChanged) {
|
||||
this.updateTabOverflowMode?.();
|
||||
if (!settleRailWidth) this.syncTerminalGeometry?.();
|
||||
@@ -4039,10 +4117,10 @@ Object.assign(CodemanApp.prototype, {
|
||||
// NOTE: Feature toggles (subagentTrackingEnabled, imageWatcherEnabled, ralphTrackerEnabled)
|
||||
// are NOT display keys — they control server-side behavior and must sync from server.
|
||||
const displayKeys = new Set([
|
||||
'showFontControls', 'showSystemStats', 'showTokenCount', 'showCost',
|
||||
'showFontControls', 'showSystemStats', 'headerStatsStyle', 'showTokenCount', 'showCost',
|
||||
'showLifecycleLog', 'showResponseViewer', 'showRedrawButton',
|
||||
'showMonitor', 'showProjectInsights', 'showFileBrowser', 'showSubagents',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'tabOrientation', 'tabRailWidth', 'tabRailDetail', 'tabRailSort', 'sessionListLayout', 'sessionSidebarFontSize', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
'subagentActiveTabOnly', 'tabTwoRows', 'tabOrientation', 'tabRailWidth', 'tabRailDetail', 'tabRailSort', 'tabGrouping', 'sessionListLayout', 'sessionSidebarFontSize', 'localEchoEnabled', 'cjkInputEnabled', 'extendedKeyboardBar',
|
||||
'skin', 'showPlanUsageLimits', 'showAttachmentsButton', 'showFileViewerButton', 'webglRendererEnabled',
|
||||
'terminalFontFamily', 'terminalFontWeight', 'terminalFontWeightBold',
|
||||
'language',
|
||||
|
||||
Reference in New Issue
Block a user