From 4f95f781381d51dfb59a37eb24569d2cf214b6bc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 26 Aug 2026 05:45:21 +0000 Subject: [PATCH] fix(cli): abbreviate home-relative cwd on Windows statusline `shortenCwd` tested containment with `cwd.startsWith(home + '/')`. On win32 `os.homedir()` reports `C:\Users\` and `process.cwd()` uses backslashes, so the test was false for every subdirectory of the profile and the statusline printed the full absolute path instead of `~\Videos\clip`. Exact-home still matched, which is why the feature looked partly alive. Decide containment on separator-normalized copies of both paths, and slice the tail out of the original `cwd` so it keeps the separators the platform actually produced. `path.relative` is avoided on purpose: it follows the host platform, so a Windows path would be misread on a POSIX runner. A `..` segment now falls back to the full path rather than emitting a `~/..` form that no longer names the same directory. Comparison stays case-sensitive, like the other path helpers in the tree. The helper is exported so the regression table can inject home and cwd as plain strings and assert the Windows cases without a Windows runner; a second test drives `renderMakaPiStatusLine` through the real `os.homedir()` to cover the wiring. Statusline layout and every other segment are untouched. Closes #3825 Generated-by: Cursor Cloud Agent Co-authored-by: riba2534 --- .../cli/src/__tests__/pi-transcript.test.ts | 43 +++++++++++++++++++ packages/cli/src/pi-transcript.ts | 35 ++++++++++++--- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index b57e43775c..a66a3c873d 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -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'; @@ -38,6 +40,7 @@ import { hydrateToolsWithStoredMessages, makaPiToolPresentationStatus, replaceTranscriptWithStoredMessages, + shortenCwd, submitCompactToTranscript, toggleAllThinkingExpansion, toggleAllToolExpansion, @@ -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'); diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index 35de5bbd80..3741860ee2 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -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\` 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 {