For a subscription user, lifetime token totals + a $ estimate aren't
actionable; how full each session's context window is right now is. So:
- Removed the UsagePanel, the titlebar 💰 chip, and Ctrl+Shift+U.
- Repurposed the transcript reader (src-tauri/src/usage.rs): get_pane_context
returns each recent session's CURRENT context occupancy = the last
assistant turn's input + cache_read + cache_creation tokens (the prompt
size), instead of lifetime sums. Same UNC/$HOME/cache/recency machinery.
- src/lib/usage.ts now holds context helpers (window inference 200k vs 1M by
whether occupancy already exceeds 200k, % , green→amber→red ramp, label).
- App polls get_pane_context (15s, visibility-gated) into a cwd→context map
exposed via orchestration; each LeafPane looks itself up by leaf.cwd and
renders a slim fill bar + % in its header (hidden for non-claude/unmatched
panes).
Also fixes the narrow-pane toolbar: a ResizeObserver sets leaf--narrow /
leaf--xnarrow width tiers; the label shrinks first, split buttons / status /
secondary chips drop out by tier, and the close × + context indicator stay
pinned right and visible down to the 180px min width.
tsc clean (apart from the not-yet-installed xterm addons). Rust builds on
the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
import { createContext, useContext, type ReactNode } from "react";
|
|
import type { Orientation, NodeId, LeafShellSpec, Direction } from "./tree";
|
|
import type { PaneId, SshHost, SessionContext } from "../../ipc";
|
|
|
|
/**
|
|
* Orchestration context — every piece of shared state and every operation
|
|
* that a Pane / SplitNode / LeafPane might call. Lives in React context so
|
|
* descendants can `useOrchestration()` without prop drilling.
|
|
*
|
|
* activeLeafId comes in as a plain value (re-derived by App's useState).
|
|
* React's context is reactive: when the App-level Provider updates the
|
|
* value, ALL consumers re-render. No Svelte-style props-don't-propagate
|
|
* trap here.
|
|
*/
|
|
export interface Orchestration {
|
|
// Read-only state
|
|
activeLeafId: NodeId | null;
|
|
/** WSL distros enumerated from `wsl.exe -l -q`. PowerShell is a separate
|
|
* shell kind, not in this list. */
|
|
distros: string[];
|
|
/** Saved SSH hosts loaded from `hosts.json`. Reactive — changes when the
|
|
* user edits hosts via {@link openHostManager}. */
|
|
hosts: SshHost[];
|
|
|
|
// Tree mutations
|
|
split: (leafId: NodeId, orientation: Orientation) => void;
|
|
close: (leafId: NodeId) => void;
|
|
/** Change the shell on a leaf (WSL distro / PowerShell / SSH host).
|
|
* Always forces a respawn — the helper in tree.ts swaps the leaf id so
|
|
* the renderer remounts XtermPane. */
|
|
setShell: (leafId: NodeId, spec: LeafShellSpec) => void;
|
|
setLabel: (leafId: NodeId, label: string | undefined) => void;
|
|
toggleBroadcast: (leafId: NodeId) => void;
|
|
/** Flip the per-pane mcpAllow flag. Default-deny; chip in the pane
|
|
* toolbar drives this. */
|
|
toggleMcpAllow: (leafId: NodeId) => void;
|
|
|
|
// SSH host management
|
|
openHostManager: () => void;
|
|
|
|
// Per-pane orchestration
|
|
setActive: (leafId: NodeId) => void;
|
|
registerPaneId: (leafId: NodeId, paneId: PaneId | null) => void;
|
|
broadcastFrom: (originLeafId: NodeId, dataB64: string) => void;
|
|
notify: (message: string) => void;
|
|
|
|
// Drag-header-to-swap. dragSourceId / dragOverId are reactive so leaves
|
|
// can apply hover/source styling. The lifecycle methods are stable.
|
|
dragSourceId: NodeId | null;
|
|
dragOverId: NodeId | null;
|
|
beginHeaderDrag: (leafId: NodeId) => void;
|
|
setHeaderDragOver: (leafId: NodeId | null) => void;
|
|
endHeaderDrag: (commitSwap: boolean) => void;
|
|
|
|
// Per-leaf idle reporting. LeafPanes call reportLeafIdle when their
|
|
// own quiet-state crosses the threshold; App aggregates so the titlebar
|
|
// can show an "N idle" count without spamming toast notifications.
|
|
reportLeafIdle: (leafId: NodeId, idle: boolean) => void;
|
|
|
|
// Multi-window pane transfer ---------------------------------------------
|
|
/** Pop a pane out of the current workspace into a fresh top-level window.
|
|
* The PTY stays alive across the move (the new window's XtermPane
|
|
* adopts the existing PaneId; scrollback ring is replayed). */
|
|
moveToNewWindow: (leafId: NodeId) => void;
|
|
/**
|
|
* Navigate focus from within a pane's key-handler. XtermPane emits the
|
|
* intent; LeafPane/App resolve the target leaf and set it active.
|
|
*
|
|
* `{ kind: "direction", dir }` — move to the spatial neighbour in that
|
|
* direction using the same flattenLayout geometry as Ctrl+Shift+Arrow.
|
|
* `{ kind: "index", n }` — focus the Nth leaf in DFS (walkLeaves) order,
|
|
* 1-indexed, clamped to the leaf count (so Alt+9 with 3 panes picks pane 3).
|
|
*/
|
|
navigateTo: (intent: NavigateIntent) => void;
|
|
/** Returns a PaneId only for leaves that just arrived via a window
|
|
* transfer (so LeafPane can pass `existingPaneId` to XtermPane to skip
|
|
* 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
|
|
* the actual target leaf from the current tree without XtermPane needing
|
|
* to know anything about layout geometry or leaf ordering. */
|
|
export type NavigateIntent =
|
|
| { kind: "direction"; dir: Direction }
|
|
| { kind: "index"; n: number };
|
|
|
|
const OrchestrationContext = createContext<Orchestration | null>(null);
|
|
|
|
export function OrchestrationProvider({
|
|
value,
|
|
children,
|
|
}: {
|
|
value: Orchestration;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<OrchestrationContext.Provider value={value}>
|
|
{children}
|
|
</OrchestrationContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useOrchestration(): Orchestration {
|
|
const orch = useContext(OrchestrationContext);
|
|
if (!orch) {
|
|
throw new Error(
|
|
"useOrchestration() must be called inside <OrchestrationProvider>",
|
|
);
|
|
}
|
|
return orch;
|
|
}
|