Extract the duplicated codeburn-binary lookup (PATH building, npx-shim skipping,
default lookup dirs, resolver) from menubar-installer and the Antigravity
statusline installer into src/persistent-codeburn.ts. Both now import it and
keep their own user-facing "install globally" message.
Match the Antigravity statusLine hook on the trailing agy-statusline-hook token
instead of a bare substring, so a custom command that merely mentions the token
is treated as custom (protected, backed up) rather than silently overwritten.
Fix Antigravity hook stale CLI paths: install the statusLine hook through a
persistent codeburn binary resolved from PATH and repair stale CodeBurn-owned
hooks on re-install.
A bare startsWith let an unlisted future minor collapse into the base entry
(gpt-5.6 -> "GPT-5"), the same mis-bucketing class as #420 but for non-Claude
families. Require an exact match or a key-plus-dash boundary in both
getShortModelName and the Codex provider so new versions fall through to their
raw id instead of a wrong name and pricing tier.
The Claude provider kept its own shortNames map with no claude-opus-4-8
entry, so the models table and menubar bucketed it into "Opus 4" — the
actual #420 path (the earlier models.ts map is unused here). Delegating to
getShortModelName removes the duplicate and picks up new versions for free.
New Claude releases no longer need a hand-maintained SHORT_NAMES entry or
FAST_MULTIPLIERS row. Display names are derived from the claude-<family>-<major>-<minor>
id, and the fast-mode multiplier rides along as a 5th element in the LiteLLM
snapshot tuple (provider_specific_entry.fast). Fixes#420: claude-opus-4-8 gets
its own line and correct pricing instead of falling into the Opus 4 bucket.
The menubar kept its own copy of each provider's OAuth grant and refreshed
it on a timer, racing the CLI. Both Claude and Codex use single-use refresh
tokens that rotate on every refresh, so the menubar's self-rotation could
invalidate the user's own CLI login and surfaced as "disconnected" after a
long idle period.
Codex: read ~/.codex/auth.json fresh each cycle; only self-refresh when
last_refresh is older than 8 days; on 401 re-read the source before spending
our token; write rotated tokens back to auth.json (atomic, preserving other
keys); recover from reuse/invalid_grant by re-reading instead of going
terminal.
Claude: never HTTP-refresh the CLI-owned token. On expiry/401 re-read the
keychain with a no-UI query (LAContext.interactionNotAllowed) to adopt a
token the CLI already rotated; when none is available yet, report a transient
sourceTokenStale rather than a terminal disconnect.
A launchd-spawned process gets separate TCC attribution from the LaunchServices
app, so the out-of-process refresher prompted for "access data from other apps"
on every run regardless of the GUI's grant. Remove it entirely: the in-app loop
(timer survives sleep, popover-open recovery) is the source of truth and now
writes menubar-status.json on each successful refresh. On upgrade, any leftover
com.codeburn.refresh LaunchAgent is unloaded and deleted.
Run CodeBurn's own signed binary in --refresh-once mode under the LaunchAgent
so the spawned CLI inherits CodeBurn's TCC grant. The bare-node shell script
prompted "node would like to access data from other apps" because launchd-
spawned children lose the signed app's TCC attribution. Drops the generated
shell script, scriptEnvironment, and per-period regeneration; the plist is now
static and period is read from UserDefaults at refresh time.
refreshStatusButton now uses the fresher of the in-memory payload and
the file written by the LaunchAgent, so the menubar number advances
within ~30s even if the in-app loop is dead.
Migrate off the osascript distributed-notification form (dropped by
napped apps) to a static /bin/sh runner. The plist no longer encodes
period, so it migrates once; period changes rewrite only the script.
Opening the popover now unconditionally restarts a dead timer and
clears stuck loading bookkeeping before force-fetching, so a user
looking at a stuck tab always recovers within one CLI round-trip.
The sleep handler tore down the DispatchSource timer; a missed wake
notification then left it nil forever, stranding the refresh loop.
Cancel only in-flight tasks and let the kernel-paused timer resume.
A quiet refresh torn down across sleep/wake (or a generation reset) can
leave an orphaned inFlightKeys entry for the current key. The stuck-loading
recovery guard bailed whenever any in-flight entry existed, so the popover's
retry loop no-oped forever and the spinner ("Loading Today...") never
cleared — observed on a long-lived instance that crossed the midnight day
rollover.
Clear stale loading/in-flight state via the existing watchdog before the
in-flight guard, so an orphaned entry can no longer trap recovery. A healthy
in-flight fetch (younger than the watchdog) is still respected.
Add subagentTypes to session-cache validateCall for consistency with
other string-array fields (tools, bashCommands, skills).
Stabilize Forge dedup key fallback: use model+tokens instead of array
index so deletions between scans don't cause double-counting.
Add mutual exclusivity validation for --day/--days/--from/--to on the
status command, matching the report command's existing checks.
PR #393 accidentally deleted the RefreshBackoff integration from PR #388,
leaving RefreshBackoff.swift as dead code and tests calling nonexistent
methods. Delete the dead code and fix the broken test target.
Add exponential backoff to the loading watchdog (8s, 16s, 32s... up to
60s, max 6 attempts) so it stops hammering the CLI when it's unavailable.
After exhausting retries, show an error overlay with a Retry button.
Fix recoverFromStuckLoading to skip recovery when a fetch is already
in-flight (avoids killing healthy fetches via generation bump).
Fix selectedDay to return nil for multi-day selections, and pass days
through startInteractiveSelectionRefresh so the cache key matches
currentKey.
KiloCode >= 7.2.21 stores sessions in ~/.local/share/kilo/kilo.db
instead of VSCode globalStorage. Extract a shared SQLite session parser
from the OpenCode pattern and wire KiloCode to discover from both the
legacy globalStorage path and the new SQLite database.
Closes#404
Commit 5800179 (multi-day calendar) accidentally removed stuck-loading
recovery. Re-add an 8-second watchdog loop in MenuBarContent that calls
recoverFromStuckLoading(). Change inFlightKeys from Set to timestamped
Dict so the watchdog can detect orphaned quiet-refresh entries. Stop
nuking payloadRefreshGeneration in the watchdog (it was killing healthy
in-flight fetches). Bump Package.swift platform to macOS 15 to match
NSAttributedString(attachment:) usage.
In commit 5800179, the Commander configuration for `antigravity-hook` and `agy-statusline-hook` commands in `src/main.ts` was accidentally deleted during conflict resolution. This commit restores them along with the missing imports.
* feat(claude): group Cowork sessions by space name instead of sanitized path
Claude Desktop local-agent-mode (Cowork) sessions were appearing as
separate projects with cryptic sanitized-path names like
`-Users-carlo-Library-Application-Support-Claude-...`. This meant
sessions from the same Cowork space (e.g. PhD_Thesis) showed up as
dozens of unrelated entries.
Changes:
- `discoverSessions()` detects paths inside the desktop sessions dir
(`~/Library/Application Support/Claude/local-agent-mode-sessions`)
and resolves the human-readable space name by reading the sibling
`local_<id>.json` (for spaceId) and `spaces.json` (id → name map)
- `CODEBURN_DESKTOP_SESSIONS_DIR` env var overrides the desktop path
for testability
- Sessions without a spaceId fall back to `userSelectedFolders[0]`
basename, then the session title (instead of the unintelligible slug)
- `isCoworkOutputPath()` in parser.ts prevents the ephemeral
`outputs/` cwd from being used as the canonical project key,
which would have given every session a unique grouping key
- Parse-version bump (`cowork-space-grouping-v1`) invalidates stale
cache so all files re-scan with the new logic on first run
- 5 new integration tests covering spaceId grouping, cross-space
separation, folder fallback, title fallback, and no-metadata fallback
* test: use generic Project1/Project2/SubFolder names in cowork tests
* fix(parser): detect container-mode Cowork sessions by file path, not just cwd
Cowork sessions run in two modes:
- Local-mode: cwd is an ephemeral per-session outputs/ dir inside the
desktop sessions directory — already handled.
- Container-mode: session runs inside Docker, so cwd is a container-local
path like /sessions/<adjective-name> that does not exist on the host.
The JSONL file still lives inside the desktop sessions directory, so we
detect these by also checking the file path.
Rename isCoworkOutputPath → isCoworkSession and check both cwd and filePath.
Without this, container-mode sessions leaked their Docker cwd as the
canonical project path and showed as e.g. "sessions/trusting-inspiring-ri"
instead of grouping under the Cowork space name.
Also add a test covering the container-mode case and use generic names
(Project1, Project2, SubFolder) throughout the Cowork test suite.
* feat(parser): merge same-project sessions across AI providers by path
When a repository has sessions from multiple tools (e.g. both Claude Code
and Codex), parseAllSessions now groups them into a single ProjectSummary
instead of showing duplicate entries.
The previous outer merge keyed on p.project (a string name), so Claude's
"LibricicloAgain" and Codex's "Users-carlo-Desktop-LibricicloAgain" were
treated as different projects despite referring to the same directory.
The root cause is that Codex's sanitizeProject() strips the leading '/'
from cwds, producing "Users/carlo/Desktop/foo" instead of
"/Users/carlo/Desktop/foo". The new crossProviderKey normalises both by
stripping leading slashes and lowercasing before comparison. Non-path
identifiers (like Cowork space names: "PhD_Thesis") have no '/' and fall
back to comparing the project name, so they are unaffected.
* feat(parser): resolve other-provider worktrees before cross-provider merge
Codex (and similar tools) store worktrees under paths like
~/.codex/worktrees/<hash>/Repo. These were not being resolved to their
main-repo path because canonicalizeProviderCallProject only operates on
call.projectPath, which Codex doesn't set — the project is inferred from
the session source slug instead.
Fix: before the cross-provider merge in parseAllSessions, run worktree
resolution on each non-Claude ProjectSummary's projectPath. Restore the
missing leading '/' (Codex strips it via sanitizeProject) before calling
resolveCanonicalProjectPath so the stat() can find the .git file. If the
path is a worktree, replace project name and path with the canonical
main-repo values so the downstream crossProviderKey matches.
* feat(dashboard): show more projects in By Project panel to fill panel height
The panel was hardcoded to 8 projects regardless of the Daily Activity
panel height alongside it. Pass the same `days` value used by Daily Activity
so the two panels stay in sync: 14 for week/6-months views, 31 for
month/30-days, 20 for all-time, and a minimum of 8 for the today view.
* feat(parser): resolve subdirectory sessions to their git repo root
When a cwd like ~/Repo/src/subdir is recorded, it should group with the
rest of the ~/Repo project. Previously resolveCanonicalProjectPath only
checked for a .git entry at the exact cwd, so subdirectory sessions
created separate ProjectSummary entries.
Walk up the directory tree until a .git entry is found. Guard against
foreign paths (a Windows path like C:\… on macOS cannot be walked on
the local filesystem) by only walking paths that look like absolute
paths on the current platform.
This also folds non-Claude provider subdirectory sessions into the parent
repo via resolveOtherProjectWorktrees, which now triggers whenever
canonical.path differs from the input path (not only for git worktrees).
* Address reviewer feedback: deduplicate getDesktopSessionsDir and fix test
Export getDesktopSessionsDir from claude.ts and import it in parser.ts
instead of maintaining an identical copy in both files.
Update the 'merges by sanitized slug' test to reflect the new path-aware
merge behavior: sessions with the same slug but different cwds now stay
separate (crossProviderKey keys by path, not slug), which is correct since
they represent different repos.
Adds "Icon Only" option to the Metric picker in Settings. When
selected, the menubar shows only the flame icon without any cost
or token text. Flame tinting for quota/budget alerts still works.
Broaden message/part parsing to handle OpenCode versions that store
tokens at the session level, use alternative field names, or emit
different part type identifiers. Adds session-level token fallback
when per-message parsing yields nothing.
Closes#392
* Add multi-day calendar selection with custom popover UI
Replace native DatePicker with a custom-themed CalendarPopover supporting
non-contiguous multi-day selection. Add --days CLI flag for comma-separated
date filtering that post-filters from the bounding range scan.
CLI: parseDaysFlag() parses and validates dates, filterProjectsByDays()
filters turns to only selected days. Verified that sum of individual --day
calls matches --days output across cost, calls, activities, models, and
providers.
Swift: DataClient passes --days for multi-day, with single-day fallback.
AppStore tracks selectedDays as Set<String> with PayloadCacheKey support.
CalendarPopover uses pending state committed only on Done tap.
* Clear + Done in calendar resets to current period
When user clears all selected days and taps Done, switch back to the
active period instead of silently keeping the old day selection.
The menubar status item can now track Today, Week, Month, or 6 Months
independently from the popover's active period. Setting is persisted
in UserDefaults and changeable from Settings or via `defaults write`.
Thanks @ozymandiashh. Closes#291.
Adds extensionless session index and nested execution file discovery
while preserving legacy .chat support. Modern execution JSON is parsed
for identifiers, timestamps, model IDs, conversation text, structured
tools, and token estimates. Also fixes dedup key poisoning on invalid
timestamps in the legacy parser.