Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/collectors/session-finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 9 additions & 12 deletions src/render/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
15 changes: 0 additions & 15 deletions src/render/lines/activity-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/test-expanded-layout-no-duplicate-context.mjs
Original file line number Diff line number Diff line change
@@ -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');
35 changes: 30 additions & 5 deletions tests/unit/test-session-finder-pane-binding.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ 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`);
fs.writeFileSync(
filePath,
[
'# Snapshot file',
`export TMUX_PANE='${paneId}'`,
assignment,
"export PATH='/usr/bin'",
'',
].join('\n'),
Expand All @@ -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;
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down