feat(tools): user-defined right-column panels backed by a markdown-emitting script - #243
Open
frenchie4111 wants to merge 6 commits into
Open
feat(tools): user-defined right-column panels backed by a markdown-emitting script#243frenchie4111 wants to merge 6 commits into
frenchie4111 wants to merge 6 commits into
Conversation
…itting script A tool is a directory under <worktree>/.harness/tools/<id>/ with a tool.json (static title, so the panel has a header before the script has ever run) and an executable script whose stdout is rendered as markdown. The directory name becomes a `tool:<id>` panel key, so custom tools flow through the existing per-repo order/visibility config and sit alongside the built-ins in the gear menu. Markdown maps onto the built-in panels' vocabulary rather than document typography — headings become the ChangedFilesPanel section header, list items become rows, and `harness:` links drive send-to-agent / open-file. A narrow contract is what keeps custom panels from drifting out of the design system. Tools are spawned directly on their shebang rather than through a login shell: `-ilc` cost 1-2s per run and would let rc-file chatter leak into stdout, which here is the panel body. useWatchedQuery grows `revalidateOnFileChange` and treats fallbackPollMs of 0 as "no polling", so a tool marked refresh:"manual" runs only on mount and on the refresh button — tool scripts routinely hit the network and the built-in panels' cadence would hammer an API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… contract Discovering that custom tools exist at all required reading the source. Adds an entry at the bottom of the right column's panel menu that sends the full authoring contract to the agent in the active worktree, reusing the same onSendToAgent path the harness:send link verb already uses. The contract lives in src/shared/tools.ts next to the types it describes, so the manifest fields, env vars, and markdown mapping have one source of truth rather than drifting from a copy in the renderer. Exporting it from shared also leaves the door open to serving it over MCP later without a second copy. Costs nothing per session (unlike an MCP tool description, which every agent would pay for whether or not the user ever writes a tool) and is discoverable by the user, who is the one who doesn't yet know the feature exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Seeding the active tab's chat was too implicit — the contract landed as a message in whatever conversation happened to be running there, possibly mid-task, with no indication that was about to happen. Opening the new-worktree screen with the branch and kickoff prompt pre-filled shows the user exactly what's going to run before anything does, and both fields stay editable. It also fits how tools resolve: discovery is worktree-local, so the panel goes live on the new branch while the agent iterates on it, and reaches everyone else on merge. NewWorktreeScreen gains initialBranch / initialPrompt, following the existing initialPRNumber prefill pattern. The prefill is transient renderer state cleared when the screen closes, alongside newWorktreeRepo and newWorktreeInitialPRNumber. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
react-markdown sanitizes hrefs whose protocol isn't in its safe list, so every harness:send / harness:file / harness:refresh link arrived with href="" and fell through to the inert-text branch. Action rows rendered as plain, unclickable text — the feature's whole interactive surface was dead on arrival. Allow the harness: scheme through and defer to defaultUrlTransform for everything else, so the javascript: protection stays intact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The panel body carried py-1 and the list wrapper py-0.5, neither of which the built-in panels have, so a tool's first section header sat lower than Changed Files' does and the whole panel looked misaligned against its neighbours. Match ChangedFilesPanel exactly: a bare scroll container, and rows as direct siblings of their header. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Main renamed the product, so the config file is now .ness.json with .harness.json read as a legacy fallback. Custom tools shipped the old brand throughout their user-facing contract: the .harness/tools directory users create, the harness: link scheme, and the HARNESS_* env vars handed to every script. Unlike .harness.json there's nothing to preserve here — the feature is unreleased, so no dual-read path is needed and none is added. Renaming now costs one commit; renaming after people have tool directories on disk costs a migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
frenchie4111
force-pushed
the
allow-custom-tools
branch
from
August 20, 2026 11:31
6914df3 to
6dff782
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds user-defined tools to the right column. A tool is a directory in the worktree:
Discovery is worktree-local (not repoRoot, unlike
.harness.json) so a branch can iterate on its own tooling and a PR that edits a tool exercises the new version.Markdown is the entire contract. Rather than rendering document typography into a 280px column,
SidebarMarkdownmaps the markdown AST onto the vocabulary the built-in panels already use:#/#####/####-list item`code`/**bold**[label](harness:send?text=...)[label](harness:file?path=...)[label](harness:refresh)[label](https://...)A tool physically cannot express anything outside the design system, so custom panels look native by construction.
Panel keys are namespaced
tool:<id>, so they slot into the existingrightPanelOrder/hiddenRightPanelsmachinery in.harness.jsonfor free — drag-to-reorder and hide both work with no new persistence.effectiveRightPanelOrdernow takes the discovered keys, so a new tool appends without a config write and a deleted tool drops out of a saved order automatically.Env exposed to the script:
HARNESS_WORKTREE_PATH,HARNESS_BRANCH,HARNESS_REPO_ROOT,HARNESS_TOOL_DIR,HARNESS_TOOL_ID.Two findings that changed the implementation
runWorktreeScript'szsh -ilc. That cost 1–2s per run (a test timed out at 5s; the suite took 10.6s) and — worse — would let rc-file chatter (nvm banners, starship init) leak into stdout, which here is the panel body. Now the script is spawned directly and its own shebang picks the interpreter.path-fix.tsalready merged the login-shell PATH at boot, so there was nothing to gain. Suite went to 1.05s. EACCES is translated to "run chmod +x".useWatchedQueryrefetches on every git change. Arefresh: "manual"tool would still re-run constantly and hammer whatever API it talks to. AddedrevalidateOnFileChangeand madefallbackPollMs: 0mean "no polling", so manual tools run only on mount and on the refresh button.Known gaps
Called out deliberately rather than half-implemented:
.harness/tools/directory and opening a worktree runs it.Demo tool (not committed)
.harness/tools/isn't gitignored, so shipping a demo would give every contributor a live panel invokingghon mount. Left it out; paste this into a repo to try it:.harness/tools/pages/tool.json{ "title": "Pages", "script": "run.sh", "refresh": "auto" }.harness/tools/pages/run.sh(chmod +x)Test plan
npm run typechecknpx electron-vite buildnpx vitest run— 11 new tests insrc/main/tools.test.ts(discovery defaults, malformed manifest, script-path escape rejection, env exposure, non-zero exit, missing/non-executable script) + 3 inrepo-configs.test.tsfor tool-aware ordering🤖 Generated with Claude Code