Build a Live Claude Code UI Mod

Build a live Claude Code pane that tracks tool calls and resets on demand.

Introduction

30 Second Summary

Busy coding sessions can bury tool activity inside a long transcript. That makes it hard to see what your coding assistant just used.

In this project, you will build Tool Pulse as a local mod for Claude Code that opens an interactive pane. The pane displays a live tool-call count, the latest tool, and a Reset control.

What You'll Build

Your finished Tool Pulse pane will sit beside the transcript with a counter that climbs whenever Claude Code uses a tool.

By the end of this project, you'll have:

  • The ability to open Tool Pulse on demand with /tool-pulse inside Claude Code.
  • A live view of tool activity where the count climbs and the latest-tool label changes while Claude works.
  • A validated local mod that hot-reloads after saved changes. Its Reset control clears the dashboard without starting another Claude turn.
  • Secret Mission: Build an automated UI test that fires a fake tool call, mounts the Tool Pulse pane, and presses Reset without a live session or network.

Are there any prerequisites?

Existing Claude Code access on a Mac, a trusted local workspace, and basic JavaScript familiarity are the only prerequisites.

The project adds no new subscription or cloud resource. Normal plan or API usage still applies during the interactive session.

Before We Start

This first checkpoint helps you commit to building Tool Pulse before the hands-on work begins. You are creating a Claude Code pane that keeps a live tool-call count and the latest tool name visible beside the transcript, where you can also interact with it.

Prepare the Mod Workspace

A Claude Code UI mod can only load when the installed build supports mods. The local environment must also allow hooks modules to run.

This step confirms both requirements before any project code exists. You will finish with an empty Tool Pulse workspace open in Visual Studio Code.

In this step, get ready to:
  • Confirm that your Claude Code version supports terminal mods.
  • Create the empty directories for the Tool Pulse workspace.
  • Confirm that mods can load before opening the workspace in Visual Studio Code.
Check your Claude Code version

Terminal mods require Claude Code 2.1.287 or later. Start by checking the version installed on your Mac.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into the search field.
  • Press Enter to open macOS Terminal.
  • Check your installed Claude Code version by running this command:
claude --version

What does this command check?

The command prints the Claude Code version available to Terminal. The result determines whether you can continue or need an update.

✔️ I see version 2.1.287 or higher

Your Claude Code build supports terminal mods. That compatibility check is complete.

ⓧ I see an older version

Claude Code uses a different update command for each installation method. Choose the method used to install it on your Mac.

Native or npm

  • Update the native or npm installation by running this command:
claude update
What does this command do?

The Claude Code updater applies the current release to a native or npm installation. Let the update finish before checking the version again.

Homebrew stable

  • Update the stable Homebrew cask by running this command:
brew upgrade claude-code
What does this command do?

Homebrew upgrades the stable Claude Code cask installed on your Mac. Let the upgrade finish before checking the version again.

Homebrew latest

  • Update the latest Homebrew cask by running this command:
brew upgrade claude-code@latest
What does this command do?

Homebrew upgrades the latest Claude Code cask installed on your Mac. Let the upgrade finish before checking the version again.

  • Confirm the updated version by running the check again:
claude --version

What should this confirm?

The second check reads the updated executable available to Terminal. A result of 2.1.287 or later confirms that your installation supports terminal mods.

Still seeing an older version?

Confirm that the update command matches your original installation method. Restart Terminal if it still reports the previous version.

Help me identify and update my Claude Code installation on macOS.

ⓧ Command not found

Terminal cannot find the Claude Code launcher. Use Anthropic's setup guide to restore a supported installation.

  • Open the Claude Code setup guide in your browser.
  • Follow the macOS installation path that matches your Claude Code access.
  • Return to Terminal after the installation finishes.
  • Check the installed version by running this command:
claude --version

What should this confirm?

The command confirms that Terminal can now find Claude Code. A result of 2.1.287 or later gives you the mod support required for Tool Pulse.

Claude Code still unavailable?

Restart Terminal so it can refresh its command path. Confirm that the installation completed before running the version check again.

Help me make the Claude Code launcher available in Terminal.

Create the mod workspace

A local mod uses a plugin manifest directory plus a hooks directory. Both directories remain empty during this environment check.

  • Move to your Desktop by running this command:
cd ~/Desktop

Why start on the Desktop?

This command moves Terminal to a predictable location before creating the workspace. The Tool Pulse directory stays easy to find from Terminal or Visual Studio Code.

  • Create the two empty Tool Pulse directories by running these commands:
mkdir -p tool-pulse/.claude-plugin tool-pulse/hooks
cd tool-pulse

What do these commands do?

  • The mkdir command creates tool-pulse/.claude-plugin plus tool-pulse/hooks.
  • The -p option creates the parent tool-pulse directory when required.
  • The cd command moves Terminal into the new workspace.

Before you run the availability check, do you expect Claude Code to find a hooks module inside these empty directories?

  • Confirm that the local environment allows mods by running this command:
claude plugin test

Why is this message useful?

The test runner checks the current plugin directory for a hooks module. The message no hooks module to load confirms that mods can load while correctly reporting that your workspace is empty.

You should see output containing no hooks module to load. Your environment has passed the mod availability check.

Seeing different test output?

Confirm that Terminal is inside the tool-pulse directory. Local mods can also be unavailable when an organization policy blocks them.

Help me troubleshoot the mod availability check.

Open the workspace in Visual Studio Code

The Visual Studio Code launcher can open the directory currently selected in Terminal. Its Explorer provides a visible check that both required directories exist.

  • Open the current tool-pulse workspace in Visual Studio Code by running this command:
code .

What does this command do?

The code launcher opens the current Terminal directory in Visual Studio Code. The workspace should therefore open at tool-pulse.

✔️ Visual Studio Code opens

Your Tool Pulse workspace is open in Visual Studio Code. Keep the Terminal window available for the final verification.

ⓧ The code launcher is unavailable

The Visual Studio Code app can install its Terminal launcher through the Command Palette. This makes the code command available to new Terminal sessions.

  • Press Cmd+Space to open Spotlight.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.
  • Press Cmd+Shift+P to open the Command Palette.
  • Type Shell Command: Install 'code' and optionally 'copilot' commands in PATH into the Command Palette.
  • Select the matching action from the Command Palette.
  • Restart Terminal after the installation completes.
  • Return to the Tool Pulse workspace by running this command:
cd ~/Desktop/tool-pulse

Where does this command go?

This command returns Terminal to the tool-pulse directory on your Desktop. The next launcher command opens the correct workspace.

  • Open the workspace with the installed launcher by running this command:
code .

What should happen now?

Visual Studio Code should open with tool-pulse as the current workspace. The launcher is now available to Terminal.

Launcher still unavailable?

Repeat the Command Palette installation action. Restart Terminal after Visual Studio Code confirms the action.

Help me install the Visual Studio Code launcher on macOS.

  • Confirm that you trust the local workspace if Visual Studio Code asks about workspace trust.
  • Select Explorer in the left Activity Bar.
  • Expand the tool-pulse workspace in the Explorer.

You should see .claude-plugin plus hooks inside tool-pulse. Both directories should be empty.

Before the final check, what should the version command prove? What should the empty-workspace test report?

  • Switch back to the Terminal window from earlier.
  • Confirm the installed Claude Code version by running this command:
claude --version

What does this result prove?

A result of 2.1.287 or later confirms that your Claude Code build supports terminal mods. This completes the version half of the final check.

You should see version 2.1.287 or later.

  • Confirm mod availability from the empty workspace by running this command:
claude plugin test

What does this result prove?

The message no hooks module to load confirms that the mod runner is available. It also confirms that no hooks module has been added early.

You should see output containing no hooks module to load. Your empty Tool Pulse workspace is ready.

✔️ Awesome, my workspace matches

That is the foundation ready. Visual Studio Code now has the trusted empty Tool Pulse workspace open.

ⓧ I'd like to double check the full code

There is no project code yet. The tool-pulse/.claude-plugin directory exists and is empty.

The tool-pulse/hooks directory also exists and is empty. No other project artifact is required at this point.

Your environment can load mods. Next, you will define Tool Pulse and open its first static pane inside Claude Code.

Open a Static Tool Pulse Pane

Your trusted tool-pulse workspace gives you a safe place to build. A mod becomes visible when Claude Code discovers its plugin files.

This step creates the first visible win inside Claude Code. You will register /tool-pulse to open a pane that displays two default values.

In this step, get ready to:
  • Define the files that let Claude Code discover the mod.
  • Register /tool-pulse to open a static pane.
  • Validate the mod before loading it in a local session.
Define the plugin files

A plugin manifest gives the mod its identity. A hooks loader tells Claude Code which module contains the mod logic.

The manifest uses JSON to describe Tool Pulse. The initial project-local version is 0.1.0.

  • Select the empty .claude-plugin directory in the Visual Studio Code Explorer.
  • Use the new-file control in the Explorer.
  • Enter plugin.json as the file name.
  • Create the file using the editor confirmation control.
  • Add the Tool Pulse identity to plugin.json by pasting this code:
{
  "name": "tool-pulse",
  "version": "0.1.0",
  "description": "Shows live Claude Code tool-call activity in an interactive pane",
  "author": { "name": "Learner" }
}

What does this manifest do?

  • The name value identifies this plugin as tool-pulse.
  • The version value records the first local release of the plugin.
  • The description value summarizes what the pane displays.
  • The author object records who owns this local artifact.
  • Save plugin.json using the save control in Visual Studio Code.
  • Confirm plugin.json appears inside .claude-plugin in the Explorer.

Manifest missing from the Explorer?

Check that plugin.json sits directly inside .claude-plugin. Move it into that directory if it appears beside the directory.

Make sure the file name ends with .json. Remove any extra text that your editor added to the name.

Help me check my Tool Pulse plugin manifest location.

✔️ Awesome, I've got everything!

Your plugin manifest is saved inside .claude-plugin.

ⓧ I'd like to double check the full code

Compare your .claude-plugin/plugin.json file with this complete version.

{
  "name": "tool-pulse",
  "version": "0.1.0",
  "description": "Shows live Claude Code tool-call activity in an interactive pane",
  "author": { "name": "Learner" }
}

The hooks loader provides the path from the manifest to the module that registers each handler. Claude Code resolves this path from the hooks directory.

  • Select the empty hooks directory in the Visual Studio Code Explorer.
  • Use the new-file control in the Explorer.
  • Enter hooks.json as the file name.
  • Create the file using the editor confirmation control.
  • Point the hooks loader at register.js by pasting this code:
{
  "description": "The Tool Pulse hooks module",
  "modules": ["./register.js"]
}

How does the loader work?

  • The description value labels the hooks module.
  • The modules array points to ./register.js beside this loader.
  • Save hooks.json using the save control in Visual Studio Code.
  • Confirm hooks.json appears inside hooks in the Explorer.

Loader saved in the wrong place?

Check that hooks.json sits inside the hooks directory. The path to register.js depends on both files sharing that directory.

Help me verify my Tool Pulse hooks loader.

✔️ Awesome, I've got everything!

Your hooks loader now points to the module you will create next.

ⓧ I'd like to double check the full code

Compare your hooks/hooks.json file with this complete version.

{
  "description": "The Tool Pulse hooks module",
  "modules": ["./register.js"]
}
Build the static pane

The register.js file is the JavaScript hooks module. Its event handlers register the slash command before opening the pane.

Module-level values hold the pane identifier plus the two displayed defaults. The renderer reads those values each time Claude Code draws this pane.

  • Select the hooks directory in the Visual Studio Code Explorer.
  • Use the new-file control in the Explorer.
  • Enter register.js as the file name.
  • Create the file using the editor confirmation control.
  • Add the shared state plus the command handlers to register.js by pasting this code:
const PANE = 'tool-pulse'

let calls = 0
let lastTool = 'None yet'

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: 'tool-pulse',
      description: 'Open the Tool Pulse dashboard',
    })
    return next(e)
  })

  on('command.run', { command: 'tool-pulse' }, async ($) => {
    await $.ui.open({
      id: PANE,
      title: 'Tool Pulse',
      focus: true,
      closeOnEscape: true,
    })
    return {}
  })
}

What does this code do?

  • PANE gives the pane a stable identifier that later handlers can recognize.
  • calls starts the visible counter at 0.
  • lastTool starts the latest-tool label at None yet.
  • session.start registers the project command when a session begins.
  • command.run opens the focused Tool Pulse pane when the command runs.
  • Save register.js using the save control in Visual Studio Code.
  • Confirm register.js appears beside hooks.json in the Explorer.

Module missing from the hooks directory?

Move register.js into the hooks directory if it appears elsewhere. The loader looks for the module beside hooks.json.

Help me locate my Tool Pulse hooks module.

The command can open a pane once a render handler supplies its contents. This renderer builds a vertical layout from the current module-level values.

  • Place your cursor on the blank line before the final closing brace in register.js.
  • Add the pane renderer by pasting this code:
  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
    if (e.requestId !== PANE) return next(e)

    const { Box, Text } = $.ui.resolve(e)

    return Box({
      flexDirection: 'column',
      rowGap: 1,
      children: [
        Text({ children: ['Tool calls: ' + calls] }),
        Text({ children: ['Last tool: ' + lastTool] }),
        Text({ children: ['Run a Claude task to update this pane.'] }),
      ],
    })
  })

How does the pane render?

  • The requestId check limits this handler to the Tool Pulse pane.
  • $.ui.resolve(e) obtains the interface constructors supported by the current surface.
  • Box arranges the pane contents in one vertical column.
  • Text turns the current counter plus the latest-tool label into visible rows.
  • Save register.js using the save control in Visual Studio Code.

✔️ Awesome, I've got everything!

Your hooks module now registers the command before rendering the static pane.

ⓧ I'd like to double check the full code

Compare your complete hooks/register.js file with this version.

const PANE = 'tool-pulse'

let calls = 0
let lastTool = 'None yet'

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: 'tool-pulse',
      description: 'Open the Tool Pulse dashboard',
    })
    return next(e)
  })

  on('command.run', { command: 'tool-pulse' }, async ($) => {
    await $.ui.open({
      id: PANE,
      title: 'Tool Pulse',
      focus: true,
      closeOnEscape: true,
    })
    return {}
  })

  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
    if (e.requestId !== PANE) return next(e)

    const { Box, Text } = $.ui.resolve(e)

    return Box({
      flexDirection: 'column',
      rowGap: 1,
      children: [
        Text({ children: ['Tool calls: ' + calls] }),
        Text({ children: ['Last tool: ' + lastTool] }),
        Text({ children: ['Run a Claude task to update this pane.'] }),
      ],
    })
  })
}
Validate and open the pane

Static validation checks the plugin structure before Claude Code loads it. This catches misplaced files plus invalid hooks code before an interactive session begins.

Before you validate, do you think Claude Code will detect all three files as one mod?

  • Switch back to the terminal in the tool-pulse workspace from earlier.
  • Validate the completed mod by running this command:
claude plugin validate .

What should you see?

The validator reads the manifest plus the hooks module. You should see a successful validation result after it detects the registered handlers.

That pass is a real milestone. Claude Code now recognizes your three files as one loadable mod.

Validation did not pass?

Check that plugin.json is inside .claude-plugin. Check that both hooks files are inside hooks.

Confirm the module path is ./register.js. A different path prevents the loader from finding the module.

Help me diagnose my Tool Pulse validation result.

Claude Code mods run with your user permissions. This workspace contains only the three local files you just reviewed.

A workspace trust prompt can appear during startup. Accepting it allows your local mod to load in this workspace.

  • Start a local development session by running this command:
claude --plugin-dir .

What happens in this session?

The command loads Tool Pulse for this session. Claude Code also watches the plugin directory for saved changes.

The terminal now hosts the interactive Claude Code session. Keep this session running for the next steps.

  • Accept the workspace trust prompt if it appears.

Before you open the pane, which two default values do you expect the renderer to display?

  • Enter /tool-pulse at the Claude Code prompt.

You will see a pane titled Tool Pulse. It shows Tool calls: 0 plus Last tool: None yet.

Pane did not open?

Confirm the validation succeeded before this session started. Restart the local session after correcting any validation problem.

Check that you entered /tool-pulse inside the Claude Code prompt. Entering it in the regular shell does not run the registered command.

Help me open the Tool Pulse pane.

Your own slash command now opens a real pane inside Claude Code. Next, you will test what this first renderer can observe during real tool activity.

Prove the Dashboard Is Stale

Your Tool Pulse command now opens a pane inside Claude Code. Its labels currently come from module-level defaults.

A finished-looking interface can still be disconnected from runtime activity. This step puts the static dashboard against a real task so its behavior becomes visible.

In this step, get ready to:
  • Confirm the pane's starting values.
  • Ask Claude to inspect the workspace through real tool calls.
  • Compare the transcript activity with the dashboard's values.
Confirm the starting display

A clear baseline lets you spot whether runtime activity changes either dashboard value.

  • Switch back to the Claude Code session from earlier.
  • Enter /tool-pulse at the Claude Code prompt if the pane is closed.

The pane should show Tool calls: 0. It should also show Last tool: None yet.

Trigger real tool activity

The transcript provides direct evidence when Claude uses tools. You can compare that evidence with the pane after the task finishes.

  • Return focus to the Claude Code prompt.

Before you submit this task, do you expect both dashboard values to change?

  • Enter List the files here and read .claude-plugin/plugin.json at the Claude Code prompt.
  • Approve each read-only tool call if Claude Code asks for permission.
  • Watch the transcript until Claude finishes the task.

You will see Claude list the workspace files. You will also see it read .claude-plugin/plugin.json.

That gives you the evidence you need. Claude has used real tools inside the same session.

Compare the pane with the transcript

The transcript now shows runtime activity. The dashboard reveals whether the current hooks module can react to it.

  • Return focus to the Tool Pulse pane.
  • Compare its tool-call count with the completed transcript.
  • Compare its latest-tool label with the completed transcript.

You will still see Tool calls: 0. You will still see Last tool: None yet.

This stale result is the intended outcome. The pane can draw its defaults without reacting to the tools Claude uses.

Why did the pane stay stale?

The existing ui.render handler draws the current values of calls and lastTool. It does not observe tool activity by itself.

The module has no tool.call event handler to change those values. It also performs no UI invalidation to request another render.

Seeing a different result?

  • Submit the exact workspace inspection task again if the transcript does not show tool activity.
  • Approve the read-only requests if Claude Code pauses for permission.
  • Check that hooks/register.js still has no tool.call handler or Button if the pane updates.

help me diagnose my stale-dashboard result.

The dashboard's missing path is now visible. Next, you'll make tool calls update its state. You will also trigger a redraw after each call.

Make Tool Pulse Live and Interactive

Your static Claude Code pane already opens through /tool-pulse. Its unchanged values exposed a missing event handler.

The tool.call event will update shared state whenever Claude attempts a tool. UI invalidation will request a fresh render.

A control callback will reset the same shared state. That control proves the pane can respond without starting another Claude turn.

In this step, get ready to:
  • Connect real tool calls to the dashboard's shared state.
  • Add a Reset control that redraws the pane.
  • Demonstrate the complete event-to-render loop after local plugin validation.
Connect tool calls to shared state

A tool.call event arrives before an attempted tool executes. Your handler records that event before next(e) lets the tool continue.

  • In the Explorer sidebar of Visual Studio Code, select hooks/register.js.
  • Insert the tool-call handler immediately above the existing ui.render hook by copying this code:
  on('tool.call', async ($, e, next) => {
    calls += 1
    lastTool = String(e.tool)
    $.ui.invalidate('ui.render')
    return next(e)
  })

How Does the Event Path Work?

  • The calls += 1 line increments the shared total once for every attempted tool call.
  • The String(e.tool) expression stores the latest tool identifier as displayable text.
  • The $.ui.invalidate('ui.render') call asks Claude Code to run the render hooks again after the state changes.
  • The return next(e) line allows the original tool call to continue.
  • Save hooks/register.js with Cmd+S.
  • Return to the Claude Code session in Terminal.

You'll see a reload message in the session. That hot reload confirms Claude Code has loaded the new event handler.

No Reload Message?

Confirm that you saved hooks/register.js inside the current tool-pulse folder. Confirm that the original session is still running with --plugin-dir from the mod root.

Help me find why my Tool Pulse hooks module is not hot-reloading.

Add the Reset control

The pane already reads the module-level values during each render. A Button can change those values through its onPress callback.

  • In hooks/register.js, replace the existing ui.render hook with this completed version:
  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
    if (e.requestId !== PANE) return next(e)

    const { Box, Text, Button } = $.ui.resolve(e)

    return Box({
      flexDirection: 'column',
      rowGap: 1,
      children: [
        Text({ children: ['Tool calls: ' + calls] }),
        Text({ children: ['Last tool: ' + lastTool] }),
        Text({ children: ['Run a Claude task to update this pane.'] }),
        Button({
          key: 'reset',
          label: 'Reset (r)',
          hotkey: 'r',
          onPress: () => {
            calls = 0
            lastTool = 'None yet'
            $.ui.invalidate('ui.render')
          },
        }),
      ],
    })
  })

How Does Reset Work?

  • The resolver now includes Button so the terminal surface can construct the control.
  • The reset key gives the control a stable identifier.
  • The r hotkey activates the button while its pane has keyboard focus.
  • The callback restores both module-level values to their defaults.
  • The final invalidation redraws the pane with the restored values.
  • Save hooks/register.js with Cmd+S.
  • Return to the open Claude Code session.
  • Reveal the new control by entering this slash command at the Claude Code prompt:
/tool-pulse

What Should I See?

The command handler opens the focused Tool Pulse pane. You'll see Reset (r) beneath the three text rows.

Reset Control Missing?

Check that the resolver includes Button. Check that the Button({ block sits inside the children array.

Help me find why the Reset button is missing from my Tool Pulse pane.

Validate and demonstrate the live loop

The static validator checks the manifest and hooks module before you run the final demonstration. The live check then proves that one event can update shared state and redraw the pane.

  • In macOS Terminal, create another tab with Cmd+T.

Before you run the validator, do you expect it to accept both new handlers? Hold that prediction for the result.

  • Validate the completed plugin from the tool-pulse folder by running:
claude plugin validate .

What Does the Validator Check?

The validator reads the plugin manifest and hooks module. A successful run ends with ✔ Validation passed.

Validation Did Not Pass?

Use the validator's file reference to locate the problem. Compare every closing bracket and comma in the recently edited hook.

Help me diagnose why my completed Tool Pulse plugin does not pass validation.

Before you send this task, do you expect the next pane render to keep the defaults or show live activity? Make your choice before the tools run.

  • Return to the running Claude Code session.
  • Trigger tool activity by sending this prompt:
Inspect .claude-plugin/plugin.json, hooks/hooks.json, and hooks/register.js

What Happens During the Inspection?

Claude inspects the three plugin files through tool calls. Each attempted tool updates calls and lastTool before the call continues.

  • Open the dashboard after the inspection by entering:
/tool-pulse

What Should the Live Pane Show?

You'll see a tool-call count above zero. You'll also see the latest tool name beside Last tool:.

Before you press r, which two values do you expect Reset to restore? Check your prediction against the pane.

  • Press r while the Tool Pulse pane has focus.

You'll see Tool calls: 0 and Last tool: None yet immediately. The focused button also responds to Enter.

You did it. Tool Pulse now reacts to real tool calls and resets on demand.

✔️ Awesome, I've got everything!

Your hooks/register.js now contains the complete event-render-control loop.

ⓧ I'd like to double check the full code

Compare your complete hooks/register.js with this reference.

const PANE = 'tool-pulse'

let calls = 0
let lastTool = 'None yet'

export function register(on) {
  on('session.start', async ($, e, next) => {
    await $.command.register({
      name: 'tool-pulse',
      description: 'Open the Tool Pulse dashboard',
    })
    return next(e)
  })

  on('command.run', { command: 'tool-pulse' }, async ($) => {
    await $.ui.open({
      id: PANE,
      title: 'Tool Pulse',
      focus: true,
      closeOnEscape: true,
    })
    return {}
  })

  on('tool.call', async ($, e, next) => {
    calls += 1
    lastTool = String(e.tool)
    $.ui.invalidate('ui.render')
    return next(e)
  })

  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
    if (e.requestId !== PANE) return next(e)

    const { Box, Text, Button } = $.ui.resolve(e)

    return Box({
      flexDirection: 'column',
      rowGap: 1,
      children: [
        Text({ children: ['Tool calls: ' + calls] }),
        Text({ children: ['Last tool: ' + lastTool] }),
        Text({ children: ['Run a Claude task to update this pane.'] }),
        Button({
          key: 'reset',
          label: 'Reset (r)',
          hotkey: 'r',
          onPress: () => {
            calls = 0
            lastTool = 'None yet'
            $.ui.invalidate('ui.render')
          },
        }),
      ],
    })
  })
}

What Should Match?

The file contains four registered handlers inside register(on). The tool.call handler and Reset callback both invalidate the same render hook after changing shared state.

Secret mission

Test the UI Without a Live Session

Turn the Tool Pulse demo into a repeatable automated test. You will fire a fake tool call, mount the terminal pane, press Reset, and verify the complete event-render-control loop without a live session or network.

Clean Up Your Resources

Clean Up Your Resources

Tool Pulse lives entirely in local files, so keeping the project creates no ongoing cost. Decide whether to keep your live session active, pause it for later, or remove the local directory.

Resources you used:

  • Hot-reloading Claude Code session that currently loads the local mod.
  • Local tool-pulse directory containing .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.js, and tests/tool-pulse.test.ts.

Keep everything running

No action needed. Choose this if you are still developing or demonstrating Tool Pulse.

  • Leave the hot-reloading Claude Code session active.
  • Keep the tool-pulse directory available for future validation and testing.

Normal Claude Code plan or API usage still applies when you ask Claude to perform interactive work.

Pause - I'll come back to this later

End the hot-reloading session to stop Tool Pulse from loading. Your local files stay ready for another session.

  • Close the Terminal tab running the hot-reloading Claude Code session.
  • Keep the tool-pulse directory on your Mac.
  • Start future Claude Code sessions without loading the Tool Pulse plugin when you want the mod to stay paused.

Delete - I don't want to use this again

Remove the local project resources to start fresh. Your Claude Code installation and account stay untouched.

  • Close the Terminal tab running the hot-reloading Claude Code session.
  • Close the Visual Studio Code window for tool-pulse.
  • Click Finder in your Mac's Dock.
  • Locate tool-pulse in the directory where you created it.
  • Move tool-pulse to the Trash.
  • Empty the Trash.
  • Return to the directory that previously contained tool-pulse.
  • Confirm that tool-pulse is no longer listed.

Nice Work!

Nice Work!

You did it! Your Tool Pulse Claude Code UI mod now tracks tool calls inside an interactive pane.

You've learned how to:

  • Register /tool-pulse during session.start. Open a focused interactive pane from command.run.
  • Track live tool activity through tool.call. Update calls plus lastTool for each event. Redraw the pane through ui.render invalidation.
  • Create a Reset control that restores the dashboard defaults without another Claude turn. Validate the mod locally while the --plugin-dir session hot-reloads saved changes.
  • Secret Mission: Built an automated UI test that fires a fake Bash tool call. The test mounts the terminal pane. It presses the keyed Reset control. It verifies both dashboard states without a live session or network.

Ready to quiz yourself?