The per-file map of Claude Code Tray's own sources — every file under src/, one row each, with what it is responsible for and the rules its type carries. Use when you need to know which file owns a behaviour, where a new file goes, or what a type already does before writing a second reader of the same thing.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add alegauss/claude-tray --skill file-map --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of File Map?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/alegauss-file-map)More formats (shields.io, HTML) on the badges page.
---
name: file-map
description: The per-file map of Claude Code Tray's own sources — every file under src/, one row each, with what it is responsible for and the rules its type carries. Use when you need to know which file owns a behaviour, where a new file goes, or what a type already does before writing a second reader of the same thing.
---
# File map (Claude Code Tray)
Every file under `src/`, one row each. **This file is a reference, consulted rather than read** —
which is why it lives here and not in [AGENTS.md](../../../AGENTS.md), whose whole content is loaded
on every turn against a byte budget (T219). AGENTS.md keeps the folder-level map and the placement
rules; the per-file detail is here, complete.
The sources live under `src/`, one folder per subsystem (T129–T132) — the folder is the map, this
file is the detail. **Where does a new file go?** In the folder of the subsystem it belongs to; the
repo root is for the project, not for code. The namespace stays **flat `ClaudeTray`** regardless of
folder, `lang/` never moves, and the csproj globs `**/*.cs` so a new folder needs no csproj edit —
AGENTS.md states all three, because getting them wrong costs a redo.
## `src/Tray/` — the resident app
| File | Responsibility |
|---|---|
| `src/Tray/Program.cs` | `Main` and nothing else: the arg dispatch that routes every flag, `ArgValue`, plus `StartupManager` (the HKCU Run entry) and `WpfInputBridge`. `Utf8Console` is the **one** place the console's code page is set (T283) — a property of being a read-out, not of any flag, and `--selftest` fails if a second file sets it. |
| `src/Tray/TrayContext.cs` | The resident app: tray icon, menu, poll/flash/update timers plus the 6h `_backgroundSample` one (context scan + activity-grid warm-up), `ApplySettings`, tooltip, icon render, the watched-profile list and `OpenMain` — the **one** entry point to the **one** window (T158), from a left-click on the icon or the menu's bold *Open*. |
| `src/Tray/IconRenderer.cs` | GDI+ icon (vector number + outline + fill bar + projection color) at the real size; also the app `.ico` and social image. |
| `src/Tray/Updater.cs` | Checks GitHub Releases; downloads/runs the installer for in-app self-update. `CurrentVersion`. |
| `src/Tray/TooltipText.cs` | What the tooltip says, composed from an explicit `Input` rather than from a live tray (T214) — it is `NOTIFYICONDATA.szTip`, drawn by the shell, so there is nothing to screenshot and nothing in the accessibility tree, and composing it away from the tray is the only way the surface can be reviewed at all. Owns `Cap`, the 127 characters that survive the Windows limit and decide whether the projection sentence appears in full, compact or not at all. |
## `src/Cli/` — the headless printers
One file per flag family; `Main` dispatches, these run and exit.
| File | Responsibility |
|---|---|
| `src/Cli/ContextCli.cs` | `--context` and `--context-report`: the scan printed as a report (projects, sources, findings, cleanup prompt, skill/agent usage, calibration) and the same scan written as markdown. |
| `src/Cli/ActivityCli.cs` | `--activity`: the weekly activity profile as a 24×7 shaded grid, plus the measured-hours variant. |
| `src/Cli/ReadOut.cs` | How a headless read-out reports that it could not do the thing it was asked (T261): `Failed(error)` prints the sentence **and** sets the exit code, so no call site can do one without the other — four of them printed `error:` and returned 0, and `--root` exists for the fixture loop, where the caller is a script. The rule it states is measured, not assumed: `ContextScan.Error` is only ever a scan that returned immediately, while a scan that finished with holes carries `Truncated` and a null error, and `UsageEvidence` — the one type where partial is representable — says so with `Complete` and stays a note. |
| `src/Cli/LiveCli.cs` | `--tail` and `--live`: each assistant turn as it lands, and the rolling tok/s with its per-project sparklines. |
| `src/Cli/SessionsCli.cs` | `--sessions [--all\|--refresh] [--project <slug-or-name>] [--root <dir>]`: one row per **conversation** — last turn, duration, calls, billed tokens, cache reads, how many transcripts the fan-out wrote, models. The headless companion to the page, and the only way to falsify `SessionIndex`'s per-file cache: it prints the tree it walked, files-read beside files-seen, and `--refresh` re-reads every one of them. |
| `src/Cli/ProfilesCli.cs` | `--profiles [--check]`: every profile on the machine, its auth, its config-dir action and its icon accent band. |
| `src/Cli/SelfTestCli.cs` | **Seven files, one class** (T381): `Run` and the vocabulary (`Check`, `Skip`, `Temp`, `CodeOf`, `Repo`, the counters, `IsSuite`) here, then `.Usage` (the quota arithmetic, pure over synthetic readings), `.Transcripts` (the byte cursor, the grid, sessions and what one was worth — it builds a `projects/` tree and walks it), `.Docs` (a document against what it documents), `.Lang`, `.Ui` (previews, captures, what a control announces) and `.Profiles` (accounts, auto-follow, the linking script). A source scan that must skip the suite keys on `IsSuite`, by stem: keyed on the filename, one went red on the split rather than on a defect. `--selftest [--quick]`: 380+ assertions of two kinds — an invariant over synthetic inputs (pacing, store, grid, tail, live-rate, series, language table, method note, number format, palette) and a document against the thing it documents (the ledger's index, this map, the flag catalogue, the active marker) — exiting non-zero on failure — run on every push by `.github/workflows/check.yml` and again before an installer is packaged by `build.yml`. Writes only a temp tree and a `selftest` profile dir, both removed. Every check that reads this repository's own sources goes through `CodeOf` — comments removed lexically, string literals kept — because each of the three that hand-rolled that first read its own explanatory paragraph as the thing it was counting (T285). The repo's test suite. |
| `src/Cli/ProbeCli.cs` | `--probe [--live] [--all]`: the rate-limit headers verbatim — the recorded capture log first, then one live call, which is itself recorded against the monitored profile (T212) rather than printed and dropped. This app reads nine of the fourteen, and its reading is not what the API said — which is why every printed name now carries a READ/UNREAD mark taken from the parser itself, and each profile opens with the count (T278). Quota metadata only, never a token. |
| `src/Cli/StatsPreviews.cs` | The one table of Statistics previews `--stats` and `--capture-stats` both read (T186): a variant per row with what it feeds the page, the modifiers that compose with any of them, and the refusal — an unknown name prints the catalogue and exits 1 rather than rendering the default sample as if it were what was asked for. |
| `src/Cli/ToastPreviews.cs` | The same table for the toast cards, read by `--simulate-reset` and `--capture-toast` both (T198). Two rules it carries: an unknown variant is refused with the catalogue, and **a capture flag never defaults its output path** — `--capture-toast` requires one, and a default that is kept goes under git-ignored `docs\_preview\`, never the working directory. |
| `src/Cli/PreviewCli.cs` | The deterministic previews behind the published images: `--simulate-reset`, `--capture-toast`, `--check-toasts` (T257: every card asked whether it fits, in the language the process is in), `--render`, `--makeicon`, and the gap-demo report `--stats gapdemo` feeds. `--render` writes **three** contact sheets, each 8× with the real pixels and on both backdrops: `SaveMarkSheet` for the accent band (T147), `SaveBillingSheet` for stopped-against-paying at every tray size (T265) and `SaveScopeSheet` for the digits' own colour naming their window at 16px (T321). Separate files on purpose — one image carrying two claims cannot say which of them failed. |
| `src/Cli/PreviewSurface.cs` | What a preview tells `Capture-Window.ps1` it drew, on stdout, as a rectangle the script asserts against (T217). Only the app knows what it drew and where, so a capture that honestly reports a correct copy of a window with no popup in it — the silent failure that cost three captures verifying T170 — is caught by the app naming its own surface rather than by the picture. |
| `src/Cli/TooltipCli.cs` | `--tooltip [variant]`: the tray tooltip's text for a synthetic reading, printed with each row's character count against `TooltipText.Cap` (T214). The read-out for the one surface no flag can photograph, in every state and any language. |
## `src/Usage/` — quota, spend and live throughput
| File | Responsibility |
|---|---|
| `src/Usage/ApiClient.cs` | Reads OAuth token from `~/.claude/.credentials.json`, calls the API, parses `anthropic-ratelimit-unified-*` headers. The parse is one method over a *lookup*, so `NamesRead` is the parser enumerating itself (T278) — the read-out saying which arriving headers reach a field cannot drift from the lines that read them. That is exact only while the parse has **no branch**: `--selftest` scans this file's own header literals against `NamesRead` and fails the build on a conditional read (T284). |
| `src/Usage/HeaderProbe.cs` | The capture log behind `--probe`: every rate-limit response whose header *shape* has changed, appended per profile with the headers verbatim. Records a transition whenever it happens, so the reading T181 needs does not depend on somebody running a command at the right moment. `IsRead`/`Unread` answer the other half — which of the recorded names this app actually parses — from `ApiClient.NamesRead` rather than from a list of its own (T278), and `Readership` carries that as one value so the count the read-out states cannot be recounted apart from the names under it (T282). |
| `src/Usage/QuotaState.cs` | Which of three states an account is in — in the quota, past it and billing, or stopped — ranked over five signals: an observed overage figure, a measured refusal, the `overage-in-use` header, the local extra-usage flag, and the overage status that may buy a poll but never paint a screen. The single answer the icon, the tooltip, the poll's idle and the toast all read, so they cannot disagree about whether work has stopped. Its `Resolve(UsageData, …)` overload answers about the **account** — the worst bounded window, never the metric on the icon (T274); the caption stays with the metric, in `TooltipText`. |
| `src/Usage/ExtraUsageAlarm.cs` | The "you have started paying" transition, as a sequence rather than a predicate: a seed, a rise on either `overage-in-use` or the overage figure, and one latch so a spell is announced at most once and the next spell still can be. **It takes its seed rather than reading one** (T290) — the notifier used to fetch the previous reading from `UsageHistory`, which the same poll had already appended to, so a first poll compared itself with itself and a crossing at launch was never announced. A type that cannot look up its own previous reading cannot look up the wrong one, which is also what lets `--selftest` drive the whole sequence with no tray. It also **carries the profile key its readings belong to** (T292): the tray rebuilds it when the icon changes hands, and a reading arriving for another account re-baselines quietly instead of comparing — so a forgotten call site costs one late alert rather than a false "you have started paying". |
| `src/Usage/MonitoredAccount.cs` | Everything the tray holds in memory **about the account the icon follows** — the reading and when it was taken, the last good snapshot, the burn tracker, the extra-usage alarm, the overage spell's start, the transient-error count and the auth auto-open latch (T293). It exists so a profile switch is **one assignment** instead of a run of statements nobody could check: a field added here is dropped by a switch because there is nowhere else for it to live. Building it is what found the last three — leaving `ConsecutiveErrors` behind drew a red error icon on an account whose own polls had never failed, and leaving `AutoOpenedForAuth` behind suppressed the sign-in prompt on the account you had just switched to in order to sign in. **Not** for `_otherData` (keyed per profile on purpose, T137) or the `ApiClient` (a handle, rebuilt from the profile list). Its constructor takes the profile key, because `ExtraAlarm` is seeded from that account's own history and the ordering T290 fixed still binds. `TrayContext.RefreshWatched` is the only assigner. |
| `src/Usage/OverageSpell.cs` | Which reading the *current* overage spell started at — a walk backwards through `UsageHistory` to the first of the run the newest reading belongs to (T280). Read from the store rather than remembered, so a tray restarted mid-spell answers the same as one that watched it begin. **It answers `null` rather than guessing**: a run reaching the oldest line on file, or one preceded by a reading carrying neither header, has no observed crossing, and the store's own beginning is not an event. A gap in the readings does *not* restart the run — the alternative would be inventing a return to the quota nobody measured. Pure over a list, so `--selftest` drives every ending with no tray. |
| `src/Usage/UsageReport.cs` | The pacing report over the two rate-limit windows (5h session, 7d week): the live headers say how much is used and when it resets, the transcripts give the *shape* of the burn, scaled to land on the live number. |
| `src/Usage/ListPrices.cs` | The published API rate card, compiled in, plus the arithmetic that turns a conversation's counted tokens into a **list-price equivalent** (T346): dollars per million by model-id prefix (longest match, so a dated id resolves through its family), a cache read at 0.1× the model's input rate, a five-minute write at 1.25× and a one-hour write at 2×, and a TTL-less write at the dearer of the two. Carries `Read` — the date the rates were read — because a rate card with no date goes stale silently, and every surface that shows a figure prints it. Reports what it could **not** price rather than folding an unknown model in at a guess. Not a bill and no string here says "cost": §I.7. |
| `src/Usage/UsageHistory.cs` | Append-only log of each successful poll's rate-limit reading (`usage-history.jsonl`, pruned at 8 days) so the burn-up charts draw *measured* utilization instead of inferring it from token counts. Three of its columns are nullable on purpose — `ux`/`rx` (the overage figure) and `ix` (the API saying the account was past its quota): absent is not a measured zero and not a measured no, and a spell can carry the third without the first. |
| `src/Usage/BurnTracker.cs` | Utilization history → least-squares slope → projects exhaustion (`Projection.Ok/Danger/Unknown`). |
| `src/Usage/UsageInsights.cs` | Aggregates last 24h of `~/.claude/projects/**/*.jsonl` into a cost-weighted breakdown. Owns the per-model `Price` table the whole app shares. |
| `src/Usage/ActivityProfile.cs` | The weekly activity shape: 168 buckets (day-of-week × local hour) of `p(active)`, mined from transcript **timestamps only**, decayed per week and shrunk toward a flat prior. Cached daily in `%LocalAppData%\ClaudeTray`, over a per-file sweep cache (`activity-sweep.json`, path+size+mtime → the absolute local hours that file touched) so a rebuild costs only the transcripts that changed; the week index is derived at aggregation, never cached, because it is relative to now. The projection follows this instead of a uniform slope. |
| `src/Usage/WorkKinds.cs` | **Which kind of work ate the range**, folded out of the task rows the index stores: one row per task kind and one per *named* slash command — tasks, median, total — heaviest first. Median rather than mean, because one overnight run among forty short ones moves a mean and says nothing about a usual task; commands split by name, because the finding behind it was about one command and not about automation in general. A command's name is a name (§I.1); the prompt after it is never read. Reports, never advises. |
| `src/Usage/HeaviestWindow.cs` | The **heaviest five hours on record**, swept out of the index's per-minute token series rather than read from the API — a window that has already closed, which is the question the anchored 5h window cannot answer. Two pointers over the occupied minutes (a month is twelve thousand buckets among four hundred thousand empty ones), a half-open frame, and the median **active** day's own peak beside it so the number has something to be compared against. Minute resolution is the settled trade: 0.018% under the exact per-turn peak, against 2.6% for hour-aligned. A measurement and never a prediction — nothing here knows the plan's allowance. |
| `src/Usage/HourlyUsage.cs` | Permanent per-hour aggregate (spend + coverage per local day) folded out of `usage-history.jsonl` before its 8-day pruning discards it. Lets idle be *measured* instead of inferred, and is the store week-over-week comparison reads. Owns the measured half of the away-week test (T152): a week is judged only once half its 168 hours carry a reading, then dropped if it is under `AwayFraction` of the median judged week's active hours. |
| `src/Usage/ActivityShape.cs` | The weekly projection spent along that shape: calibrated per *measured active hour*, flat through usually-idle stretches, sloped through working ones. Returns null (→ the old straight line) when the profile is thin or the window can't be calibrated. Weekly only. |
| `src/Usage/TranscriptTail.cs` | Byte-level tail over `~/.claude/projects/**/*.jsonl`: watcher + 3s floor sweep, a per-file cursor that only advances past a newline, and de-duplication by `requestId`. The watcher's paths are the sweep's **work list**; the whole tree is walked only every 30s (`ReconcileMs`), or on every sweep when there is no watcher. Reports each assistant turn within ~250ms for the cost of the appended bytes. `--tail`. |
| `src/Usage/LiveRate.cs` | The rolling tokens/s over that tail: an age-weighted 60s window (triangular kernel, so a pause decays from the moment it starts) with attack-only smoothing. Caller-driven `Tick` — no timer, so a hidden window costs nothing. Also serves that rate **as a series** (`RateHistory`/`TypeRates`/`Projects(n)`: the same kernel at every second, so the newest point equals the headline, plus a 3τ smoothing warm-up before the first reported point so a second's value never changes after it has been drawn) and each project's **sticky slot** — fixed colour/order while it has anything in the window. Sits beside `WindowPace.TokensPerSecond`; neither is quota. `--live`. |
| `src/Usage/SessionFixture.cs` | A synthetic `projects/` tree for the **Sessions pane**, so a published screenshot of it is a picture of invented work and never of somebody's own (T335). `--capture-stats` renders that pane from the monitored profile and the pane carries each conversation's opening prompt, so the command that makes a README shot would otherwise put real prompts in a public git history — and a screenshot cannot be un-published. Same shape as `AccountFixture`: several projects, a fan-out under one conversation, both task kinds, and one prompt past `PromptChars` so the truncation is visible rather than assumed. Reached by `--stats sessions` / `--capture-stats <out> sessions`. |
| `src/Usage/SessionIndex.cs` | One pass over a profile's `projects/**/*.jsonl` producing **one row per conversation** — the unit every other reader here aggregates away (window, project, hour). Per row: slug + display name, first and last turn, calls, the four token classes, every model that answered, and how many transcripts the fan-out wrote. Three properties it exists to hold: a fan-out folds into the session that spawned it (via `TranscriptTail.Locate`), one response is one call however many content-block lines carried it, and it reads *a profile's* tree. A scan, not a tail — `TranscriptTail` answers "what is happening now" from appended bytes; this answers "what did that session cost" from the whole file. Cached per transcript on T92's path+size+mtime key in the shared `session-index.json`; measured 4.5s cold over 1.2 GB, 117ms warm. `--sessions`. |
| `src/Usage/EffortMix.cs` | The effort ladder and what a session or task ran at, as a **mix** rather than a winner: `low`/`medium`/`high`/`xhigh`/`max` in that order — which is not alphabetical, and the reason the scale is spelled in one place — plus the merge two readers fold call counts with, and the one-line rendering (a single level is its own name; several become shares, so the dear minority survives). A level the ladder does not name is kept and printed as the transcript spelled it. Labels come from `stats.effort.*`, so a level nobody here has run is still legible on its first sighting. |
| `src/Usage/SessionTasks.cs` | One session cut into the **tasks** that produced it, with the fan-out each caused hanging under it: task → workflow → agent, each node carrying its own cost beside its subtree's. A task begins at a person-ask and ends at the next one; telling a person-ask from the many `user` lines that are the harness writing about itself is `SessionIndex.TryReadPersonAsk`'s job, shared so the two readers cannot disagree — and a mid-turn message is not a `user` line at all but an `attachment` of type `queued_command`, which is where every one of them lives. Turns predating the first ask become one *inherited* node, or a resumed session's spend vanishes. On demand and uncached: a session is a handful of files. `--sessions <id>`. |
| `src/Usage/LiveChart.cs` | Pointing at a plot snaps a crosshair to a second, dots each line and opens an in-plot readout of that second (T104) — its text comes from the caller's `Readout` callback, its numbers from the *drawn* history. Draws that rate as the last 3 minutes of **lines** on the Statistics window's **Throughput** tab — two instances, one per project (fixed slot colours + grey "others") and one per token type, both always drawn so a colour never changes meaning. 1 Hz rebuild + a `TranslateTransform` slide cancelling its jump, two samples past the left edge so the endpoint is clipped rather than oscillating; see the type doc for both. Flat and silent when nothing runs; stopped when the window is hidden or minimized, and not drawn at all while another tab is selected. **Appends** to its own history rather than re-importing the recomputed series, so a turn reported late cannot rewrite points already on screen (wholesale adopt only on first render, a changed series set, or a gap in the clock). Scaled to the **newest** 180 samples and to a round ceiling ruled + labelled in tok/s in a right-hand gutter — the visible peak, or the p95 of the moving samples once the peak is >2× it, with the runs above it drawn dashed along the ceiling and the hover saying how many and how big. `--stats live` renders a deterministic synthetic chart for screenshots (`ThroughputFixture`). |
| `src/Usage/ThroughputFixture.cs` | The deterministic three minutes behind `--stats live` / `--capture-stats … live`: four repos plus a residual, one deliberate cache write, and the pose the readout is held in. Shaped through the real `LiveRate.RateFrom` kernel, so the published image is what the app would draw. `ContextFixture`'s rule: published chart shots come from here. |
## `src/Context/` — the Context Load Inspector
| File | Responsibility |
|---|---|
| `src/Context/ContextScanner.cs` | Scans every file Claude Code loads before the first prompt (instruction chain + `@imports`, memory index/files, skill & agent frontmatter), splits **eager** (paid every request) from **lazy**, measures observed session-zero from transcripts, and caches the scan by a path+size+mtime fingerprint. |
| `src/Context/ContextFixture.cs` | Builds a throwaway `~/.claude` lookalike (`--sample`) where all 16 rules fire. Use it for any published screenshot — the real machine's project names are client names. |
| `src/Context/ContextReport.cs` | Renders a whole scan as one markdown document (`--context-report`): summary, project table, findings, evidence, and the method behind the numbers. Paths and counts only. |
| `src/Context/ContextNudges.cs` | Rate limiter for the opt-in context-growth toast: at most one per project per week, remembered in `context-nudges.json`. |
| `src/Context/ContextHistory.cs` | Append-only log of each project's eager context (`context-history.jsonl`), one line per project per day and only when it moved. Feeds the drift sparkline and the "+N this week" line. |
| `src/Context/ContextPrompt.cs` | Builds the cleanup prompt handed to Claude Code: findings + fixes + paths, never file contents, and it asks Claude to show its plan before deleting. The app has **no** write path into `~/.claude` — see IMPROVEMENTS §I.4. |
| `src/Context/ContextUsage.cs` | Mines the transcripts for `Skill`/agent invocations (names and counts only) so the window can say "used 45×" or "never". Per-file cache; runs outside `Scan` because it reads hundreds of MB. Memory recalls are deliberately not counted — see the type doc. |
| `src/Context/ContextRules.cs` | The advisor over a `ContextScan`: grounded rules → `Finding` (severity + one sentence + the concrete fix). No new IO and never file contents; narrowed to what is objectively measurable so it doesn't cry wolf. |
| `src/Context/TokenEstimate.cs` | Chars→tokens estimation for markdown, classified per line (prose / code fence / table). Always rendered as an estimate ("≈4.9k"). |
## `src/Profiles/` — accounts, profiles and settings
| File | Responsibility |
|---|---|
| `src/Profiles/ClaudeAccount.cs` | The local Claude Code account/install reading behind the **System information** settings page: `.claude.json` + `.claude/.credentials.json` → plan (tier → "Claude Max 5x", unmapped verbatim), holder, org, extra usage, dates, config dir (`CLAUDE_CONFIG_DIR` honoured), CLI version, project count. Every field nullable; opens files, never writes one; reads the credentials file **only** for `expiresAt`, `subscriptionType` and the scope *count* — no token reaches the UI. `Read(dir)` reads **one** config dir; `Discover()` returns every **profile** (default first, deduped by `accountUuid`), and the `~/.claude.json` fallback is offered *only* for the home dir, or a second profile would inherit the default account's identity. **Only `Discover(settings.Profiles)` applies the label the user typed** — anything reporting what a menu will say takes it (T232). `Discover()` also resolves each profile's **effective auth** (subscription / API key / Bedrock / Vertex) from files+env — presence of a key only, never a value — and `QueryAuthStatusAsync` asks `claude auth status --json` for the authoritative answer. The settings-file pick prefers whichever candidate **names an account**: a near-empty `~/.claude/.claude.json` would otherwise shadow the real `~/.claude.json`. `--profiles [--check]`. |
| `src/Profiles/AccountFixture.cs` | Builds a throwaway pair of config dirs — a personal **Max 20x** (no org, so that row collapses) and a **Team seat** (org, type, role) — read back through `ClaudeAccount.Read`, so the page's own parser sees the fixture. Use it for any published shot of **System information** (`--settings System --sample`, `--reveal` to unmask): masking hides a name and an address, but the organization and its mail domain *are* the reading, and here the org is a client's. No token is written even in the fixture. |
| `src/Profiles/EnvironmentFixture.cs` | The sampled `CLAUDE_CONFIG_DIR` behind `--sample-env` (T231): the disagreements this machine is never in, so T172's mark and T173's read-back are visible without rewriting the developer's registry. Modes resolve from the profiles really here; sampling is one-way. |
| `src/Profiles/ProfileStore.cs` | Where everything derived from **one** profile lives: `%LocalAppData%\ClaudeTray\profiles\<key>\`, key = the account (`acct-<digest>`) or the config dir (`dir-<digest>`). `UsageHistory`, `HourlyUsage`, `ContextHistory`, `ContextNudges` and the `ActivityProfile` cache all take that key **explicitly** — no ambient "current profile", so polling several means passing keys. Owns the one-time **move** of the pre-profile flat files into the default profile's dir — a copy would double the hourly store. `context-cache.json` / `context-usage.json` stay shared: keyed by path + size/mtime, they cannot confuse profiles. Also `Observing`: **a store that writes consults it**, so `--second-tray` persists nothing (T239). |
| `src/Profiles/ProfileActivity.cs` | Which profile is being worked in *now*: the newest `projects\**\*.jsonl` **write timestamp** per config dir — directory entries only, never a file opened — probed on the usage poll's cadence, so auto-follow (T126) costs no resident watcher (the reason T101 was dropped) and ~20ms per config dir. `Pick` applies the policy: on-subscription + credentials on disk, a turn inside `FollowWindowSeconds` (30min), nothing stamped more than `MaxFutureSkewSeconds` ahead, and never below the floor a manual choice stamps. Also the single `<config dir>\projects` resolver `ProfileRef` reuses. **`MarkShared` is the refusal that is about the evidence rather than the profile (T365)**: a junction can put two config dirs behind one `projects` tree, and then the newest write is the same fact twice — so both are taken out of the running. Not a tolerance on the comparison, which would have covered nothing: `Read` walks the profiles in order, so a turn landing between two walks made whichever was scanned *second* look newer, and that is never the profile the icon is already on. |
| `src/Profiles/ProfileLink.cs` | The script that makes two profiles **one setup**, composed and handed over — never run (T367). `Catalogue` is the fixed table of what a config dir holds and what happens to each entry, and the four verdicts are the design: **merge** then link (`projects`, `file-history`, `skills` union by entry name; `history.jsonl` by line, ordered on its timestamp), **adopt whole** where a per-entry merge breaks something (`plugins` records an absolute `installPath`; `CLAUDE.md` is prose), **withheld** for `settings.json`, whose union widens the other account's permission allowlist and is therefore emitted commented out, and **never** for `.claude.json` and `.credentials.json` — §I.6 is not re-decided in a merge. `For` asks the filesystem which side has what and whether a link is already there, so a second run is a no-op; `Script` is pure over that plan — no clock, so the same pair renders the same text twice and `--selftest` asserts what is in it. The emitted PowerShell prints its plan and needs `-Apply` to act, never elevates, never deletes (originals move to `<name>.pre-link-<stamp>`) and refuses **before the first move** when the plan needs a file symlink and the machine has neither Developer Mode nor elevation. `--link-profiles`. |
| `src/Profiles/SettingsUnion.cs` | What unioning two `settings.json` files would add, so the one decision `ProfileLink` withholds can be made on evidence (T373). **Read, never merged** — §I.4 permits measuring permissions and forbids editing them. Three things it keeps apart, each of them a way a report about risk stops describing risk: the **two directions** (a union adds to each side), **narrowing counted apart from granting** (entries arriving in `deny` take capability away, and folding them in makes the safe half look risky), and **hooks reported per event** with a count each side, because a hook is a command line that runs and no total answers it. `Error` means nothing was measured and never that nothing would change — a file only one side has is the *widest* form of this decision, not the emptiest. `Lines` renders it as the comment block the script carries; parses comments and trailing commas, since a hand-edited settings file has both. |
| `src/Profiles/EnvironmentProfile.cs` | The one thing the app writes outside its own settings file: the user-scope `CLAUDE_CONFIG_DIR`, so a profile picked by hand applies to every Claude Code session (T145). What the tray sets, the tray removes — the previous value is remembered and put back. |
| `src/Tray/MenuAccess.cs` | What the tray menu tells UI Automation, which is not what it draws (T234). `ToolTipText` reaches **no** accessibility property, and it is where the Profile submenu states how far a pick reaches (T171) and that a running session keeps what it started with (T172) — so `AnnouncingMenuItem` supplies an accessible object whose `Help` carries it, which is the one property a client can read (`AccessibleDescription` was measured and reaches only `LegacyIAccessible`, which the managed client cannot name a pattern for). That custom object costs the framework's TogglePattern, so the state is announced as a word in front of the sentence: `SwitchMenuItem` says on/off — where "off" and "not a switch" used to be the same silence — and a checked entry says so. `MenuAccess.Announce` sweeps the built menu on open, so an entry written later is covered without being listed. |
| `src/Profiles/Settings.cs` | `Settings` model (JSON in `%LocalAppData%\ClaudeTray`, path exposed as `Settings.DataDir`); clamps out-of-range values. `MonitoredConfigDir` picks which profile the **icon** follows (`ClaudeAccount.PickMonitored` is the one implementation the tray and `--profiles` share), and `FollowActiveProfile` lets `ProfileActivity` move it. `Clone()` is a JSON round-trip through the same serializer that writes the file, so the settings edit buffer and `ApplySettings`' write-back are **total by construction** — a new field needs no copy line anywhere, which is the point (T141). The other end of that round trip is `CarryUnchangedFrom(live, opened)`: Save merges **by which write is newer** — any field the page left at the value it opened with takes the live one, so a menu pick made while the window sat open is not written back over (T229). It replaced T162's `[TrayOwned]` attribute, whose two-owners-per-field assumption `FollowActiveProfile` and `SyncEnvironmentProfile` broke: each has a control on the Claude Code page *and* a toggle in the menu, so neither could be marked. **Nothing has to be declared any more** — a field with no control on any page simply never differs from the snapshot — which is what makes T126's and T155's "missing from the list" defect unrepeatable. Also `ClaudeProfile` — a registered profile is `{Label, ConfigDir, WorkDir}` and **nothing else**: no address, no token, no plan, so the tray is never a second store of credentials. |
## `src/Core/` — helpers with no subsystem of their own
| File | Responsibility |
|---|---|
| `src/Core/Localization.cs` | The dependency-free `L` / `{local:Loc key}` layer over the embedded `lang\<code>.json` files: language picked from Settings, else the OS UI language, English as the fallback for a missing key. Also `L.N(stem, count, …)` — a counted string as `<stem>.one` / `<stem>.many`, the count as `{0}` through `Nums` (T376). Two forms, not three: all five shipped languages have two, and a plural library is a NuGet package §I.3 forbids. `--selftest` fails a stem with only one half, and a `.many` with no `{0}`; the singular needs none, because `"1 minute"` is prose. |
| `src/Core/Dates.cs` | The other half of `Nums`' rule (T263): a date follows the display language, and that means the **order** of its fields and not only the words. A custom pattern pins the order while the culture supplies the month name, which is how an axis read *"début août 3"* in French and `3/8` for 3 August to an American whose short date is `M/d`. `MonthDay` narrows the culture's own `MonthDayPattern`; `DayMonthDigits` takes the order and the separator from its `ShortDatePattern`. The System page needs none of this — standard specifiers (`d`, `g`) already carry the order, and only a year-less date and a plot label do not have one. |
| `src/Core/Nums.cs` | The app's **only** namer of a culture for a number (T216): `Of` and `Pct`, both invariant, plus the reasoning for why there is one convention rather than a split between charts and prose. Dates are the other half of that rule and go through `L.DateCulture`, which is named for what it may reach. `--selftest`'s `format` section sweeps every static formatter on every surface listed in `Surfaces` against it. |
| `src/Core/OutFile.cs` | Creating a file a flag was told to write, directory and all (T187) — the rule every capture shares, stated once instead of at each call site. |
| `src/Core/StoreFile.cs` | The one way this app mutates its **own store**, counting each effective write as it happens (T356). `--selftest` asserted "a check run adds nothing to your files" by diffing `%LocalAppData%\ClaudeTray` — a tree the user's resident tray also writes, so a run straddling a poll went red on somebody else's work, four times in an afternoon. A diff cannot answer *did **we** write*, because when the gate holds every change it sees is another process's by definition; a counter can, and this is where it lives. Only effective mutations count (creating a directory that exists, deleting a file that does not, change nothing). It is not a gate — `ProfileStore.Observing` stops the write, this only records one — and the source scan beside `ObservingTray` is what keeps every store writer coming through here, since a counter is worth exactly as much as its being the only way past. |
| `src/Core/SampleRoot.cs` | Where a fixture is built: a directory whose path holds no user name (`%PUBLIC%`, temp as fallback), because fixture screenshots get published and an absolute path spells out the Windows account. Shared by `ContextFixture` and `AccountFixture`. |
| `src/Core/ProjectSlug.cs` | The app's **only** reader and writer of the `projects/<slug>` encoding (T105): `Encode` (also what the fixture names its dirs with), `RootFor`/`NameFor`/`ShortNameFor` — exact, by walking a recorded `cwd` up to the ancestor that encodes to the slug — `TryProbe`, the filesystem guess for when no cwd exists, and `Literal`/`Tail` for reporting an unresolvable one. Also the **only** place that decides how a directory is *named on screen* (T154): `ShortName` = its last two segments (`turing/2026.3`), which both the Statistics legend and the Context project list go through, since the leaf alone labels three checkouts of a release folder identically. |
| `src/Core/SafeWalk.cs` | The recursive `~/.claude` walk every scan goes through: per directory, so an unreadable one (untrusted junction, denied ACL, folder deleted mid-sweep) skips its subtree instead of aborting the sweep. Materializes each directory's entries — a `try` around a lazy `Enumerate*` catches nothing — and resolves a reparse point to its target before opening it. |
## `src/Ui/` — the window and its pages
The rules this folder carries — two windows and everything else a page, `TryFindResource` in a
constructor, one class in several `partial` files per surface — are in AGENTS.md, because each of them
is a defect somebody already shipped. What is here is which file is which.
| File | Responsibility |
|---|---|
| `src/Ui/MainWindow.xaml(.cs)` | The shell (T158): a nav strip over three destinations, each built on its first visit and then kept collapsed, so a scan / a chart's history / a half-edited settings page survives a switch. Owns the chrome; `Statistics` is the one page the tray reaches into (a fresh reading per poll). |
| `src/Ui/SettingsPage.xaml(.cs)` | The WPF Fluent settings page, with its own six-page sidebar. **All layout lives in the XAML.** Save applies through the callback and confirms in place; Cancel raises `Cancelled` and the shell rebuilds the page from the live model — discarding by construction rather than control by control. |
| `src/Ui/ContextPage.xaml(.cs)` | The Context Load page: master/detail over `ContextScanner` — projects left; right, the session-zero gauge (base overhead / instructions / memory / skills, with the transcript-measured tick) over the per-source eager/lazy breakdown. Scans on a background thread; view models are `public` because WPF binding resolves paths by reflection over public types only. |
| `src/Ui/StatisticsPage.xaml(.cs)` | The Statistics page and its tabs, one `partial` file per surface: `.{Throughput,Chart,Profiles,Format,Note}.cs`. |
| `src/Ui/ToastWindow.xaml(.cs)` | The second window: the reset/context cards, deterministic under `--simulate-reset` and `--capture-toast`. |
| `src/Ui/PageWindow.cs` | The code-only host that shows a single page for the previews and captures, which are about the page rather than the shell. `--main` opens the real shell. |
| `src/Ui/Brand.cs` | The colours whose value is not a free choice, declared once (T310) — today that is **clay**, which means *past the included quota* on the icon, the chart, the toast and the markup. Exposes the bytes plus one conversion per edge (GDI+ `Color`, WPF `Brush`, the band's alpha, the palette's hex string), because a WinForms icon and a WPF chart can share a value and never a brush. `--selftest` fails a second spelling anywhere under `src/`. |
| `src/Ui/SettingsRow.cs` | The Fluent `SettingsCard` row every settings page is built from: leading title and optional wrapping description, trailing control right-aligned. A lookless control whose visual tree is the implicit style in `SettingsPage.xaml`, and whose label is a *neighbour* of the control rather than its content — which is what the automation tree has to be told about (T175). |
| `src/Ui/SessionListRow.cs` | One row of the Statistics window's **Sessions** pane: project, clock, duration, turns, tokens — and a hover carrying the session id (what `--resume` takes), the models and the agent-transcript count. `public` for the same binding reason as `ProjectRow`. **The column list is the design**: a list of conversations is exactly where a person wants a subject line, which makes this the surface that would erode §I.1, so there is no title, no prompt and no summary — project plus clock is what recognises the morning being looked for. |
| `src/Ui/LinkPlanRow.cs` | One row of the linking plan on the Claude Code settings page (T370): the entry's name, its verdict as one word, and what that means — the count where there is one. `public` for the binding reason `ProjectRow` carries. **Localized where `ProfileLink` is not**, and the split has a consequence: `Detail` never renders the catalogue's `Unit` noun, because "union by session uuid" would be half-translated in four languages — a count needs no noun. It renders **null and zero differently** (`history.jsonl` cannot be counted without opening a file full of prompts, §I.1) and keeps the two refusals and the two absences as dimmed rows rather than dropping them, since an entry missing from the list reads as one nobody had an opinion about. |
| `src/Ui/ProjectRow.cs` | One row of the Context page's project list. `public`, like the view models beside it, because WPF resolves a `{Binding}` path by reflection over public types only — an internal one binds to nothing, silently. |
| `src/Ui/SourceRows.cs` | `SourceGroup` and the rows under it: one kind of context source — Instructions, Memory, Skills, Agents — and its files, with the count carried in the header rather than in a string that would need a plural rule. |
| `src/Ui/RowStyle.cs` | How rows inside a group are ordered and drawn: `SourceSort` (the order the sort picker offers) and the per-row style the gauge and the list share. |
| `src/Ui/ContextText.cs` | The display words the Context page's view models share — one `L.T` lookup per `ContextKind`, so a kind is spelled the same wherever it appears. |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!