The time you sent each message, drawn above it

Claude Code mods, packaged as a plugin marketplace. A mod is a plugin whose hooks module (TypeScript, here) runs inside the session. Mods need Claude Code 2.1.287 or later and are on by default.
| Plugin | What it does |
|---|---|
| time | The time you sent each message, drawn above it |
| gh-ci-status | GitHub Actions runs of the session's repo, pinned above the prompt, with links to the PR and the run |
| activity-log | Every event of the session, tool calls and their full results included, appended to a JSONL file per session |
| agent-flow | A live tree of the session's subagents and teammates in a pane: status, current tool, time, calls and tokens |
The time above a message is the one of its first drawing, which is when you sent it, and it is kept per message, so a resize or a redraw does not move it. A resumed session, or a reload of the plugin, draws the old messages with the current time.
⚙ owner/repo · Actions · 1 running · 1 finished
#167 ◐ Running PR 0m37s chore(ci): smoke-test PR
main ● Success Deploy 2m15s Release 1.4.0
gh does (gh repo view, the default remote), so gh must be on your PATH and logged in. Without a GitHub remote it stays quiet. The check runs at session start; if it fails there, no network or an expired token, it runs again at the next command that wakes the band (below), so after gh auth login, or once the network is back, a push is enough. Each failure goes to the debug log (claude --debug). While gh fails, the header says so (gh error for 2m05s) and keeps the last rows.gh run list every 60 s, and every 15 s while a run is in flight or for 6 minutes after a push. A git push, git subtree push, gh pr merge, gh workflow run or gh run rerun in Bash wakes it.#N links to the PR (matched by branch through gh pr list, or from refs/pull/N/head); the workflow name (on a narrow band, the status) links to the run.details on the band's header opens a pane with every run of the last list, and under each failed one its failed jobs and steps, each linking to the job: ctrl+x tab to focus the band, then d; d or Esc closes it. A mouse click reaches it only in fullscreen./gh-ci opens or closes the same pane, band or not; before the repo is found it looks again and says so.Writes everything the session does to ~/.claude/activity-log/<date>-<sessionId>.jsonl, one JSON object per line. The date is the local date of the session's first line; after a /clear the lines go to a new file, under the new session id.
seq: a start line with the event's input (and origin, who raised it), written before the event runs, then an end line with its result and durationMs, or an error line. Tool calls, the rows the conversation keeps, the model's requests, prompts, commands, agents and the $ calls of other plugins are all there.agentId on the start line. The model's response and a plugin's spawned process stream; their end line lists the chunks, which pass through unchanged as they come.ui.render and ui.resolve, which fire for each component on every redraw; prompt.edit, which fires on every keystroke with the draft so far (prompt.submit keeps the text sent); and this plugin's own calls. Telemetry is only the collector stream of telemetry.log; the anthropic stream, and telemetry.mark with it, is closed to installed plugins.tool.describe, command.describe and the $ calls of other plugins.seq starts again at 0 in the same file, and the last second of lines may be lost.Reading it with jq:
# tool calls, with input and result side by side
jq -s -c 'map(select(.event == "tool.call")) | group_by(.seq)[]
| {tool: .[0].input.tool, agentId: .[0].agentId, input: .[0].input, result: .[1].result}' FILE
# one subagent's lines (its ids: jq -r '.agentId // empty' FILE | sort -u)
jq -s -c --arg id AGENT_ID 'map(select(.agentId == $id) | .seq) as $seqs
| .[] | select(.seq | IN($seqs[]))' FILE
# how many of each event
jq -r 'select(.phase == "start") | .event' FILE | sort | uniq -c | sort -rn
2 running · 1 done · 1 waiting
● main · running 1m24s · Agent 1m23s
● general-purpose: find the auth flow · running 1m23s · Grep 4s · 7 calls
✓ Explore: read the session store · completed 36s · 12 calls · 20.4k tok
● general-purpose: run the test suite · running 1m22s · waiting for approval: Bash · 3 calls
/flow opens a pane with the main loop and every subagent and teammate under the loop that spawned it; /flow again closes it. The pane does not take the keyboard. Terminal only.Agent call, which lasts as long as its child, does not count), or while a running agent has had no event for 2 minutes. Ended rows are dimmed.agent.spawn, tool.call, turn.start, turn.complete, and classic.PermissionRequest when no settings hook decided it), checked against $.agent.list() every 2 s while something runs and the pane is open; the list has the last word on status. An agent the list named once and then left out is marked gone./clear and /resume start the tree empty; a background agent that survives /clear comes back once the list names it (while something runs with the pane open). A reload of the plugin starts it empty too; run /flow to draw the pane again, and running agents come back once the list names them.# mods need Claude Code 2.1.287 or later
claude --plugin-dir /path/to/claude-mods-diegorv/plugins
That folder loads activity-log too, which writes everything the session sees to disk; to leave it out, point --plugin-dir at single plugins instead (--plugin-dir plugins/time).
Under --plugin-dir, edits to a plugin's files reload it without restarting the session.
Or install them from the marketplace on GitHub:
/plugin marketplace add diegorv/claude-mods-diegorv
/plugin install activity-log@claude-mods-diegorv
/plugin install agent-flow@claude-mods-diegorv
/plugin install gh-ci-status@claude-mods-diegorv
/plugin install time@claude-mods-diegorv
Or from a local clone, by the path of its root (the folder with .claude-plugin/marketplace.json, not plugins/):
/plugin marketplace add /path/to/claude-mods-diegorv
/plugin install activity-log@claude-mods-diegorv
/plugin install agent-flow@claude-mods-diegorv
/plugin install gh-ci-status@claude-mods-diegorv
/plugin install time@claude-mods-diegorv
A marketplace added from a local path loads its plugins in place, so edits need no version bump or reinstall; they take effect at the next session start or /reload-plugins, not on save as under --plugin-dir. Both marketplaces are named claude-mods-diegorv, and only one marketplace per name can be registered, so remove one before adding the other.
The marketplace was renamed, and the install ids with it. If you installed under the old name, run /plugin marketplace remove claude-function-hooks, which also uninstalls its plugins, then one of the two marketplace blocks above.
Tested with Claude Code 2.1.289; the API may still change between releases. /plugin names the mods the session loaded, in a line such as 1 mod active · time. The old CLAUDE_CODE_ENABLE_FUNCTION_HOOKS flag is ignored; remove it.
To turn one off, disable its plugin in the Installed tab of /plugin. To turn off every installed mod, start the session with --safe-mode, which also disables your other customizations, or, for every session, set "disableAllHooks": true in ~/.claude/settings.json, which also stops your settings hooks and custom status line.
npm test # node --test, no dependencies
npm run typecheck # tsc
npm run format # prettier
npm run validate # claude plugin validate
claude plugin test runs every *.test.ts in a plugin with the claude-code/testing kit; these use node:test, so it is not used here.
The API's type declarations are not in git. Claude Code writes them to plugins/<name>/.claude-plugin/types/ each time it loads the plugins with --plugin-dir (a one-prompt headless run, claude -p --plugin-dir plugins "ok", will do). Before npm run typecheck, load the plugins once, and again after updating Claude Code.
plugins/gh-ci-status/
.claude-plugin/plugin.json manifest
hooks/hooks.json points at the entry module
src/core/ the run model and the text derived from it, no I/O
src/utils/ generic helpers (text)
src/app/ use cases (the poller, the start-up), dependencies injected
src/infra/ external clients (GitHub through gh)
src/components/ the band's view model and its JSX
src/hooks/ the wiring to the engine
*.test.ts next to the file it tests, run with node:testsrc/hooks/register.tsx 20 lines1// Wires the piece to the engine: one hook.
2//
3// ui.render on UserMessage draws the time above the message
4//
5// The time is the one of the message's first drawing, which is when it was
6// sent, and it is kept per message, so a resize or a redraw does not move it;
7// a resumed session, or a reload of the plugin, draws old messages with the
8// current time.
9import type { Register } from "claude-code";
10import { timeFor } from "../utils/clock.ts";
11import { Timestamp } from "../components/timestamp.tsx";
12
13export const register: Register = (on) => {
14 const drawnAt = new Map<string, string>(); // message id -> the time of its first drawing
15 on("ui.render", { component: "UserMessage" }, async ($, event, next) => {
16 const time = timeFor(drawnAt, event.requestId, await $.clock.now());
17 return Timestamp($.ui.resolve(event), time, await next(event));
18 });
19};
20src/utils/clock.ts 14 lines1// Wall-clock time as `HH:MM:SS` in the local time zone.
2export function formatTime(epochMs: number): string {
3 const twoDigits = (value: number) => String(value).padStart(2, "0");
4 const date = new Date(epochMs);
5 return [date.getHours(), date.getMinutes(), date.getSeconds()].map(twoDigits).join(":");
6}
7
8// The time a message was first drawn, remembered per message id so a redraw keeps it.
9export function timeFor(drawnAt: Map<string, string>, id: string, nowMs: number): string {
10 const time = drawnAt.get(id) ?? formatTime(nowMs);
11 drawnAt.set(id, time);
12 return time;
13}
14src/components/timestamp.tsx 15 lines1// A time line drawn above another element.
2import type { Elements as EngineElements, RenderChildren } from "claude-code";
3type Elements = Pick<EngineElements["terminal"], "Box" | "Text">;
4
5const COLOR = "#a78bfa";
6
7export function Timestamp({ Box, Text }: Elements, time: string, below: RenderChildren) {
8 return (
9 <Box flexDirection="column">
10 <Text color={COLOR}>{`⏱ ${time}`}</Text>
11 {below}
12 </Box>
13 );
14}
15