Shelve the per-pane context indicator (keep narrow-toolbar fix)

Reliable per-pane context tracking isn't achievable from transcripts: we
can't distinguish 'claude is live in this pane' from 'a shell sitting in
a directory that recently had a claude session' (claude renders inline,
not alt-screen; no WSL foreground-process access), and the 200k-vs-1M
window isn't recorded so % is unreliable. Removed the context indicator,
its OSC 7 cwd injection (pty.rs), the get_pane_context backend
(usage.rs), src/lib/usage.ts, the orchestration paneContext map, and the
App poll. The narrow-pane toolbar reflow (leaf--narrow/xnarrow tiers,
label shrink, close × pinned) is KEPT — it's verified and independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-28 23:47:06 +01:00
parent 15c2842ce1
commit 00a1e24ecf
10 changed files with 4 additions and 568 deletions

View file

@ -23,10 +23,8 @@ import {
createPaneWindow,
takePendingWindowInit,
pushWindowWorkspaces,
getPaneContext,
type PaneId,
type SpawnSpec,
type SessionContext,
type SshHost,
type McpStatus,
type McpMirror,
@ -241,7 +239,6 @@ export default function App() {
token: null,
});
const [mcpPanelOpen, setMcpPanelOpen] = useState(false);
const [contextSessions, setContextSessions] = useState<SessionContext[]>([]);
const [ready, setReady] = useState(false);
const [notifications, setNotifications] = useState<Toast[]>([]);
const [paletteOpen, setPaletteOpen] = useState(false);
@ -753,66 +750,6 @@ export default function App() {
const openHostManager = useCallback(() => setHostManagerOpen(true), []);
const closeHostManager = useCallback(() => setHostManagerOpen(false), []);
// ---- claude context tracking --------------------------------------------
// Reads each recent session's current context occupancy from ~/.claude
// transcripts (backend), for the per-pane context-fill indicator. The fetch
// guard collapses overlapping ticks.
const contextFetchingRef = useRef(false);
const refreshContext = useCallback(async () => {
if (contextFetchingRef.current) return;
const distros = new Set<string>();
for (const leaf of walkLeaves(treeRef.current)) {
if (leaf.shellKind === "wsl" && leaf.distro) distros.add(leaf.distro);
}
if (distros.size === 0) {
setContextSessions([]);
return;
}
contextFetchingRef.current = true;
try {
const sessions = await getPaneContext(Array.from(distros));
// TEMP diagnostic — remove once the context bar is confirmed working.
console.log(
"[ctx] distros",
[...distros],
"→",
sessions.length,
"sessions:",
sessions.map(
(s) =>
`${s.cwd} | ${s.contextTokens}tok | ${Math.round((Date.now() - s.lastActiveMs) / 1000)}s ago`,
),
);
setContextSessions(sessions);
} catch (e) {
console.warn("getPaneContext failed:", e);
} finally {
contextFetchingRef.current = false;
}
}, []);
// Poll on a light interval, gated on visibility so a hidden/minimized window
// stays quiet.
useEffect(() => {
const tick = () => {
if (document.visibilityState === "visible") void refreshContext();
};
tick();
const id = window.setInterval(tick, 15000);
return () => clearInterval(id);
}, [refreshContext]);
// cwd -> newest session's context, consumed by each LeafPane via orchestration.
const paneContext = useMemo(() => {
const m = new Map<string, SessionContext>();
for (const s of contextSessions) {
if (!s.cwd) continue;
const prev = m.get(s.cwd);
if (!prev || s.lastActiveMs > prev.lastActiveMs) m.set(s.cwd, s);
}
return m;
}, [contextSessions]);
// Outside-click dismissal for the titlebar dropdowns. Mirrors the
// per-pane shell-picker pattern in LeafPane.tsx.
useEffect(() => {
@ -1348,7 +1285,6 @@ export default function App() {
reportLeafIdle,
moveToNewWindow,
getInitialPaneIdFor,
paneContext,
}),
[
activeLeafId,
@ -1374,7 +1310,6 @@ export default function App() {
reportLeafIdle,
moveToNewWindow,
getInitialPaneIdFor,
paneContext,
],
);

View file

@ -82,10 +82,6 @@ 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;
@ -105,7 +101,6 @@ export default function XtermPane({
focusTrigger = 0,
fontSize,
onNavigate,
onCwd,
}: XtermPaneProps) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
@ -126,7 +121,6 @@ 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);
@ -137,7 +131,6 @@ 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]);
// -------------------------------------------------------------------------
@ -211,29 +204,6 @@ export default function XtermPane({
searchAddonRef.current = search;
term.loadAddon(search);
// OSC 7 (cwd reporting): WSL panes emit `\e]7;file://<host><path>\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 */
}
// Defer out of term.write()'s synchronous path: OSC handlers run while
// xterm processes PTY data, which can coincide with React's render
// phase — calling the parent's setState directly there triggers a
// "cannot update while rendering" warning and the update gets dropped.
const reported = path;
queueMicrotask(() => onCwdRef.current?.(reported));
}
return true;
});
// Initial size — fit before asking the PTY for its dimensions.
fit.fit();

View file

@ -39,26 +39,6 @@ export interface SshHost {
export const listDistros = (): Promise<string[]> => invoke("list_distros");
// ---- claude context tracking ----------------------------------------------
/** One claude session's current context-window occupancy, read from its
* transcript. Mirrors Rust SessionContext. `contextTokens` is the prompt
* size of the last assistant turn (input + both cache buckets). */
export interface SessionContext {
sessionId: string;
cwd: string;
distro: string;
lastActiveMs: number;
contextTokens: number;
model: string;
}
/** Scan ~/.claude/projects in the given WSL distros (distinct distros of open
* WSL panes) and return each recent session's current context occupancy.
* WSL/Windows only returns [] otherwise. */
export const getPaneContext = (distros: string[]): Promise<SessionContext[]> =>
invoke("get_pane_context", { distros });
export const spawnPane = (args: {
spec: SpawnSpec;
cols: number;

View file

@ -272,40 +272,9 @@
color: #fcc;
}
/* ---- per-pane context-fill indicator ----------------------------------- */
.pane-ctx {
/* Fallback right-anchor: when .pane-status is hidden (narrow tiers) its
margin-left:auto is gone, so carry it here too. First auto in DOM order
(status ctx actions) consumes the free space; the rest no-op. */
margin-left: auto;
display: flex;
align-items: center;
gap: 5px;
font-size: 10px;
color: #9aa0a6;
}
.pane-ctx-bar {
width: 42px;
height: 6px;
background: #2a2a2a;
border-radius: 3px;
overflow: hidden;
}
.pane-ctx-fill {
display: block;
height: 100%;
border-radius: 3px;
transition: width 0.3s, background 0.3s;
}
.pane-ctx-pct {
font-variant-numeric: tabular-nums;
min-width: 26px;
text-align: right;
}
/* ---- narrow-pane reflow -------------------------------------------------
The close button + context indicator stay visible at every width; lower-
priority toolbar items drop out by tier so a 180px pane keeps its close ×. */
The close button stays visible at every width; lower-priority toolbar items
drop out by tier so a 180px pane keeps its close ×. */
.leaf--narrow .pane-status,
.leaf--narrow .pane-actions .pane-btn:not(.close) {
display: none;
@ -316,10 +285,6 @@
.leaf--xnarrow .bcast-chip {
display: none;
}
/* Keep just the % (drop the bar) at the tightest width. */
.leaf--xnarrow .pane-ctx-bar {
display: none;
}
.xterm-wrap {
flex: 1 1 auto;
min-height: 0;

View file

@ -10,27 +10,12 @@ import {
import { createPortal } from "react-dom";
import { type LeafNode, resolveFontSize, type LeafShellSpec } from "./tree";
import { useOrchestration } from "./orchestration";
import {
contextLabel,
contextColor,
contextFraction,
formatTokens,
} from "../../lib/usage";
import XtermPane from "../../components/XtermPane";
import type { SpawnSpec } from "../../ipc";
import "./LeafPane.css";
const IDLE_THRESHOLD_MS = 5000;
/** Only show the context indicator when the pane's directory has a claude
* session that was active within this window generous, because a live
* session you're actively working in can sit idle for a long while (reading,
* thinking, away). It only suppresses genuinely dormant directories (old
* projects). NOTE: this can't tell "claude is live in this pane" from "a
* shell sitting in a directory that recently had a claude session" that
* needs a foreground-process probe into WSL (deferred). */
const CONTEXT_ACTIVE_MS = 3 * 60 * 60 * 1000;
/** How far past a viewport edge the cursor must travel before a release is
* treated as "drag pane out of window" instead of "drop on empty space
* inside this window". Picked so an accidental release on the OS titlebar
@ -188,36 +173,6 @@ 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<string | null>(null);
const onPaneCwd = useCallback(
(cwd: string) =>
setLiveCwd((cur) => {
if (cur === cwd) return cur;
// TEMP diagnostic — remove once the context bar is confirmed working.
console.log("[ctx] pane reported cwd via OSC7:", cwd);
return cwd;
}),
[],
);
// TEMP diagnostic — logs the per-pane match decision on change.
useEffect(() => {
if (leaf.shellKind !== "wsl") return;
const c = liveCwd ?? leaf.cwd;
const hit = c ? orch.paneContext.get(c) : undefined;
console.log(
"[ctx] MATCH",
leaf.label ?? leaf.distro,
"cwd=",
c,
hit
? `${hit.contextTokens}tok ${Math.round((Date.now() - hit.lastActiveMs) / 1000)}s ago`
: "→ no match",
);
}, [liveCwd, leaf.cwd, leaf.shellKind, leaf.label, leaf.distro, orch.paneContext]);
// ---- broadcast ---------------------------------------------------------
const onTerminalInput = useCallback(
(b64: string) => {
@ -448,18 +403,6 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
};
})();
const matchCwd = liveCwd ?? leaf.cwd;
const matchedCtx =
leaf.shellKind === "wsl" && matchCwd
? orch.paneContext.get(matchCwd)
: undefined;
// Suppress stale matches: only surface the bar while the session is actively
// being written (a live claude keeps its transcript fresh).
const ctx =
matchedCtx && Date.now() - matchedCtx.lastActiveMs < CONTEXT_ACTIVE_MS
? matchedCtx
: undefined;
return (
<div
ref={rootRef}
@ -612,24 +555,6 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
<span className={`pane-status ${statusOk ? "ok" : "err"}`}>{status}</span>
)}
{ctx && (
<span
className="pane-ctx"
title={`Context: ${contextLabel(ctx)} (${ctx.model})`}
>
<span className="pane-ctx-bar">
<span
className="pane-ctx-fill"
style={{
width: `${Math.round(contextFraction(ctx) * 100)}%`,
background: contextColor(contextFraction(ctx)),
}}
/>
</span>
<span className="pane-ctx-pct">{formatTokens(ctx.contextTokens)}</span>
</span>
)}
<span className="pane-actions">
<button
className="pane-btn"
@ -677,7 +602,6 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
onDataReceived={onDataReceived}
onFocus={onXtermFocus}
onNavigate={onPaneNavigate}
onCwd={onPaneCwd}
focusTrigger={focusTrigger}
fontSize={resolveFontSize(leaf.fontSizeOffset)}
/>

View file

@ -1,6 +1,6 @@
import { createContext, useContext, type ReactNode } from "react";
import type { Orientation, NodeId, LeafShellSpec, Direction } from "./tree";
import type { PaneId, SshHost, SessionContext } from "../../ipc";
import type { PaneId, SshHost } from "../../ipc";
/**
* Orchestration context every piece of shared state and every operation
@ -77,10 +77,6 @@ export interface Orchestration {
* the spawn). One-shot App clears the entry once the pane has
* registered. */
getInitialPaneIdFor: (leafId: NodeId) => PaneId | undefined;
/** cwd -> the newest claude session's current context occupancy, for the
* per-pane context-fill indicator. A leaf looks itself up by `leaf.cwd`;
* absent for non-claude / unmatched panes. Polled by App. */
paneContext: Map<string, SessionContext>;
}
/** Discriminated intent emitted by XtermPane's key handler. App resolves

View file

@ -1,47 +0,0 @@
// Helpers for the per-pane context-fill indicator. Context occupancy (token
// count) comes from the backend (src-tauri/src/usage.rs, get_pane_context); this
// turns it into a window %, a colour, and a human label.
import type { SessionContext } from "../ipc";
const WINDOW_LARGE = 1_000_000;
/**
* Assumed context window. The transcript does NOT record whether a session
* runs the 200k or 1M window (the model id is bare, e.g. `claude-opus-4-7`
* the `[1m]` that claude's /context shows is display-only), so the % can't be
* computed reliably. We assume 1M (the common case here) for the fill bar, and
* the indicator LABEL shows the absolute token count, which is accurate
* regardless of the real window that's the figure to trust.
*/
export function contextWindow(_contextTokens: number): number {
return WINDOW_LARGE;
}
/** Fraction (0..1) of the inferred window currently occupied. */
export function contextFraction(s: SessionContext): number {
const w = contextWindow(s.contextTokens);
return w > 0 ? Math.min(1, s.contextTokens / w) : 0;
}
export function contextPercent(s: SessionContext): number {
return Math.round(contextFraction(s) * 100);
}
/** Green → amber → red ramp as the window fills. */
export function contextColor(fraction: number): string {
if (fraction >= 0.85) return "#d65a5a";
if (fraction >= 0.6) return "#d6a23a";
return "#5aa84a";
}
export function formatTokens(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
if (n >= 1_000) return Math.round(n / 1_000) + "k";
return String(n);
}
/** e.g. "274k tokens · ~27% of 1M (last turn)" for a tooltip. */
export function contextLabel(s: SessionContext): string {
return `${formatTokens(s.contextTokens)} tokens · ~${contextPercent(s)}% of 1M (last turn)`;
}