SLOPSHOPPER

time

The time you sent each message, drawn above it

newrows
v0.2.0no licenseupdated 2026-10-05diegorv/claude-mods-diegorv/plugins/time
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · time
⏱ 01:53:20 ⟨Claude Code's own drawing⟩ ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Your message
⏱ 01:53:20 ⟨Claude Code's own drawing⟩
README

claude-mods-diegorv

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.

PluginWhat it does
timeThe time you sent each message, drawn above it
gh-ci-statusGitHub Actions runs of the session's repo, pinned above the prompt, with links to the PR and the run
activity-logEvery event of the session, tool calls and their full results included, appended to a JSONL file per session
agent-flowA live tree of the session's subagents and teammates in a pane: status, current tool, time, calls and tokens

time

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.

gh-ci-status

⚙ owner/repo · Actions · 1 running · 1 finished
#167  ◐ Running    PR       0m37s  chore(ci): smoke-test PR
main  ● Success    Deploy   2m15s  Release 1.4.0
  • Finds the repo the way 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.
  • Polls 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.
  • Shows runs from a push, a pull request, a manual dispatch, the merge queue, a release, or a workflow another one started, and leaves cron and issue bots out. Toasts when a run starts and when runs finish: one per run that failed or needs you, one for those that passed; a cancelled or skipped run shows only on the band. A finished run stays for 5 minutes; one that failed, timed out or needs you stays until a newer run of the same workflow, branch and trigger shows up, for 30 minutes at most.
  • 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.

activity-log

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.

  • Each event is two lines with the same 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.
  • The content is whole, nothing cut: a tool's full input and result, every message. An event that runs in a subagent has its 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.
  • The files hold everything the session saw, secrets included. The folder is made 700 and each file 600, but nothing is redacted: delete what you do not want kept.
  • Left out: 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.
  • It is large: about 2 MB for a one-line prompt, mostly tool.describe, command.describe and the $ calls of other plugins.
  • Lines are written about once a second and at session end. After a hot reload of the plugin, 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

agent-flow

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.
  • A row: status glyph, type and name (or the task's description), status and elapsed time, the oldest tool call in flight and how long it has run, the calls ended, and once the agent ended its tokens. The header counts the agents running, those done, and the loops waiting for approval.
  • A row stands out (bold, colored) while its loop waits for approval, while a tool call has run over 30 s (an 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.
  • The tree comes from the engine's events (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.
  • Tokens are uncached input plus output, summed over the agent's turns; cache reads and writes are left out.
  • "waiting for approval" stays until the tool call ends, approved or not: no event carries the person's answer. In auto or dontAsk mode, or for a background subagent, it can show briefly before an automatic deny; it clears when the call ends.
  • The engine's own forks (compaction, memory) are not shown, and events from loops no spawn or list named (a workflow's agents) are ignored.
  • /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.
  • Based on the idea of claude-agent-flow by Charlie0113-T (Apache-2.0); no code is copied.

Use

# 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.

Develop

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.

Plugin layout

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:test
Source 3 files
src/hooks/register.tsx 20 lines
1// 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};
20
src/utils/clock.ts 14 lines
1// 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}
14
src/components/timestamp.tsx 15 lines
1// 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