Skip to content
Closed
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
43 changes: 43 additions & 0 deletions packages/cli/src/__tests__/pi-transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import assert from 'node:assert/strict';
import { homedir } from 'node:os';
import { join, sep } from 'node:path';
import { describe, test } from 'node:test';
import { visibleWidth } from '@earendil-works/pi-tui';
import type { PipeShellOutput, PtyShellOutput } from '@maka/core/shell-run';
Expand All @@ -38,6 +40,7 @@ import {
hydrateToolsWithStoredMessages,
makaPiToolPresentationStatus,
replaceTranscriptWithStoredMessages,
shortenCwd,
submitCompactToTranscript,
toggleAllThinkingExpansion,
toggleAllToolExpansion,
Expand Down Expand Up @@ -244,6 +247,46 @@ describe('Maka Pi TUI transcript', () => {
);
});

test('abbreviates a home-relative cwd on every platform (#3825)', () => {
// Windows reports the profile with backslashes, so the abbreviation has to
// be decided on separator-normalized paths rather than on a `home + '/'`
// prefix, which matched nothing there. Home and cwd are injected as plain
// strings so every expectation holds on any runner.
const cases: [cwd: string, home: string, expected: string][] = [
['/Users/alice/workspace/project', '/Users/alice', '~/workspace/project'],
['/Users/alice', '/Users/alice', '~'],
['C:\\Users\\alice\\Videos\\clip', 'C:\\Users\\alice', '~\\Videos\\clip'],
['C:\\Users\\alice', 'C:\\Users\\alice', '~'],
['C:\\Users\\alice\\', 'C:\\Users\\alice', '~'],
// Windows accepts either separator inside one path.
['C:/Users/alice/Videos', 'C:\\Users\\alice', '~/Videos'],
['C:\\Users\\alice\\Videos', 'C:/Users/alice', '~\\Videos'],
// Outside home, or only sharing a name prefix with it: unchanged.
['/opt/maka', '/Users/alice', '/opt/maka'],
['/Users/alicebob/x', '/Users/alice', '/Users/alicebob/x'],
['D:\\Projects\\maka', 'C:\\Users\\alice', 'D:\\Projects\\maka'],
['C:\\Users\\alicebob\\x', 'C:\\Users\\alice', 'C:\\Users\\alicebob\\x'],
// A `..` segment can walk back out of home, so the full path is kept.
['/Users/alice/../bob', '/Users/alice', '/Users/alice/../bob'],
['C:\\Users\\alice\\..\\bob', 'C:\\Users\\alice', 'C:\\Users\\alice\\..\\bob'],
// No usable home: nothing to abbreviate against.
['/Users/alice/project', '', '/Users/alice/project'],
];
for (const [cwd, home, expected] of cases) {
assert.equal(shortenCwd(cwd, home), expected, `${cwd} under ${home}`);
}
});

test('status line renders the cwd through the home abbreviation (#3825)', () => {
// The statusline resolves home itself, so drive the wiring through the
// real one and build the expectation with the platform's own separator.
const line = stripAnsi(
renderMakaPiStatusLine({ ...meta(), cwd: join(homedir(), 'workspace', 'project') }, 200),
);
assert.match(line, new RegExp(`~\\${sep}workspace\\${sep}project$`));
assert.equal(line.includes(homedir()), false);
});

test('keeps assistant text after a tool call visible after the tool block', () => {
const state = createMakaPiTranscriptState();
appendUserPrompt(state, 'inspect the package');
Expand Down
35 changes: 28 additions & 7 deletions packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1513,15 +1513,36 @@ function firstLinePreview(text: string): string {
}

/**
* Shorten an absolute path to a `~`-relative form for the statusline.
* `/Users/alice/workspace/project` → `~/workspace/project`.
* Falls back to the original path if it is not under the home directory.
* Shorten an absolute path to a `~`-relative form for the statusline:
* `/Users/alice/workspace/project` → `~/workspace/project`,
* `C:\Users\alice\Videos\clip` → `~\Videos\clip`.
*
* Containment is decided on separator-normalized copies. Windows reports the
* profile as `C:\Users\<name>` and accepts either separator inside one path, so
* a literal `home + '/'` prefix test matched nothing there and the statusline
* showed every profile subdirectory in full (#3825). `path.relative` is not
* used for the same reason in reverse: it follows the host platform, so a
* Windows path would be misread on a POSIX runner. The tail is sliced out of
* the original `cwd` rather than the normalized copy, keeping whichever
* separators the platform actually produced.
*
* Falls back to the original path when it is not under home. A `..` segment
* falls back too — it can walk back out, and only the unabbreviated path is
* guaranteed to still name the same directory. Comparison stays
* case-sensitive, like the other path helpers in the tree.
*/
function shortenCwd(cwd: string, homeDir?: string): string {
export function shortenCwd(cwd: string, homeDir?: string): string {
const home = homeDir ?? homedir();
if (home && cwd.startsWith(home + '/')) return `~${cwd.slice(home.length)}`;
if (home && cwd === home) return '~';
return cwd;
// A trailing separator on home would push the slice offset past the prefix.
const normalizedHome = home.replace(/[\\/]+$/, '').replaceAll('\\', '/');
if (!normalizedHome) return cwd;
const normalizedCwd = cwd.replaceAll('\\', '/');
if (normalizedCwd === normalizedHome) return '~';
if (!normalizedCwd.startsWith(`${normalizedHome}/`)) return cwd;
const tail = normalizedCwd.slice(normalizedHome.length + 1);
if (!tail) return '~';
if (tail.split('/').includes('..')) return cwd;
return `~${cwd.slice(normalizedHome.length)}`;
}

function formatCost(costUsd: number): string {
Expand Down