Claude Action Timeline
Timestamps for every Claude Code action. A live timeline of what Claude
Code is doing in your workspace — every prompt, tool call, reply and how long
each took — read from the session transcripts Claude Code already writes
locally.

Unofficial. This is an independent, community-built extension. It is not
made by, endorsed by, or affiliated with Anthropic. "Claude" is Anthropic's
trademark and is used here only to describe what the extension reads.
What you get
- Timeline view (activity bar → clock icon): one row per action, each
led by its wall-clock time so when reads straight down the left edge.
Each row's icon carries two things: its shape says what the row is —
your prompt, Claude's reply, a thinking block, a tool call, a compaction —
and its colour says how it went. Prompts are purple and replies green;
thinking and compactions are muted; tool calls, which are most of the view,
stay in the ordinary foreground so the coloured rows can be seen at all.
Red always means this went wrong, never a particular kind: a failed tool
call keeps its tool icon and turns red, so it can be told apart from the
API itself failing.
Tool calls show a one-line summary (the command's
description, the file's name, the search pattern…) and gain a duration the
moment their result lands. Failed tool calls are marked, and a call the
permission layer refused reads
denied with its reason — a permission
rule, a call you declined, or an auto-mode classifier outcome — in a muted
icon rather than the error red, because a rule doing its job is not a
failing tool. A call still waiting for its result reads no result 4m
with an ellipsis icon — the age stays current — and API errors and
conversation compactions get rows of their own instead of masquerading as
replies or vanishing.
- Work you sent to the background stays visible. A command Claude hands
off — a CI watch, a long build, a subagent, a workflow — reads
background · no report 4m while it is out there, and shows what really
happened when the notification lands: the true duration, and failed,
stopped or killed if that is how it ended. It never says "running",
because a background task dies with the session that started it and the
transcript cannot tell that from work still in progress; it says what was
recorded, which is that the work was handed off and nothing has come back.
- The task list, pinned on top. When Claude is working from a plan, the
current list sits above the timeline — progress, the task running now, and
the rest with their states — with the time it was last written beside it.
It is the plan as Claude last wrote it, not a live view, and it says so;
a subagent's own list never replaces the session's. Collapse it and it
stays collapsed.
- Prompt rows are what you typed. Claude Code writes a great deal besides
your typing as user records — notifications from finished background agents,
the output of
! commands, the summary blob after a compaction, the editor
announcing which file is open — and none of it draws a row. Slash commands
do, because running one is something you did: the row reads /model opus
rather than the XML the command expands into. Only shapes the extension
recognizes are filtered, so a future Claude Code change puts noise back in
the timeline rather than making your prompts disappear from it.
- Hover a prompt to read all of it. A row shows the first 80 characters;
hovering shows the prompt in full, up to 1000. (Longer than that, and
Reveal in Transcript has the rest.) There is no jump to this message in
the chat because Claude Code exposes nothing to jump to — showing you what
you asked is the reachable half of that wish.
- Click to open: rows for file tools (
Read, Edit, Write,
NotebookEdit) open the file they acted on; the hover shows the full path.
Rows for everything else stay inert.
- Reveal in Transcript: right-click any row to open the session's
.jsonl
at the line that row came from — the raw record behind the summary.
- Status bar: the time and name of Claude's latest action, always in
view. Hovering it shows the session being followed, its state (working,
awaiting your input, or quiet), when it started and how long it spans, the
last action with its duration, what the view holds, and the busiest tool.
It names no file on disk — the hover is on screen whenever the extension
is, so it stays fit for a screenshot or a screen share; Reveal in
Transcript and Copy Freeze Report are where a path is disclosed,
because you asked for it there. The hover ends with a Send feedback link
(also in the palette as Claude Timeline: Send Feedback): rate the
extension on the store your editor installs from — the VS Marketplace in
official VS Code, Open VSX in VSCodium, Cursor or Windsurf — or report a
problem or idea on GitHub issues. Click the status bar item to open the
timeline.
- Frozen-session triage: the session's state is assessed from the
transcript — the one signal that keeps answering when the chat UI hangs.
When a turn is open but nothing has been written for a while (3 minutes;
10 for shells, agents and workflows, which legitimately run long), the
status bar turns warning-coloured with the measured span (
quiet 4m) and
the hover names the unanswered call and the recovery path. Facts, not
verdicts: an idle session reads as awaiting your input, never as a
flag, because the transcript cannot tell a hung tool from a slow one —
and this extension does not pretend it can. Large single-turn writes
(16k+ characters) note the known VS Code extension hang they can trigger
in the row's tooltip, with the workaround.
- Sleep/resume freeze detection: when the machine wakes from sleep with a
session that was mid-turn and whose transcript has stopped moving — the
signature of the known Claude Code freeze-after-sleep issues — a warning
offers one-click recovery: copy the exact
claude --resume <session-id>
command, or open a fresh terminal with it pre-typed. See
Frozen session detection and recovery.
- Copy Freeze Report: one command puts a paste-ready diagnostic on the
clipboard — state, unanswered calls with ages, recent API errors,
compactions, transcript stat, time since the last transcript write, the
resume details when the machine slept, recovery steps. Prompt and reply
text is counted, never quoted, so the report is safe for a public issue
after a read-through.
- Live: the view follows the newest session for the workspace and updates
as the transcript grows (directory watch + a 2-second polling backstop).
- Session picker: pin an older session or go back to following the
latest. Each entry says how long ago it was written to and which one is on
screen, so with two chat windows live you can tell them apart.
- Copy as text: the whole visible timeline as timestamped plain text.
- Log (Claude Timeline: Show Log): which directory was searched, which
session is being followed and why it changed, and a lines-in/events-out
tally. An empty timeline has several causes and this is how you tell them
apart. Raise the detail with Developer: Set Log Level.
Rows read like this — a text illustration, not a screenshot; the right-hand
part is VS Code's dimmed item description, and the columns are padded here
only for legibility:
14:04:12 The other session was mid-turn, so it stayed put
14:04:05 Wait for PR [#6](https://github.com/shivakrishnakokkula/claude-action-timeline/issues/6) CI checks Bash · background · no report 4m
14:04:01 Validate main and wait for its CI Bash · background · 2m 5s
14:03:59 Publish the package Bash · 0.3s · denied (permission rule)
14:03:58 Run the unit tests Bash · 6.4s · failed
14:03:57 FOLLOW_IDLE_QUIET_MS Grep · 0.2s
14:03:56 follow.ts Read · 0.1s
14:02:44 /model opus
14:02:41 why does the timeline keep flipping?
Each row opens with its wall-clock time, so when reads straight down the
left edge. After it, whatever the action carried: a Bash call's description, a
file tool's file name, a search pattern, or — for a prompt, reply or thinking
block — its opening 80 characters, all whitespace collapsed to single spaces
and an ellipsis at the cut. A multi-line prompt therefore reads as one
flattened line, not as its first line; hover it and the whole prompt is
there. On the right, the tool, the duration
once its result lands, and the outcome when there is one to report — failed
when the result came back an error, denied with its reason when the call was
refused before it ran, or stopped and killed for background work that
ended those ways. A row with none of those — a prompt, with durations off —
simply has nothing on the right. Newest is at the top by default.
How it works
Claude Code appends one JSON line per event to
~/.claude/projects/<workspace-slug>/<session-id>.jsonl. This extension only
reads those files — no network, no processes, nothing written to disk. The
transcript format is internal to Claude Code and changes between versions, so
the parser is deliberately defensive: lines it does not recognize are skipped
silently rather than breaking the view.
Durations come from pairing each tool_use id with its tool_result line, so
they measure the real gap between the call and its result.
Work sent to the background is the exception, and it is why background rows
exist at all. Claude Code answers a backgrounded call immediately — the reply
says where the work went, not what it did — so pairing on that reply timed
every such job at a second or two and called it a success, including the ones
that went on to fail. Those calls pair a second time instead, by task id, with
the notification that ends the task, and the row shows what that reports.
Frozen session detection and recovery
Claude Code sessions can freeze when the machine sleeps or hibernates and
resumes (claude-code#53846,
claude-code#77880):
after wake the CLI stops responding, or the backend dies while the panel
stays open. Nothing is lost — the conversation is on disk — but the session
has to be resumed, and first you have to notice which one froze.
This extension notices from the transcripts alone. It stays read-only and
offline: no process inspection, no network, no OS power APIs. The poll that
already runs every 2 seconds doubles as a heartbeat — a long gap between
ticks means the machine (or the extension host) was suspended — and a few
minutes after the resume, every session in the workspace's store is
classified:
- Mid-turn at sleep, transcript unmoved since resume → flagged as a
frozen candidate. The timeline pins a warning row, the status bar turns
warning-coloured with Claude session may be frozen, and one notification
offers the recovery actions below.
- Mid-turn, but the transcript advanced → the session survived; nothing
is said.
- Idle at a turn boundary → never warned. Silence while waiting for your
input is normal, however long it lasts.
- Waiting on a shell, agent, workflow, or a backgrounded task → exempt.
A ten-minute build that straddles the wake is not a freeze, and the
ordinary quiet-time flag still covers it if it truly stalls. The exemption
holds however long the transcript is: a session that would otherwise be
flagged is checked against its whole file for still-running background work
before any warning appears, and if that check cannot answer, none appears.
- Last written well before the machine slept → stale, and never flagged.
A session you finished with days ago was not running to be frozen, even
though its last turn may never have been closed — which is what happens
every time Claude Code is interrupted or closed on a prompt. Sessions
outside
staleAfterMinutes (default 60) of the sleep are skipped without
their transcripts being read at all.
The notification's buttons: Copy resume command puts the exact
claude --resume <session-id> on the clipboard; Open recovery terminal
opens a new integrated terminal with that command pre-typed but not run —
you read it and press Enter; Ignore this session silences warnings for
that session for good, and the choice survives reloading the window. The
extension never types into, or interrupts, any existing terminal.
If one wake strands more than one session you get a single notification
saying how many, with Copy resume commands (one command per line, ready
to paste) and Ignore these sessions — never one popup per session.
Recovery is the same on Windows, macOS and Linux: press Ctrl+C — on macOS
that is Control+C (⌃C), not Cmd+C — in the session's terminal, then run
claude --resume and pick the session; the conversation is preserved. A
session running in the Claude Code panel instead needs the panel closed and
reopened, then resumed (that variant is
#77880).
To stop the freeze happening at all, keep the machine from sleeping during
long runs: on Windows powercfg /change standby-timeout-ac 0 (elevated
terminal), or Settings → System → Power → sleep Never when plugged in;
on macOS caffeinate -i for the duration, or Battery settings → Prevent
automatic sleeping; on Linux systemd-inhibit, or the desktop's power
settings.
Claude Timeline: Simulate Resume (debug) fires the resume event by hand
for testing in the Extension Development Host, and every surface says so
rather than reporting a sleep that never happened: the notification and the
status bar's hover open with Simulated resume (debug), no sleep was
measured, the timeline row reads simulated resume, and the freeze report
says (simulated via the debug command). The detection is governed by the
four claudeActionTimeline.frozenDetection.* settings below.
Settings
| Setting |
Default |
Meaning |
claudeActionTimeline.claudeDir |
"" |
Directory holding projects/; empty means ~/.claude |
claudeActionTimeline.showThinking |
false |
Show thinking blocks as entries |
claudeActionTimeline.showSubagents |
false |
Show subagent (sidechain) activity |
claudeActionTimeline.showDurations |
true |
Per-tool durations — in the row, its tooltip and Copy as Text |
claudeActionTimeline.newestFirst |
true |
Newest action at the top |
claudeActionTimeline.maxEvents |
500 |
Entries kept in the view (50–5000) |
claudeActionTimeline.timeFormat |
time |
time = HH:MM:SS, timeWithDate = MMM D HH:MM:SS |
claudeActionTimeline.frozenDetection.enabled |
true |
Warn when a session looks frozen after a sleep/resume, with recovery actions |
claudeActionTimeline.frozenDetection.resumeSkewSeconds |
60 |
Tick gap read as the machine having slept (10–3600) |
claudeActionTimeline.frozenDetection.graceSeconds |
180 |
Wait after a resume before classifying sessions (0–3600) |
claudeActionTimeline.frozenDetection.staleAfterMinutes |
60 |
A transcript last written longer than this before the sleep was not in use, and is never flagged (1–10080) |
timeFormat governs the time leading each row, the status bar and Copy as
Text. A
row's tooltip is the detail view and always shows the full local date and
time, whichever format is set.
Limitations
- Multi-root workspaces: the first folder is used (re-resolved when folders
change). A window with no folder open has no workspace path to resolve, so
the view stays empty.
- Transcripts are found by the absolute path Claude Code ran in, which is what
names the directory under
projects/. A drive letter in the other case is
handled; reaching the same project by a different path (a symlink, a
subfolder, a UNC share) is not — that path has its own directory, or none.
- Remote development: the extension prefers the workspace side
(
extensionKind: ["workspace", "ui"]), so in SSH/WSL/container windows it
reads the remote ~/.claude — the machine where Claude Code actually runs.
- When two sessions write concurrently (two chat windows, or an interactive
one plus a background agent), follow-latest hands the view over once the
session it is showing has fallen silent — after 3 seconds if that session
is waiting on you, or after the same few minutes the status bar waits
before calling a session quiet if Claude is still working in it, so a long
shell command or subagent does not give the view away. Two sessions
genuinely working at once is ambiguous, and the view holds still rather
than flip-flopping; the session picker is the override, and it says which
session is on screen and how long ago each was written to.
- Long sessions are trimmed, not paged: the view shows the last
maxEvents
entries and at most 5000 are held in memory, so the earliest actions of a
very long session eventually drop out of the view.
- The transcript format is Claude Code's internal format. When a Claude Code
update changes it, unrecognized lines simply do not appear until this
extension catches up. The log warns when a batch of lines yields no events,
which is what that looks like from in here.
CLAUDE_CONFIG_DIR is honoured when claudeActionTimeline.claudeDir is
empty, so a relocated ~/.claude is usually found with no configuration.
Treat it as best-effort: it is undocumented upstream, so the setting remains
the supported way to point this extension somewhere else. The log records
which of the two won.
Development
npm install
npm run lint # ESLint
npm test # tsc + node:test over src/test
npm run package # the installable .vsix, via @vscode/vsce
Press F5 in VS Code to launch an Extension Development Host.
TypeScript is pinned to ~6.0.3: typescript-eslint throws on TS 7.0 rather
than warning, so the compiler and the lint step have to move together (see
typescript-eslint#10940). Raise both once TS >= 7.1 is supported.
engines.node (>=20.19.0) is the floor for linting and testing, inherited
from ESLint 10 — deliberately higher than the oldest runtime the extension
itself supports, which is VS Code 1.90's Node 20.9 host (see below).
npm run package needs Node 22 or newer — @vscode/vsce pulls in @azure/*
packages and @typespec/ts-http-runtime that require it — so npm ci on
Node 20 logs EBADENGINE warnings for a tool that only the packaging step
uses.
CI (GitHub Actions) runs lint and tests on a matrix of ubuntu-latest and
windows-latest against Node 20, 22 and 24; a second job then runs the
packaging step on Linux and uploads the resulting .vsix as a build artifact.
Node 20 is in the matrix deliberately: engines.vscode admits VS Code 1.90,
whose extension host runs Node 20.9, so the 20.x line is where the oldest
supported host lives. The @types/node pin to ^20.19 keeps the compiler on
that line — it rejects APIs from Node 21 and later, though not the handful
added between 20.9 and 20.19 — and the matrix's Node 20 leg proves the code
runs there.
The requirements (each paired with the test or CI step that holds it), the
design rationale, and the security posture — including the ledger of accepted
warnings — live in docs/.
Releasing
Pushing a v* tag builds the .vsix and attaches it to a draft GitHub
Release. The workflow refuses tags whose commit is not on main, and tags
that disagree with package.json about the version — so a release can only
come from reviewed history, and its artifact can never contradict its own
label.
git tag -a vX.Y.Z -m "X.Y.Z"
git push origin vX.Y.Z
Then review the draft and publish it. Storefront publishing stays manual:
publish tokens are long-lived credentials with write access to the publisher
account, which is not something to keep in CI for a project this size. From a
Node 22+ shell, with the .vsix downloaded from the release, the same tested
artifact goes to both storefronts:
npx @vscode/vsce publish --packagePath claude-action-timeline-X.Y.Z.vsix
npx ovsx publish claude-action-timeline-X.Y.Z.vsix -p <open-vsx-token>
(vsce reads its Azure DevOps token from VSCE_PAT or an earlier
vsce login; Open VSX serves VSCodium, Cursor and Windsurf users.)
License
MIT