diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index d4d78e5..13cb343 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -354,6 +354,23 @@ fn build_command(spec: &SpawnSpec) -> Result<(CommandBuilder, &'static str)> { let resolved_cwd = cwd.as_deref().unwrap_or("~"); c.arg("--cd"); c.arg(resolved_cwd); + // Make the shell report its working directory via OSC 7 on every + // prompt, so the frontend can map this pane to the claude session + // running in it (the context-fill indicator; see usage.rs + + // LeafPane). We set PROMPT_COMMAND in the environment and forward it + // through WSLENV — default Ubuntu bash inherits an env-provided + // PROMPT_COMMAND. A user shell that hard-assigns PROMPT_COMMAND (or + // a non-bash login shell) simply won't report, and the indicator + // stays hidden for that pane — no breakage either way. + c.env( + "PROMPT_COMMAND", + r#"printf '\033]7;file://%s%s\033\\' "$HOSTNAME" "$PWD""#, + ); + let wslenv = match std::env::var("WSLENV") { + Ok(v) if !v.is_empty() => format!("{v}:PROMPT_COMMAND/u"), + _ => "PROMPT_COMMAND/u".to_string(), + }; + c.env("WSLENV", wslenv); Ok((c, "failed to spawn wsl.exe; is WSL installed?")) } SpawnSpec::Powershell => { diff --git a/src/components/XtermPane.tsx b/src/components/XtermPane.tsx index a993c11..3ebb2a9 100644 --- a/src/components/XtermPane.tsx +++ b/src/components/XtermPane.tsx @@ -82,6 +82,10 @@ interface XtermPaneProps { * Defined as an optional callback so single-pane windows don't require * wiring it up. */ onNavigate?: (intent: NavigateIntent) => void; + /** Fired with the shell's reported working directory (from an OSC 7 escape, + * which WSL panes emit via an injected PROMPT_COMMAND — see pty.rs). Used to + * map the pane to the claude session running in it. */ + onCwd?: (cwd: string) => void; } const DEFAULT_XTERM_FONT_SIZE = 13; @@ -101,6 +105,7 @@ export default function XtermPane({ focusTrigger = 0, fontSize, onNavigate, + onCwd, }: XtermPaneProps) { const containerRef = useRef(null); const termRef = useRef(null); @@ -121,6 +126,7 @@ export default function XtermPane({ const onDataReceivedRef = useRef(onDataReceived); const onFocusRef = useRef(onFocus); const onNavigateRef = useRef(onNavigate); + const onCwdRef = useRef(onCwd); // Stable ref for setSearchOpen so it can be called from inside the // attachCustomKeyEventHandler closure without the closure going stale. const setSearchOpenRef = useRef<(v: boolean) => void>(setSearchOpen); @@ -131,6 +137,7 @@ export default function XtermPane({ useEffect(() => { onDataReceivedRef.current = onDataReceived; }, [onDataReceived]); useEffect(() => { onFocusRef.current = onFocus; }, [onFocus]); useEffect(() => { onNavigateRef.current = onNavigate; }, [onNavigate]); + useEffect(() => { onCwdRef.current = onCwd; }, [onCwd]); useEffect(() => { setSearchOpenRef.current = setSearchOpen; }, [setSearchOpen]); // ------------------------------------------------------------------------- @@ -204,6 +211,24 @@ export default function XtermPane({ searchAddonRef.current = search; term.loadAddon(search); + // OSC 7 (cwd reporting): WSL panes emit `\e]7;file://\e\\` on + // every prompt (via the PROMPT_COMMAND we inject at spawn). Capture the + // path and report it up so the pane can be matched to its claude session. + // Registered before data flows so the first prompt's cwd is caught. + term.parser.registerOscHandler(7, (data) => { + const m = /^file:\/\/[^/]*(\/.*)$/.exec(data); + if (m) { + let path = m[1]; + try { + path = decodeURIComponent(path); + } catch { + /* leave raw if it isn't valid percent-encoding */ + } + onCwdRef.current?.(path); + } + return true; + }); + // Initial size — fit before asking the PTY for its dimensions. fit.fit(); diff --git a/src/lib/layout/LeafPane.tsx b/src/lib/layout/LeafPane.tsx index a158396..7b7a764 100644 --- a/src/lib/layout/LeafPane.tsx +++ b/src/lib/layout/LeafPane.tsx @@ -179,6 +179,15 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { return () => ro.disconnect(); }, []); + // Live cwd reported by the shell via OSC 7 (WSL panes). Used to match this + // pane to the claude session running in it — more reliable than leaf.cwd, + // which is the (often unset) spawn cwd and doesn't follow `cd`. + const [liveCwd, setLiveCwd] = useState(null); + const onPaneCwd = useCallback( + (cwd: string) => setLiveCwd((cur) => (cur === cwd ? cur : cwd)), + [], + ); + // ---- broadcast --------------------------------------------------------- const onTerminalInput = useCallback( (b64: string) => { @@ -409,8 +418,11 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { }; })(); + const matchCwd = liveCwd ?? leaf.cwd; const ctx = - leaf.shellKind === "wsl" && leaf.cwd ? orch.paneContext.get(leaf.cwd) : undefined; + leaf.shellKind === "wsl" && matchCwd + ? orch.paneContext.get(matchCwd) + : undefined; return (