Compare commits
2 commits
24ab7f067f
...
02d97d1520
| Author | SHA1 | Date | |
|---|---|---|---|
| 02d97d1520 | |||
| d776f962da |
4 changed files with 57 additions and 1 deletions
|
|
@ -59,6 +59,7 @@ Four-agent research pass (terminal-landscape, AI-orchestration, xterm/Tauri ecos
|
|||
- [x] ~~**Per-session cost / token tracking.**~~ Done (code) 2026-05-28 — **WSL-only v1, pending Windows runtime verify.** Backend `src-tauri/src/usage.rs` (`get_claude_usage(distros)` command): probes `$HOME` per distro via `wsl.exe`, reads `~/.claude/projects/*/*.jsonl` over the `\\wsl.localhost\<distro>` UNC share, tallies `message.usage` **per model per assistant line** (sessions can switch models). Cached by `(path,size,mtime)`; recency-capped 30d/50 sessions. Frontend: `src/lib/usage.ts` holds the editable pricing table (per-MTok, matched by opus/sonnet/haiku substring) + cost/format helpers; `UsagePanel.tsx` (MCP-panel modal pattern) lists sessions, highlights those whose transcript `cwd` matches an open pane (`[pane: label]`); titlebar 💰 total chip; App polls 20s (visible) / 5s (panel open); **Ctrl+Shift+U** opens it. **Design choice:** session-list attribution (not 1:1 pane binding) — avoids the unsolvable "2 claudes in one cwd" ambiguity. **Caveats:** cost is an estimate (cache-creation priced at 5m rate; rates hardcoded, may drift); panes with no explicit cwd (`~`) won't highlight; PowerShell/SSH show nothing. Plan: `~/.claude/plans/greedy-cooking-flask.md`.
|
||||
- **PIVOTED 2026-05-28 → per-pane context-fill indicator (replaces the panel).** User decided lifetime token totals + $ aren't worth it on a subscription; what's actionable is *current context-window occupancy* per pane (spot the one needing `/compact`). Removed `UsagePanel`, the 💰 titlebar chip, and `Ctrl+Shift+U`. Repurposed `usage.rs`: `get_pane_context` returns each recent session's **current** occupancy = the LAST assistant turn's `input + cache_read + cache_creation` tokens (verified ~274k on this 1M session). `src/lib/usage.ts` now does window inference (200k vs 1M by whether occupancy already exceeds 200k — model id doesn't encode the variant), %, color ramp. App polls 15s (visibility-gated) → `cwd→SessionContext` map via orchestration; `LeafPane` renders a slim fill bar + % in the header, matched by `leaf.cwd`. **Also fixed narrow-pane toolbar** (user report: close × clipped when slim): a `ResizeObserver` in LeafPane sets `leaf--narrow`/`leaf--xnarrow` tiers; label shrinks first, split/status/secondary chips drop by tier, close × + context indicator stay pinned-right + visible down to the 180px min. Plan: `~/.claude/plans/greedy-cooking-flask.md` (rewritten for the pivot). **Pending Windows runtime verify.** Window-size 200k/1M is inferred (approx near boundary); `~`-spawned / cd'd panes may not match their session.
|
||||
- **Windows test 2026-05-28:** narrow-pane toolbar reflow (close × stays visible when shrunk, leaf--narrow/xnarrow tiers) **VERIFIED working.** BUT the context bar **does not show** — root-caused: it keys on `leaf.cwd`, which is ~always `undefined` (`newLeaf` sets no cwd; the shell picker never supplies one; only split-inheritance propagates it). So the cwd↔transcript match never hits for normal panes. Needs the pane's *live* cwd to work — leading options: capture via OSC 7 (default WSL bash under tiletopia doesn't emit it → would need injecting a PROMPT_COMMAND at spawn, shell-specific), or an "active pane shows its distro's currently-active session" heuristic gated on recent mtime. Decision pending with user.
|
||||
- **Fix implemented 2026-05-28 (OSC 7 live cwd, user chose this) — PENDING re-test.** `pty.rs` Wsl arm now sets `PROMPT_COMMAND` (forwarded via `WSLENV=…:PROMPT_COMMAND/u`) to `printf '\033]7;file://%s%s\033\\' "$HOSTNAME" "$PWD"` so the shell emits OSC 7 each prompt; default Ubuntu bash inherits an env-provided PROMPT_COMMAND (a hard-assigning rc or non-bash shell won't report → bar hidden, no breakage). `XtermPane` registers `term.parser.registerOscHandler(7, …)`, decodes the path, fires new `onCwd` prop. `LeafPane` tracks `liveCwd` and matches on `(liveCwd ?? leaf.cwd)`. OSC 7 fires at the bash prompt right before `claude` launches → `liveCwd` = claude's launch cwd; also follows `cd`. **If still blank after re-test:** check the shell actually emits OSC 7 (it won't if the user's rc hard-sets PROMPT_COMMAND, or default shell isn't bash) and that backend `get_pane_context` returns sessions (UNC/$HOME probe).
|
||||
- **Superseded — original lifetime-token panel refinements (kept for history):** (1) **Scope** — panel + titlebar chip now default to sessions matching open panes ("this workspace"), with an "open panes / all recent" toggle. The first cut summed *every* recent session on the distro (all projects, `/mnt` + home), which read as inflated. **Investigated the "double counting mounted folders + projects" report: NOT a real double count** — every transcript file is read exactly once, and no two project dirs share a cwd because claude resolves symlinks/mounts to the real path before mangling the project-dir name (e.g. the `~/claude/projects/tiletopia → /mnt/d/dev/tiletopia` symlink yields only `-mnt-d-dev-tiletopia`). The inflation was purely the global scope. (2) **Metric framing** — user is on a Pro/Max subscription where $ is meaningless (and `/usage` rate-limit quota can't be derived from transcripts); **tokens are now the headline**, the API-cost estimate is a labeled secondary `~$` kept visible so the user can validate it against real API billing at work. **Open question:** accuracy of the $ estimate vs actual API billing — user will check at work.
|
||||
- [ ] **Smart link providers.** `terminal.registerLinkProvider()` to make file paths (`src/foo.ts:12:3`), `localhost:PORT`, and error locations clickable — more flexible than the regex-only web-links addon already loaded. Open file in editor / browser. Difficulty: medium.
|
||||
- [x] ~~**Find in scrollback.**~~ Done + **verified on Windows 2026-05-28** — `@xterm/addon-search` + new `src/components/SearchBar.tsx`/`.css` overlay, Ctrl+Shift+F open / Enter / Shift+Enter / Esc, regex + case toggles, decoration highlight.
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const termRef = useRef<Terminal | null>(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://<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 */
|
||||
}
|
||||
onCwdRef.current?.(path);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Initial size — fit before asking the PTY for its dimensions.
|
||||
fit.fit();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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 (
|
||||
<div
|
||||
|
|
@ -629,6 +641,7 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) {
|
|||
onDataReceived={onDataReceived}
|
||||
onFocus={onXtermFocus}
|
||||
onNavigate={onPaneNavigate}
|
||||
onCwd={onPaneCwd}
|
||||
focusTrigger={focusTrigger}
|
||||
fontSize={resolveFontSize(leaf.fontSizeOffset)}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue