Skip to content

Latest commit

Β 

History

History
211 lines (175 loc) Β· 16 KB

File metadata and controls

211 lines (175 loc) Β· 16 KB

Keys & navigation

OpenTab opens on a stacked Months / Days (or Projects) sidebar, lazygit-style: drill from a month or day into its detail tabs, from the Sessions tab into a single session β€” cost split, model mix, subagent tree β€” and step back out with Esc.

Press ? in the app for the cheat sheet: a small panel that floats over what you were looking at and lists, lazygit-style, the keys that work where you are (this view, this tab, this overlay β€” Trends, the price table and a model's session list each have their own), then Navigation, then the Global keys. One short line per key; the long form is this page. The footer strip reads the same table, so it can only ever offer a key that does something here β€” and j/k in Trends says whether it is paging a month or walking a list, because there it depends on the tab.

How the views nest

Three levels: browse β†’ zoom β†’ session. Enter (or +) drills in, Esc steps back out. Zoom is not full-screen: the detail pane takes focus beside the sidebar, which stays clickable to re-scope in place; + maximizes/restores the detail pane (remembered between runs). The session view is full-screen.

Detail tabs per scope: years/months get Overview Β· Models Β· Projects Β· Sessions; days drop Models. A session adds Turns (per-turn cost over time, every harness that records per-step usage), Tools (per-tool / MCP spend) and Context (the context window's growth curve, % of the model's window, compaction markers, and β€” on harnesses whose logs carry content β€” an estimated breakdown of what filled it) when its harness supports them, and Harnesses joins in the merged all view. The Context tab also overlays how the session evolved: what it spent (with a per-turn and per-hour burn rate), its wall-clock span with clock times pinned to the chart's edges, and the clock time β€” and how far into the session β€” of each compaction.

Move around

Key Action
p / t / m Switch to the Projects / Time / Machines browse mode β€” Machines is one row per box (just this one until you pull another)
Tab / Shift-Tab Cycle focus Years β†’ Months β†’ Days (Time mode); Shift-Tab at the top steps back out
1 / 2 / 3 / 0 Jump straight to a panel β€” each panel wears its number in its title, lazygit-style: the sidebar top to bottom ([1] Years, [2] Months, [3] Days; in Projects mode [1] Projects) and [0] the detail pane on the right, what Enter drills into. A digit jumps from anywhere: it steps out of a zoomed detail or an open session to get there
Enter / + Drill into the selection; on a Sessions / Projects / Harnesses / Models tab, open it in this scope; on the Turns tab, fold/unfold the selected β–Έ prompt
Esc Step back out β€” session β†’ zoom β†’ browse
h / l Switch detail tabs
j / k Move in the list (↑/↓ too), or scroll the detail pane; on the Turns tab, move the β–Έ prompt cursor
PgDn / PgUp Half a page (Ctrl-D / Ctrl-U too)
g / G Jump to the top / bottom
Mouse Wheel scrolls Β· click selects (anywhere in the preview pane focuses it) Β· double-click drills Β· click a tab, or a column header to sort (again to reverse)

On the Turns tab, j/k move a cursor over the β–Έ prompt headers and Enter (or a click) folds/unfolds the selected one β€” its full prompt text and per-turn rows; g/G jump to the first/last prompt. z unfolds every prompt at once. A β–Ό line marks each context compaction β€” where the window was cleared between two turns, with what it dropped from and to. The tab's title counts them and the tokens they freed, and the lines stay visible while the prompts are folded: a compaction is a session-level event, not a turn. The Context tab charts the same events (one rule, both tabs) β€” and where that tab doesn't apply, because the harness records cumulative-total deltas rather than per-request prompts (Codex), neither tab marks anything.

Scope & filter

Key Action
R Set the date range β€” all Β· 30d (or 30) Β· 2m Β· 1y Β· 2026 Β· 2026-05 Β· start..end
a Back to all time, keeping the current selection where possible
s Sort picker for the visible list (j/k move Β· Enter Β· Esc). Sessions offer Start Date (created_at, default) and, everywhere except the Time overview's Days pane, Last Activity (ended_at, including subagent activity where tracked) β€” a single day's list is read by start time, and activity can run into a later day than the one the row is filed under, so ranking by it there is deliberately left out β€” that pane falls back to Start Date, keeping your choice for when you focus Months/Years again. The Date column follows whichever is active, and its header shows "Last act" under the latter. Projects offer the matching pair: Recency (the newest session's start) and Last Activity (the newest activity in any of the project's sessions, subagents included)
f or / Live filter β€” fuzzy (fzf-style) over sessions (title/project/id/note) and projects; model lists (P, w) match word-anchored (letters may scatter inside a word, a new word only joins at its first letter β€” opus48 works, opus no longer drags in qwen3-coder-plus), routes by substring. Non-ASCII (Γ€, η•Œ) can be typed. While filtering: ↑/↓ select Β· Enter keep Β· Esc cancel Β· Ctrl-U clear
x Clear the filter

Sessions & projects

Every session list carries a Worked column β€” how long the agent was actually working, summing its bursts and dropping the idle gaps where it waited for your next prompt (so a session you left open for hours shows minutes, not hours). It's derived from the human turns the transcript logs, not a guessed timeout; blank when the backend can't tell work from waiting (a source with no human-turn markers like Copilot OTEL or VS Code, or an export from an older opentab). It's sortable like any column (s picker or a header click), and a session's Overview spells it out: Started: … Β· worked 2h 15m (until 14:15). The Context tab still has the richer wall-clock story (burn rate, per-turn offsets).

Key Action
i / I Ignore / unignore the selection; I reveals hidden rows so they can be unignored
b / B Bookmark β˜… the selected session (remembered between runs); B shows only bookmarks, within the active range
n Note ✎ on the selected session β€” why it cost what it did, which no token count records. Opens a prompt seeded with the existing note (Enter saves Β· Ctrl-U clears Β· Ctrl-W kills a word Β· Esc cancels); saving an empty note removes it. An annotated session shows a ✎ in every list and the note in its Overview; f// searches note text too, and e exports it as a note column. Notes live in their own ~/.local/share/opentab/notes.json and are written the moment you save. Off under --demo / --no-state
o Open the selected session's / project's directory
L Launch the session in its own tool β€” opencode --session / claude --resume / codex resume. Then w window/tab Β· s right split Β· v lower split Β· p popup Β· y copy the command. tmux offers all spawn targets. Herdr offers a tab and both splits only when it provides a valid HERDR_PANE_ID for the current pane; otherwise it offers only the tab and copy. A launcher hook may offer all four. y copies anywhere. If tmux and Herdr are nested, OpenTab uses the innermost multiplexer. A session pulled from another machine reopens there only when its remotes.json entry has an SSH target: every available target wraps the command in ssh -t <target> 'cd … && …', and y yanks that same line. A box reached by url (no SSH target) offers only the yank
e Export the current list to a CSV in the working directory

Views & overlays

Key Action
T Trends β€” Daily Β· Weekly Β· Monthly Β· Calendar Β· Models Β· Providers Β· Harnesses. h/l tabs Β· j/k page months/weeks/years. On the charts and Calendar: Enter focuses, arrows pick a bar/day, Enter drills in, Esc back. On Models/Providers/Harnesses: j/k pick a row Β· s sorts its visible columns Β· Enter its sessions Β· Enter again opens one
P Model prices β€” the table behind the $ estimate; see Pricing for the views, sorting, and pinning
$ Toggle what-if prices β€” what unpriced usage would cost at API list rates
w What-if model β€” arm one priced model as a comparison target (j/k move Β· f filter Β· h/l (or Tab, or a click) switch the tier tabs between the models you've used and the whole models.dev catalog, cheapest-for-your-mix first Β· Enter arm Β· Esc cancel): "what if the expensive model had done the subagents' work too?". Used few models? The catalog tier is the point β€” it offers every model with a list price, and opens directly when nothing you've used is priceable. The selected session's Subagents tab then shows its whole tree (root included) with a What-if column β€” that node's tokens at the target's list rates β€” and a TOTAL (list rates) your models … β†’ all at … … saved … line; its Overview carries the same session comparison (Your models / All at target / Change). Both sides are priced at list rates β€” the only apples-to-apples basis for a rate substitution β€” so a session that delegated nothing (no tree to show) still answers, and repricing a single-model session at the model it already used is exactly a $0 change. There is deliberately no per-node Ξ”: a node can mix models, so no honest per-node baseline exists; the exact comparison lives at session level, where the tokens are split per model. The Cost column keeps its ordinary meaning (recorded spend, $-estimated where nothing was recorded), so it does not add up to the TOTAL. A rate substitution, not a rerun. Session-scoped β€” the sessions list, the day/month/project rollups and Trends keep showing actual spend, and $ keeps working as always. Works in demo too; w again clears it. The web browser mirrors all of it, on the same key
H Harness picker (j/k move Β· Enter switch Β· Esc cancel)
M Machine filter (fleet only) β€” narrow every view to one box; the harness picker's twin (j/k move Β· Enter arm/clear Β· Esc cancel)
C Colour-theme picker β€” j/k live-preview Β· Enter keep Β· Esc revert (themes are shared with the web browser)
D Demo (anonymize for a shareable screen) β€” opens a multi-check picker of what to scramble: Titles (session / prompt / project / model / machine names), Turns (the expandable full prompt text), Spend (dollars + token magnitudes). j/k move Β· Space toggle a category Β· a all/none Β· Enter apply Β· Esc cancel. While demo is on, D switches it straight back off (one press, no picker); the categories are remembered, so D again re-offers them. From the CLI: --demo (all) or --demo titles,spend
r / q / ? Reload the data Β· quit Β· help

The global toggles stay live inside the overlays: ?, C, H, M (fleet), and D work from anywhere, Trends and Prices included.

What persists between runs

The active harness, range, sort, focused sidebar panel, ignored projects, bookmarks, pinned price rows, theme, and $ what-if view are remembered between runs, stored in ~/.local/state/opentab/state.json (the XDG state dir β€” regenerable prefs). Pass --no-state to disable; --demo never persists.

Session notes (n) are kept apart, in ~/.local/share/opentab/notes.json (the XDG data dir). Everything in state.json is a preference opentab can regenerate or shrug off; a note is the one thing you wrote, so it gets its own file in the data dir, is saved on the edit rather than at quit, and a note whose session has since disappeared (a rotated transcript, a harness you didn't merge in this run) is kept, never pruned.

A w what-if target model is deliberately not remembered: it's a transient analysis mode, and a persisted one would silently re-frame every future launch's Subagents tab.

Two formatting rules worth knowing: sub-cent costs render as <$0.01 so they aren't confused with a red $0.00, which specifically means unpriced (tokens with no local price); and git worktrees fold into their main repo (--no-worktrees keeps them split).

Remap any key

Every key above β€” and every key in every picker, overlay, pager, prompt and text field β€” is remappable. The keymap lives at ~/.config/opentab/keymap.conf, a fully commented INI file installed on first run (also in the wheel as opentab/data/keymap.conf; opentab --keymap prints the path). Press K inside opentab to open it in $EDITOR ($VISUAL wins, vi as fallback): edit, save, quit, and the new bindings are live the moment the editor returns β€” the footer chips, the ? cheat sheet, and every modal title re-label themselves from the file, so the UI never advertises a key that isn't bound.

One line per action, first key shown in the UI, comma-separated aliases, empty value unbinds:

[main]
# sort this list
sort = o
# step back out β€” session β†’ zoom β†’ browse
back = esc, backspace, h
# an empty value unbinds (comments are full-line only: # or ; at line start)
export =

[menu]
# one line re-teaches j/k in EVERY picker (sort, themes, launch, …)
down = n
up = e

Key syntax: a single character (case-sensitive β€” S is shift-s; non-ASCII like ΓΆ works), named keys (enter esc space tab shift-tab backspace delete insert up down left right pgup pgdn home end f1–f12, comma for a literal ,), and control chords (ctrl-u or ^u, letters only). Ctrl-C is the hardwired panic quit and cannot be rebound.

Contexts mirror what owns the keyboard: [main] for browse/zoom/session, [trends] (+ [trends.chart] for a focused chart, [trends.drill] for a ranked row's session list), [prices] (+ [prices.sessions]), [help], [notices], the shared [menu] with per-picker overrides ([menu.sort], [menu.theme], [menu.launch], [menu.whatif], …), [filter] for the live filter line, [input] for the note/range prompts, and [prompt.prices]. A sub-context falls back to its family for anything it doesn't name; anything the file doesn't name falls back to the built-in default β€” so the file survives upgrades, and deleting a line (or the whole file) restores stock behavior.

Typos never break the TUI: a bad key name, an unknown action, or two lines fighting over one key each fall back sanely and land a precise warning as a toast (press N for the list). Rebinding a key takes it away from whatever held it β€” bind x to down and x no longer clears the filter (with a warning that clear_filter is now unreachable, unless you bind or unbind it yourself).

Custom launchers

If an executable exists at ~/.config/opentab/launcher (or $OPENTAB_LAUNCHER points at one), every L-menu launch is handed to it instead of the built-in tmux commands β€” git-hooks style. It's called as

launcher <kind> <directory> <command>
# kind ∈ window | hsplit | vsplit | popup
# e.g. launcher window /repo/myproj 'claude --resume abc123'

and a nonzero exit shows its stderr as the launch error. The footer reads "launch via launcher hook" when one is active.

For a session pulled from another machine the <command> is already the full ssh -t … 'cd … && …' line and <directory> is your home β€” the hook runs it locally, exactly as it runs a local one, and the cd happens on the far side.

Example hook β€” route launches through zellij (or kitty, or your own popup manager):

#!/bin/sh
# ~/.config/opentab/launcher β€” example: zellij instead of tmux
kind=$1 dir=$2 cmd=$3
case $kind in
  window) exec zellij action new-tab --cwd "$dir" -- sh -c "$cmd" ;;
  popup)  exec zellij run --floating --cwd "$dir" -- sh -c "$cmd" ;;
  *)      exec zellij run --cwd "$dir" -- sh -c "$cmd" ;;
esac