diff --git a/src/collectors/session-finder.ts b/src/collectors/session-finder.ts index 3ebd6a8..d8e927a 100644 --- a/src/collectors/session-finder.ts +++ b/src/collectors/session-finder.ts @@ -344,7 +344,7 @@ function readSnapshotPane(filePath: string): string | null { try { const content = fs.readFileSync(filePath, 'utf8'); const match = content.match( - /(?:^|\n)(?:export\s+)?TMUX_PANE=(?:'([^']*)'|"([^"]*)"|([^\n]+))/ + /(?:^|\n)(?:(?:export|declare\s+-x|typeset\s+-x)\s+)?TMUX_PANE=(?:'([^']*)'|"([^"]*)"|([^\n]+))/ ); const pane = match?.[1] ?? match?.[2] ?? match?.[3]; return pane ? pane.trim() : null; diff --git a/src/render/header.ts b/src/render/header.ts index e317845..3678db1 100644 --- a/src/render/header.ts +++ b/src/render/header.ts @@ -3,10 +3,10 @@ * Phase 3: Redesigned to match claude-hud layout * * Layout: - * Row 1: [Model] █████░░░░░ 45% | project-name git:(branch *) | ⏱️ 10m + * Row 1: project-name git:(branch *) | ⏱️ 10m * Row 2: 2 AGENTS.md | 3 MCPs | Approval: default * Row 3: Tokens: 12.5K | Ctx: ████░░░░ 45% (50K/128K) - * Row 4: Dir: ~/project | Session: abc12345 + * Row 4: Session: abc12345 * Row 5 (optional): ◐ Edit: file.ts | ✓ Read ×3 */ @@ -68,19 +68,17 @@ function renderCompactLayout(data: HudData, layout: LayoutConfig, width: number) /** * Render the expanded layout (multiple lines) - * Row 1: [Model] █████░░░░░ 45% | project-name git:(branch *) | ⏱️ 10m + * Row 1: project-name git:(branch *) | ⏱️ 10m * Row 2: 2 AGENTS.md | 3 MCPs | Approval: default * Row 3: Tokens: 12.5K | Ctx: ████░░░░ 45% (50K/128K) - * Row 4: Dir: ~/project | Session: abc12345 + * Row 4: Session: abc12345 * Row 5+: Activity lines (tools, todos) */ function renderExpandedLayout(data: HudData, layout: LayoutConfig, width: number): string[] { const lines: string[] = []; - // Row 1: Identity | Project | Duration + // Row 1: Project | Duration const row1Parts: string[] = []; - const identityLine = renderIdentityLine(data, layout, { maxWidth: width }); - row1Parts.push(identityLine); row1Parts.push(renderProjectLine(data)); const usageLine = renderUsageLine(data, layout); @@ -90,13 +88,12 @@ function renderExpandedLayout(data: HudData, layout: LayoutConfig, width: number const separator = layout.showSeparators ? theme.separator(' │ ') : ' '; let row1 = row1Parts.join(separator); - if (usageLine && visualLength(row1) > width) { - row1 = row1Parts.slice(0, 2).join(separator); - } if (visualLength(row1) > width) { - const availableForProject = Math.max(0, width - visualLength(identityLine) - visualLength(separator)); + const availableForProject = usageLine + ? Math.max(0, width - visualLength(usageLine) - visualLength(separator)) + : width; const projectLine = renderProjectLine(data, { includeFileStats: false, maxWidth: availableForProject }); - row1 = [identityLine, projectLine].join(separator); + row1 = usageLine ? [projectLine, usageLine].join(separator) : projectLine; } lines.push(row1); diff --git a/src/render/lines/activity-line.ts b/src/render/lines/activity-line.ts index 7010d0b..b1c59df 100644 --- a/src/render/lines/activity-line.ts +++ b/src/render/lines/activity-line.ts @@ -256,22 +256,7 @@ export function renderTokenLine(data: HudData): string | null { export function renderSessionDetailLine(data: HudData): string | null { const parts: string[] = []; - // Always show session info if we have a session const session = data.session; - - // Show working directory - const cwd = session?.cwd || data.project.cwd; - if (cwd) { - const home = process.env.HOME || ''; - let displayPath = cwd; - if (home && cwd.startsWith(home)) { - displayPath = '~' + cwd.slice(home.length); - } - if (displayPath.length > 50) { - displayPath = '…' + displayPath.slice(-49); - } - parts.push(colors.dim('Dir: ') + theme.value(displayPath)); - } // Show session ID if available if (session?.id) { diff --git a/tests/unit/test-expanded-layout-no-duplicate-context.mjs b/tests/unit/test-expanded-layout-no-duplicate-context.mjs new file mode 100644 index 0000000..ce0d984 --- /dev/null +++ b/tests/unit/test-expanded-layout-no-duplicate-context.mjs @@ -0,0 +1,86 @@ +import assert from 'node:assert/strict'; + +import { renderHud } from '../../dist/render/header.js'; +import { stripAnsi } from '../../dist/render/colors.js'; + +const layout = { + mode: 'expanded', + showSeparators: false, + showDuration: true, + showContextBreakdown: true, + barWidth: 8, +}; + +const data = { + config: { + model: 'gpt-5.5', + model_reasoning_effort: 'xhigh', + model_provider: 'openai', + approval_policy: 'auto', + sandbox_mode: 'danger-full-access', + }, + git: { + branch: 'main', + isDirty: false, + isGitRepo: true, + ahead: 0, + behind: 0, + modified: 0, + added: 0, + deleted: 0, + untracked: 0, + }, + project: { + cwd: '/Users/rex/soft/everything-claude-code', + projectName: 'everything-claude-code', + agentsMdCount: 2, + hasCodexDir: true, + instructionsMdCount: 0, + rulesCount: 0, + mcpCount: 0, + configsCount: 2, + extensionsCount: 0, + workMode: 'development', + }, + sessionStart: new Date('2026-05-09T00:00:00Z'), + session: { + id: '019e0b8c-40e8-7523-b527-597946103715', + rolloutPath: '/tmp/rollout.jsonl', + startTime: new Date('2026-05-09T00:00:00Z'), + cwd: '/Users/rex/soft/everything-claude-code', + cliVersion: '0.130.0', + model: 'gpt-5.5', + reasoningEffort: 'xhigh', + modelProvider: 'OpenAI', + }, + tokenUsage: { + last_token_usage: { + input_tokens: 225000, + cached_input_tokens: 220000, + output_tokens: 46, + total_tokens: 225046, + }, + model_context_window: 258400, + }, + contextUsage: { + used: 237046, + total: 258400, + percent: 92, + inputTokens: 5000, + outputTokens: 46, + cachedTokens: 220000, + compactCount: 1, + }, + displayMode: 'single', +}; + +const lines = renderHud(data, { width: 120, showDetails: true, layout }); +const plain = lines.map(stripAnsi).join('\n'); + +assert.match(plain, /everything-claude-code git:\(main\)/, 'expanded header should keep project and git'); +assert.doesNotMatch(plain, /\[gpt-5\.5 xhigh\]/, 'expanded layout should not repeat model identity'); +assert.doesNotMatch(plain, /Dir: /, 'expanded layout should not repeat the working directory'); +assert.equal((plain.match(/Ctx:/g) ?? []).length, 1, 'context should be shown only on the token line'); +assert.equal((plain.match(/92%/g) ?? []).length, 1, 'context percent should not be duplicated'); + +console.log('test-expanded-layout-no-duplicate-context: PASS'); diff --git a/tests/unit/test-session-finder-pane-binding.mjs b/tests/unit/test-session-finder-pane-binding.mjs index 0f43259..22ab6cd 100644 --- a/tests/unit/test-session-finder-pane-binding.mjs +++ b/tests/unit/test-session-finder-pane-binding.mjs @@ -60,7 +60,7 @@ function writeRollout(home, { sessionId, cwd, fileOffsetMinutes = 0, modifiedAt, return filePath; } -function writeSnapshot(home, threadId, paneId, nonce) { +function writeSnapshot(home, threadId, paneId, nonce, assignment = `export TMUX_PANE='${paneId}'`) { const dir = path.join(home, 'shell_snapshots'); fs.mkdirSync(dir, { recursive: true }); const filePath = path.join(dir, `${threadId}.${nonce}.sh`); @@ -68,7 +68,7 @@ function writeSnapshot(home, threadId, paneId, nonce) { filePath, [ '# Snapshot file', - `export TMUX_PANE='${paneId}'`, + assignment, "export PATH='/usr/bin'", '', ].join('\n'), @@ -77,6 +77,10 @@ function writeSnapshot(home, threadId, paneId, nonce) { return filePath; } +function assertSamePath(actual, expected, message) { + assert.equal(fs.realpathSync(actual), fs.realpathSync(expected), message); +} + const originalCodexHome = process.env.CODEX_HOME; const originalMainPane = process.env.CODEX_HUD_MAIN_PANE; const originalSessionsPath = process.env.CODEX_SESSIONS_PATH; @@ -130,7 +134,7 @@ try { const finder = new SessionFinder(cwd); const resolved = finder.check(); assert.ok(resolved, 'expected a pane-bound session to resolve'); - assert.equal( + assertSamePath( resolved.path, boundRollout, 'pane-bound shell snapshot should override the newest unrelated rollout' @@ -165,13 +169,34 @@ try { const finderOne = new SessionFinder(cwd); const resultOne = finderOne.check(); assert.ok(resultOne, 'expected pane one to resolve'); - assert.equal(resultOne.path, paneOneRollout, 'pane one should stay on its own thread'); + assertSamePath(resultOne.path, paneOneRollout, 'pane one should stay on its own thread'); process.env.CODEX_HUD_MAIN_PANE = '%72'; const finderTwo = new SessionFinder(cwd); const resultTwo = finderTwo.check(); assert.ok(resultTwo, 'expected pane two to resolve'); - assert.equal(resultTwo.path, paneTwoRollout, 'pane two should stay on its own thread'); + assertSamePath(resultTwo.path, paneTwoRollout, 'pane two should stay on its own thread'); + } + + { + const home = makeTempCodexHome(); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-hud-cwd-')); + process.env.CODEX_HOME = home; + delete process.env.CODEX_SESSIONS_PATH; + process.env.CODEX_HUD_MAIN_PANE = '%70'; + + const thread = '019d7291-a135-7fe1-b46f-8f3eca4fa451'; + const rollout = writeRollout(home, { + sessionId: thread, + cwd, + modifiedAt: new Date(), + }); + writeSnapshot(home, thread, '%70', 1775743876858615370n, 'declare -x TMUX_PANE="%70"'); + + const finder = new SessionFinder(cwd); + const result = finder.check(); + assert.ok(result, 'expected declare -x TMUX_PANE snapshots to resolve'); + assertSamePath(result.path, rollout, 'declare -x snapshot should bind the HUD to its rollout'); } } finally { if (originalCodexHome === undefined) {