JDWorkdog/ai-workbench/tree/main/mods/turn-timestamp
turn-timestamp
一个小巧的 Claude Code 插件,会在每个已完成的回复下方显示本地日期和时间,让你一眼看出刚点开的标签页是五分钟前还是五天前。
关于这个 mod
turn-timestamp:每个 Claude Code 答案下方的日期和时间
一个小巧的 Claude Code 插件,会在每个已完成的回复下方显示本地日期和时间,让你一眼看出刚点开的标签页是五分钟前还是五天前。
Completed Fri, Oct 2, 2026, 5:41:07 AM EDT
它可在终端、VS Code 扩展和桌面应用的 Code 标签页中运行。它由四个小文件组成,TypeScript 不到一百行;作者提出要求后,Claude Code 在一轮对话中写出了第一版。
问题
如果你在多个 VS Code 标签页中运行 Claude Code,或同时开着几个终端工作区,所有转录看起来都一样。界面不会告诉你某个回复是什么时候到达的。你点进一个标签页,读完最后一条消息,却不知道看到的是今天的工作,还是上周的内容。
这个插件做什么
Claude Code 现在支持 mods:以函数钩子编写、在工作阶段内部运行的插件,可以绘制面板、状态行和 toast,也可以响应事件。其中一个事件是 turn.complete,每次回复完成时都会触发。这个事件上的钩子可以返回一行短文本,引擎会把它显示在答案下方。
这个插件挂接 turn.complete,读取时钟并返回时间戳。并不是每个界面都会绘制这段返回文本:终端和 VS Code 会,桌面应用的 Code 标签页不会。因此,插件还使用终端和桌面 Code 标签页都支持的 AbovePrompt 渲染组件,直接在提示输入框上方绘制一条带状区域,显示最新时间戳。两个通道配合后,每个界面都会显示可见的时间戳,不会被折叠或隐藏。
- 只有主答案会显示时间戳。子代理的工作阶段在主工作阶段内部结束,会被跳过,所以一次回复不会出现一串时间戳。
- 标签会随工作阶段的结束原因变化:
Completed、Interrupted、Ended with error或Refused。 - 可以从
/config修改两个设置,不必编辑代码:display(auto、answer、band、status或log)和timeZone。 - 转录本身不会被改写。时间戳只用于显示。
安装
你需要 Claude Code 2.1.286 或更高版本。mods API 由 Anthropic 标为 early access,后续可能变更。
最快方式:一个文件,不用 clone
INSTALL.md 是一个自包含的安装程序。附录里带有三个插件文件,并为你的助手提供逐步说明:检查版本、盘点现状、写入、验证,以及(如果你想让它在所有地方生效)编辑一次带备份的用户级设置。随便打开 Claude Code,粘贴:
Fetch https://raw.githubusercontent.com/JDWorkdog/ai-workbench/main/mods/turn-timestamp/INSTALL.md and follow its section "Agent setup instructions" exactly. Begin by asking me whether I want the mod on for every project or for the current project only.
或者把文件保存到本地,然后粘贴 完整读取 INSTALL.md 文件,再严格按照其中的“Agent setup instructions”部分操作。下面的选项是手动执行的等价方式。
选项 1:在一个终端工作阶段中试用
git clone https://github.com/JDWorkdog/ai-workbench.git
claude --plugin-dir ~/path/to/ai-workbench/mods/turn-timestamp
选项 2:在所有地方加载,包括 VS Code 和桌面应用
VS Code 扩展和桌面应用不能接收命令行标志,因此会从用户设置的 env 区块中的 CLAUDE_CODE_PLUGIN_DIRS 变量读取插件文件夹。由于 ~/.claude/settings.json 是用户级设置,在这里改一次就能覆盖机器上的每个项目和每个 VS Code 标签页;如果你的目标是再也不弄丢标签页,这正是需要的设置。添加以下内容(与已有内容合并):
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/path/to/ai-workbench/mods/turn-timestamp"
}
}
在 macOS 和 Linux 上用 : 分隔多个文件夹,在 Windows 上用 ;。重启 Claude Code,之后每个新工作阶段都会带有时间戳。这个变量只会从用户设置或进程环境中读取,不会从项目的 .claude/settings.json 读取,因此没有按项目设置的版本。如果只想在单个项目中显示时间戳,请在启动该项目时使用选项 1。
选项 3:让 Claude 替你构建
打开 Claude Code 并输入:
加载 plugin-authoring 技能,帮我构建一个 mod,使用 turn.complete 钩子在每个完成的答案下方打印本地日期和时间。跳过子代理工作阶段。
Claude 会把插件写入一个支持热重载的开发文件夹,询问一次是否要为本工作阶段启用热重载,随后时间戳就会出现在下一条回复中。这就是这个插件的制作方式。
验证
claude plugin validate mods/turn-timestamp
你应该在输出中看到 hooks: turn.complete, ui.render{component=AbovePrompt}、一行 state writes: turn-timestamp.last,以及 Validation passed。
代码
.claude-plugin/plugin.json
{
"name": "turn-timestamp",
"version": "0.3.0",
"description": "Shows the date and time of every completed answer so you can tell how fresh a tab is.",
"author": {
"name": "John Workman"
},
"types": "./types/index.d.ts",
"userConfig": {
"display": {
"type": "string",
"title": "Where to show the stamp",
"description": "auto draws a line beneath each answer (terminal, VS Code) and a band above the prompt with the latest stamp (terminal, desktop Code tab). answer, band, status and log each pick one channel; status pins a plugin status line, which the desktop folds into a collapsed notice.",
"default": "auto",
"options": ["auto", "answer", "band", "status", "log"]
},
"timeZone": {
"type": "string",
"title": "Time zone",
"description": "An IANA zone such as America/New_York. Empty uses the machine's zone, or TZ when set.",
"default": ""
}
}
}
hooks/hooks.json
{ "modules": ["./register.tsx"] }
types/index.d.ts(时间戳带状区域读取的唯一值的状态契约)
export type TurnStamp = string
declare module 'claude-code' {
interface PluginState {
'turn-timestamp': { last: TurnStamp | null }
}
}
hooks/register.tsx
import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'
import type { TurnStamp } from '../types'
// The latest stamp, kept in session state so the band survives a hot reload.
const last = atom<TurnStamp | null>({ plugin: 'turn-timestamp', key: 'last' } as const, null)
// Formats the wall-clock time as "Fri, Oct 2, 2026, 3:42:07 PM EDT".
// An invalid or empty zone falls back to the sandbox's own zone, then to ISO.
function stamp(ms: number, timeZone: string | undefined): string {
const d = new Date(ms)
const opts: Intl.DateTimeFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short',
}
try {
return new Intl.DateTimeFormat('en-US', timeZone ? { ...opts, timeZone } : opts).format(d)
} catch {
try {
return new Intl.DateTimeFormat('en-US', opts).format(d)
} catch {
return d.toISOString()
}
}
}
export const register: Register = (on, options) => {
const display = String(options.display ?? 'auto')
const pinnedZone = String(options.timeZone ?? '').trim()
const wants = (channel: string) =>
display === channel || (display === 'auto' && (channel === 'answer' || channel === 'band'))
on('turn.complete', async ($, e, next) => {
const result = await next(e)
// Subagent turns end inside the main turn; only stamp the main answer.
if (e.agentId !== undefined) return result
const now = await $.clock.now()
const zone = pinnedZone || (await $.env.get('TZ')) || undefined
const label =
e.reason === 'answer'
? 'Completed'
: e.reason === 'aborted'
? 'Interrupted'
: e.reason === 'error'
? 'Ended with error'
: 'Refused'
const line = `${label} ${stamp(now, zone)}`
// The band above the prompt reads this; writing it redraws the band.
if (wants('band')) await update($, last, () => line)
// A plugin status line under the prompt. The desktop shows it as a
// collapsed "Claude Code notice", so it is off unless chosen.
if (wants('status')) $.ui.status(line)
// A dim transcript row, for hosts that draw none of the above.
if (wants('log')) $.ui.log(line)
// Drawn beneath the answer where the surface supports it (terminal, VS Code).
return wants('answer') ? { ...result, text: line } : result
})
// The band directly above the prompt input. Raised on the terminal and the
// desktop Code tab; VS Code does not raise it and shows the answer line instead.
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
if (!wants('band')) return next(e)
const line = await read($, last)
if (e.props.hasSurvey || line === null) return next(e)
const { Box, Text } = $.ui.resolve(e)
return (
<Box>
<Text dimColor>
{e.props.isWorking ? `Working. Last answer: ${line}` : `Last answer: ${line}`}
</Text>
</Box>
)
})
}
如何阅读这段代码:register 会取得 on 函数,以及清单 userConfig 中的 options,并填入默认值。调用 on('turn.complete', hook) 会添加一个钩子。每个钩子都会收到 $(引擎介面)、e(事件输入)和 next(调用链的其余部分)。调用 next(e) 会让引擎正常完成,并返回事件结果;对这个事件来说,结果是 { text }。返回不同的 text 后,终端和 VS Code 就会把它显示在答案下方。AbovePrompt 上的 ui.render 钩子会绘制带状区域:它从工作阶段状态读取最新时间戳,而第一个钩子中的写入会让它重新绘制。
自定义
- 固定时区。 在
/config中设置timeZone(或在~/.claude/settings.json的pluginConfigs."turn-timestamp".options下设置),值可以是America/New_York这样的 IANA 时区。 - 选择显示位置。 将
display设为answer,只显示答案下方的那一行;设为band,只显示提示输入框上方的带状区域;设为status,显示插件状态行(桌面应用会将其折叠成通知,所以默认值不是它);设为log,显示较暗的转录行。默认的auto会同时绘制答案行和带状区域。 - 缩短时间戳。 从格式选项中移除
weekday和year,即可得到类似Oct 2, 5:41 AM EDT的结果。 - 加入持续时间。 事件中有
e.durationMs。把Math.round(e.durationMs / 1000)秒追加到这一行。 - 加入 token 成本。 当工作阶段产生过 token 时,
e.usage会带有该工作阶段的 token 数量。
工作阶段运行时保存文件会热重载钩子,因此你可以调整代码,并在下一条回复中看到结果。
为什么这不只是时间戳
有意思的不是时间戳,而是实现它所需的代码这么少。mods API 为提示、工具调用、模型步骤、工作阶段边界和 UI 渲染提供带类型的事件,工作阶段内部还内置了热重载循环。凡是你希望 Claude Code 在工作阶段边界自动完成的事情,现在都可以通过一个钩子实现,也可以让 Claude 替你写出来。
保持 INSTALL.md 同步
INSTALL.md 的附录逐字嵌入了三个源文件。如果你修改了 register.tsx、index.d.ts、hooks.json 或 plugin.json,请同步更新附录,否则单文件安装程序发放的插件会比文件夹中的版本旧。
致谢
使用 Claude Code 的 plugin-authoring 技能构建。属于 AI Workbench starter kit.
安装
请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。
claude plugin marketplace add JDWorkdog/ai-workbench claude plugin install turn-timestamp
原文 / README
turn-timestamp: a date and time under every Claude Code answer
A tiny Claude Code mod that prints the local date and time beneath every completed response, so you can tell at a glance whether the tab you just clicked on is from five minutes ago or five days ago.
Completed Fri, Oct 2, 2026, 5:41:07 AM EDT
It works in the terminal, the VS Code extension, and the desktop app's Code tab. It is four small files and under a hundred lines of TypeScript, and Claude Code wrote the first version in one turn when asked.
The problem
If you run Claude Code in several VS Code tabs, or keep a handful of terminal sessions open, the transcripts all look the same. Nothing in the UI tells you when a given response landed. You click into a tab, read the last message, and have no idea if you are looking at today's work or something from last week.
What the mod does
Claude Code now supports mods: plugins written as function hooks that run inside the session and can draw panes, status lines, toasts, or react to events. One of those events is turn.complete, which fires every time a response finishes. A hook on that event can return a short line of text, and the engine shows it beneath the answer.
This mod hooks turn.complete, reads the clock, and returns a stamp. Not every surface draws that returned text: the terminal and VS Code do, the desktop app's Code tab does not. So the mod also draws a band directly above the prompt input with the latest stamp, using the AbovePrompt render component, which the terminal and the desktop Code tab support. Between the two channels every surface shows a visible stamp without anything collapsed or hidden.
- Only the main answer gets a stamp. Subagent turns end inside the main turn and are skipped, so you never see a pile of timestamps from one response.
- The label changes with how the turn ended:
Completed,Interrupted,Ended with error, orRefused. - Two settings, changeable from
/configwithout editing code:display(auto,answer,band,status, orlog) andtimeZone. - The transcript itself is never rewritten. The stamp is display-only.
Install
You need Claude Code 2.1.286 or newer. The mods API is marked early access by Anthropic, so expect it to change.
Fastest: one file, no clone
INSTALL.md is a self-contained installer. It carries the three mod files in an appendix plus step-by-step instructions for your assistant: version check, inventory, write, validate, and (if you want it everywhere) one user-level settings edit with a backup. Open Claude Code anywhere and paste:
Fetch https://raw.githubusercontent.com/JDWorkdog/ai-workbench/main/mods/turn-timestamp/INSTALL.md and follow its section "Agent setup instructions" exactly. Begin by asking me whether I want the mod on for every project or for the current project only.
Or save the file locally and paste Read the file INSTALL.md in full, then follow its section "Agent setup instructions" exactly. The options below are the manual equivalents.
Option 1: try it in one terminal session
git clone https://github.com/JDWorkdog/ai-workbench.git
claude --plugin-dir ~/path/to/ai-workbench/mods/turn-timestamp
Option 2: load it everywhere, including VS Code and the desktop app
The VS Code extension and desktop app cannot take a command-line flag, so they read plugin folders from the CLAUDE_CODE_PLUGIN_DIRS variable in the env block of your user settings. Because ~/.claude/settings.json is user-level, one edit here covers every project on your machine and every VS Code tab, which is what you want if the goal is never losing track of a tab again. Add this (merge with what is already there):
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/path/to/ai-workbench/mods/turn-timestamp"
}
}
Separate several folders with : on macOS and Linux, ; on Windows. Restart Claude Code and every new session will carry the stamp. This variable is only read from user settings or the process environment, never from a project's .claude/settings.json, so there is no per-project version of this option. If you want the stamp in a single project only, use Option 1 when you start that project.
Option 3: have Claude build it for you
Open Claude Code and ask:
Load the plugin-authoring skill and build me a mod that prints the local date and time beneath every completed answer using the turn.complete hook. Skip subagent turns.
Claude writes the mod into a hot-reloading dev folder, asks once whether to enable hot reloading for the session, and the stamp appears on its next reply. That is how this one was made.
Verify it
claude plugin validate mods/turn-timestamp
You should see hooks: turn.complete, ui.render{component=AbovePrompt} in the output, a state writes: turn-timestamp.last line, and Validation passed.
The code
.claude-plugin/plugin.json
{
"name": "turn-timestamp",
"version": "0.3.0",
"description": "Shows the date and time of every completed answer so you can tell how fresh a tab is.",
"author": {
"name": "John Workman"
},
"types": "./types/index.d.ts",
"userConfig": {
"display": {
"type": "string",
"title": "Where to show the stamp",
"description": "auto draws a line beneath each answer (terminal, VS Code) and a band above the prompt with the latest stamp (terminal, desktop Code tab). answer, band, status and log each pick one channel; status pins a plugin status line, which the desktop folds into a collapsed notice.",
"default": "auto",
"options": ["auto", "answer", "band", "status", "log"]
},
"timeZone": {
"type": "string",
"title": "Time zone",
"description": "An IANA zone such as America/New_York. Empty uses the machine's zone, or TZ when set.",
"default": ""
}
}
}
hooks/hooks.json
{ "modules": ["./register.tsx"] }
types/index.d.ts (the state contract for the one value the band reads)
export type TurnStamp = string
declare module 'claude-code' {
interface PluginState {
'turn-timestamp': { last: TurnStamp | null }
}
}
hooks/register.tsx
import { atom, read, update } from 'claude-code'
import type { Register } from 'claude-code'
import type { TurnStamp } from '../types'
// The latest stamp, kept in session state so the band survives a hot reload.
const last = atom<TurnStamp | null>({ plugin: 'turn-timestamp', key: 'last' } as const, null)
// Formats the wall-clock time as "Fri, Oct 2, 2026, 3:42:07 PM EDT".
// An invalid or empty zone falls back to the sandbox's own zone, then to ISO.
function stamp(ms: number, timeZone: string | undefined): string {
const d = new Date(ms)
const opts: Intl.DateTimeFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short',
}
try {
return new Intl.DateTimeFormat('en-US', timeZone ? { ...opts, timeZone } : opts).format(d)
} catch {
try {
return new Intl.DateTimeFormat('en-US', opts).format(d)
} catch {
return d.toISOString()
}
}
}
export const register: Register = (on, options) => {
const display = String(options.display ?? 'auto')
const pinnedZone = String(options.timeZone ?? '').trim()
const wants = (channel: string) =>
display === channel || (display === 'auto' && (channel === 'answer' || channel === 'band'))
on('turn.complete', async ($, e, next) => {
const result = await next(e)
// Subagent turns end inside the main turn; only stamp the main answer.
if (e.agentId !== undefined) return result
const now = await $.clock.now()
const zone = pinnedZone || (await $.env.get('TZ')) || undefined
const label =
e.reason === 'answer'
? 'Completed'
: e.reason === 'aborted'
? 'Interrupted'
: e.reason === 'error'
? 'Ended with error'
: 'Refused'
const line = `${label} ${stamp(now, zone)}`
// The band above the prompt reads this; writing it redraws the band.
if (wants('band')) await update($, last, () => line)
// A plugin status line under the prompt. The desktop shows it as a
// collapsed "Claude Code notice", so it is off unless chosen.
if (wants('status')) $.ui.status(line)
// A dim transcript row, for hosts that draw none of the above.
if (wants('log')) $.ui.log(line)
// Drawn beneath the answer where the surface supports it (terminal, VS Code).
return wants('answer') ? { ...result, text: line } : result
})
// The band directly above the prompt input. Raised on the terminal and the
// desktop Code tab; VS Code does not raise it and shows the answer line instead.
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
if (!wants('band')) return next(e)
const line = await read($, last)
if (e.props.hasSurvey || line === null) return next(e)
const { Box, Text } = $.ui.resolve(e)
return (
<Box>
<Text dimColor>
{e.props.isWorking ? `Working. Last answer: ${line}` : `Last answer: ${line}`}
</Text>
</Box>
)
})
}
How to read it: register gets an on function and the options from the manifest's userConfig, defaults filled in. on('turn.complete', hook) adds a hook. Every hook receives $ (the engine interface), e (the event input), and next (the rest of the chain). Calling next(e) lets the engine finish normally and hands back its result, which for this event is { text }. Returning a different text makes the terminal and VS Code show that text beneath the answer. The ui.render hook on AbovePrompt draws the band: it reads the latest stamp from session state, and the write in the first hook redraws it.
Customize it
- Pin a time zone. Set
timeZonein/config(or underpluginConfigs."turn-timestamp".optionsin~/.claude/settings.json) to an IANA zone such asAmerica/New_York. - Choose where it shows. Set
displaytoanswerfor the beneath-the-answer line only,bandfor the above-prompt band only,statusfor a plugin status line (the desktop folds this into a collapsed notice, which is why it is not in the default), orlogfor a dim transcript row. The defaultautodraws the answer line and the band. - Shorter stamp. Drop
weekdayandyearfrom the format options for something likeOct 2, 5:41 AM EDT. - Add the duration.
e.durationMsis on the event. AppendMath.round(e.durationMs / 1000)seconds to the line. - Add token cost.
e.usagecarries the turn's token counts when the turn had any.
Saving the file while a session is running hot-reloads the hook, so you can tweak and see the result on the next reply.
Why this matters beyond timestamps
The interesting part is not the stamp, it is how little it took. The mods API gives you typed events for prompts, tool calls, model steps, turn boundaries, and UI rendering, with a hot-reload loop built into the session. Anything you have wanted Claude Code to do automatically at a turn boundary is now a hook away, and Claude can write the hook for you.
Keeping INSTALL.md in sync
The appendix in INSTALL.md embeds the three source files verbatim. If you change register.tsx, index.d.ts, hooks.json, or plugin.json, update the appendix to match, or the one-file installer will hand out an older mod than the folder does.
Credits
Built with Claude Code's plugin-authoring skill. Part of the AI Workbench starter kit.
其他同名作品
- turn-timestampshissncg · ★ 0
