Imported from finchtoys/finch-releases (
skills/finch-mini-tool-creator/SKILL.md). Install upstream withnpx skills add finchtoys/finch-releases --skill finch-mini-tool-creator. Copyright stays with the author.
Finch Mini Tool Creator
This skill is the entry point for creating Finch mini tools.
- Product name: mini tool. In the app UI and to Chinese-speaking users this same concept is now labeled "Finch 小程序" (mini program) — "小程序"/"mini program" and "mini tool"/"扩展" refer to the same extension mechanism, just different display names. Only the UI copy changed; the manifest fields, code, CLI commands, and directory names below are unchanged.
- Install directory:
extensions(unchanged) - Tech/API surface: use the published
@finchtoys/minitool-apipackage for Finch APIs
Use this skill as an index first:
- Before writing any code, complete the pre-flight checklist in §0.
- Learn the basic rules in §1 Quick Start.
- Read the tool design principles in §2 Tool Design Principles — this is mandatory, not optional.
- Check the supported folder layout in §3 Project Structure.
- Read the manifest and runtime rules in §4 Core Rules.
- Use References for exact API signatures and field details.
0. Pre-flight — Read Before Coding
Do not write any code until you have read every reference file that applies to your mini tool.
Identify what your mini tool needs, then read the matching files:
| Feature | Must read |
|---|---|
| Agent tools | reference/tools.md |
| Composer toolbar buttons | reference/composer-actions.md and reference/icons.md |
| Custom icons | reference/icons.md — §2 built-in list first, then §3 SVG rules |
| Storage / secrets | reference/finch.d.ts → Storage / Secrets interfaces and reference/ui.md; ctx.storage is plaintext and credentials must use ctx.secrets |
| OAuth account linking | reference/oauth.md and reference/finch.d.ts → OAuth interfaces; standard providers use packaged PNG OAuthProviderConfig.icon, while advanced protocol authorization uses trusted providerIcon |
| Dialogs / images / Canvas | reference/ui.md §6 and reference/finch.d.ts → UI, CanvasWindow ready/state/visibility lifecycle, atomic update(), frame budget, dirty redraw, and Main motion interfaces |
| Right-panel Webview / host Session navigation / file or image annotations | reference/ui.md §7 and reference/finch.d.ts → AppPanel, WebviewBridgeApi.navigation.openSession, origin rules, Composer context drafts; static launcher entry via contributes.appPanel (see §4) |
| App View opening built-in preview/Diff/browser or another mini tool's App View | §4 "Host preview & navigation stack" bullet and reference/finch.d.ts → WebviewBridgeApi.appView, AppViewDiffRequest, AppViewContribution.embeddable |
| Status snapshots | reference/finch.d.ts → Status interface |
| Bot / remote / Agent Session messaging / container settings menu | reference/session.md and reference/finch.d.ts → SessionContainerSettingsMenuProvider |
| MCP integration | reference/mcp.md — registration, protocol-version negotiation, and standard request headers (Mcp-Method / Mcp-Name / MCP-Protocol-Version are generated by the transport, never fixed config) |
| Publishing | reference/publish.md; for a request to list an already-published mini tool in the official community, use AppCall action=feedback with feedbackCategory: "minitool" — do not directly edit the community index. |
ComposerAction icons are the most common failure point. Before setting any icon field anywhere (manifest or code), open reference/icons.md and confirm the id is in the built-in list (§2). If it is not listed there, it will render as plain text — always register a runtime SVG pack instead of guessing. Do not skip this check.
1. Quick Start
A mini tool is an npm-style TypeScript package discovered from the file system. It can contribute Agent tools, Composer toolbar buttons, bundled Skills, and other Finch runtime capabilities through a single MiniToolContext (ctx) object. There is no ExtensionContext compatibility alias — MiniToolContext is the only name.
Minimum shape:
my-mini-tool/
├── package.json
├── tsconfig.json
└── src/
└── index.ts
Core rules:
- Export
activate(ctx)as a named export. - Use
import type * as finch from '@finchtoys/minitool-api'for types only. - Push every
Disposableintoctx.subscriptions. - Keep runtime logic in
src/and compile todist/. - Use
npx @finchtoys/minitoolsfor install/update/remove. - For ComposerAction menus, every actionable item must include
iconName: reuse a built-in Finch icon first; otherwise register a Lucide (or compatible library) SVG and use itsext:reference. Seereference/icons.md. - Register Agent tools as lowercase English
snake_casenames in the form<mini_tool_name>_<function_name>; never use short generic names such asinit,build, orstatus. Seereference/tools.md. - Store credentials only with
ctx.secrets.set()after declaring exact keys (or a trailing prefix such asmcp.*) inpermissions.secrets;ctx.storageis plaintext. Read withget()and clear withdelete()—never write secrets into config JSON.
If you only need the exact API signatures, skip ahead to References.
2. Tool Design Principles
This section is mandatory reading before registering any Agent tools. Ignoring these rules produces mini tools that are hard to use, waste model context, and break the tool-selection experience.
2.1 Register as few tools as possible
Every registered tool is injected into the model's context on every turn. Too many tools waste tokens and make the model less reliable at choosing the right one.
Rules:
- Register the minimum number of tools that covers the feature.
- If a set of operations shares the same subject and input context, put them in one tool with an
actionparameter rather than separate tools. - If you genuinely need many tools (roughly 10+), expose them as a local MCP server so Finch loads them on demand. See
reference/mcp.md.
2.2 Use an action parameter to unify related operations
When one logical capability has multiple operations (create / update / delete / list / publish …), model them as a single tool with a required action enum:
ctx.tools.register({
name: 'pjblog_post',
title: 'PJBlog Post',
description: `Manage blog posts.
action:
list — list all posts (drafts and published)
create — create a new draft post
update — update the title, tags, or body of an existing post
delete — permanently delete a post
publish — publish a draft post`,
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['list', 'create', 'update', 'delete', 'publish'],
},
slug: { type: 'string', description: 'Post slug (required for update / delete / publish)' },
title: { type: 'string', description: 'Post title (required for create)' },
body: { type: 'string', description: 'Post body in Markdown (optional for create / update)' },
},
required: ['action'],
},
risk: 'medium',
async execute(input, exec) {
switch (input.action) {
case 'list': /* ... */
case 'create': /* ... */
// ...
}
},
});
Always enumerate every available action in the description field. This is the only way the model knows what it can do with the tool. One line per action, with a short explanation.
2.3 When to use a local MCP server instead
Choose a local MCP server when:
- The tool set is large and most tools are rarely used together.
- The feature wraps an external service that already has an MCP SDK.
- You want Finch to load tools on demand rather than upfront.
See reference/mcp.md for the full setup pattern.
2.4 Summary checklist before registering tools
- Is the total number of tools as small as possible?
- Are multi-operation features unified under one tool with
action? - Does every
actionvalue appear in the tooldescription? - Does each tool name follow
<mini_tool_name>_<function_name>(snake_case)? - Did you read
reference/tools.md?
3. Project Structure
Finch discovers mini tools from two supported tiers, checked in this order:
| Tier | Path | Use when |
|---|---|---|
| Personal | <finchHome>/.finch/extensions/<id>/ |
Default choice |
| Global | ~/.finch/extensions/<id>/ |
Shared machine-wide install |
Notes:
- The installed directory is named after the runtime id. Do not hard-code or depend on that directory name in your package.
- For every community mini tool, choose a stable, globally unique
package.json#name; this is its identity. Finch derives the runtime extension id at install time, rather than usingfinch.id:finch-my-tool→finch-my-tool,@yourscope/finch-my-tool→yourscope@finch-my-tool. Use a scoped npm package (@your-scope/finch-…) whenever possible, keep its name stable after publishing, and omitfinch.identirely.finch.idis only meaningful for Finch's directly copied bundled extensions. Seereference/publish.md§2 for the compatibility and migration details. - Project-level installs are not supported.
- Always install with the official CLI so the real path is used.
4. Core Rules
Manifest
A minimal mini tool needs:
manifestVersion- optional
minVersionwhen the mini tool depends on APIs introduced by a specific Finch release namemainactivationEventscontributespermissionswhen needed
minVersion declares the lowest Finch app version allowed to load the mini tool. Use one complete SemVer string such as "1.6.0"; do not use ranges such as ">=1.6.0". Omit it only when the mini tool genuinely works on every Finch release that understands its manifestVersion. Finch keeps incompatible mini tools visible in the Toolbox, but blocks activation and shows the required version. manifestVersion describes manifest schema compatibility; it does not replace minVersion.
For Composer toolbar buttons, declare id, icon, and short tooltip text in contributes.composerActions. Longer hover descriptions belong to hoverText on items returned by getMenu(), not to the manifest button declaration. Session containers may also declare icon with the same built-in or ext: SVG IconRef strategy; omitted icons fall back to bot. A settings menu's icon is a separate IconRef with its own fallback of sliders-horizontal.
A mini tool may expose one unified settings menu. Declare contributes.settingsMenu at the top level of contributes, then register it at runtime with ctx.settingsMenu.register({ getMenu, execute }). One declaration lights up two surfaces: the header actions of every session container this mini tool owns (beside the model picker in inbox mode, in the header action area in assistant mode), and the Toolcase — on the mini tool card left of the enable toggle, plus its detail page action row. ctx.settingsMenu receives surface: 'container' | 'toolcase' (with containerId only on the container surface) so getMenu() can vary rows per surface. Use getMenu() for rows and execute() for the selected row; execute() may call ctx.ui.showModalDialog() for account login or connection settings.
If the manifest also declares a top-level settings schema, Finch appends a built-in Settings row to the end of that menu, which opens the native settings form. A mini tool with only a settings schema and no contributes.settingsMenu still gets the button — clicking it opens the form directly instead of a one-row menu, and it keeps working while the mini tool is disabled.
Legacy: contributes.sessionContainers[].settingsMenu + ctx.sessionContainers.registerSettingsMenu(containerId, provider) still works, but it is limited to that container's own header — it never reaches the Toolcase. Do not use it in new mini tools; migrate to ctx.settingsMenu.register(). (Finch does promote a mini tool's single legacy container menu into the Toolcase so already-shipped mini tools are not left without an entry.)
Panel App declaration (contributes.appPanel)
Declare the mini tool's unique Panel App with contributes.appPanel. One mini tool may declare at most one (no plural appPanels). This declaration is the single source of truth for the page, title, icon, toolbar, and default instance mode. The right Panel launcher, ctx.ui.createPanel(), Composer actions, and Delivery rows all open this same app:
"contributes": {
"appPanel": {
"icon": "gauge",
"viewType": "demo.dashboard",
"instanceMode": "single",
"showInLauncher": true,
"source": { "type": "local", "path": "dist/dashboard.html" }
}
}
source.type: "local"— packaged page inside the mini tool. Finch serves it from the platform static server (http://127.0.0.1:<port>/__finch_ext__/<extensionId>/...), so the page gets a real http origin: ESM<script type="module">andfetch()both work. JS Bridge is injected by default. Never use file:// URLs — they are opaque origins where ESM/fetch are blocked.- Loading local filesystem files inside Panel App / App View webviews: use Finch's
finch-file://protocol instead of starting a second localhost server merely to relay files. It is registered for Mini Program panel and full-view webview partitions (not Finch's general-purpose Browser webview), supports<img src>, CSS URLs, and CORS-enabledfetch(), and applies the same extension/path policy as Finch's Timeline renderer:
Pass an absolute path and always wrap it withfunction toFinchFileUrl(absolutePath: string): string { return `finch-file://local?path=${encodeURIComponent(absolutePath)}`; } image.src = toFinchFileUrl('/Users/me/project/output.png'); const response = await fetch(toFinchFileUrl('/Users/me/project/report.md')); if (!response.ok) throw new Error(`Local file read failed: ${response.status}`); const markdown = await response.text();encodeURIComponent(); never concatenate a raw path afterfinch-file://. The sandboxed page has no Nodepath.join(), so backend code should preferably send the final cross-platform absolute path through the panel openingpayloador a Bridge message;finch:env.cwdmay be used when the page already has a platform-safe relative path. Images plus supported Markdown/code/text extensions are served; sensitive paths/file names return403, missing files404, and unsupported formats415.file://remains unsupported. source.type: "url"— developer-hosted service or public page; Bridge is not injected. Inlinehtmlis not supported.instanceMode: "single"reuses the app in the current Panel scope; omit formultiple. Runtime code may override this policy and pass JSON opening context withctx.ui.createPanel({ instanceMode, payload }).showInLauncherdefaults totrueand controls both the right Panel+menu and Welcome page. Set it tofalsefor apps opened only by runtime code or Delivery; the declaration remains available tocreatePanel()and Delivery clicks.titleis optional — omit it (recommended) and Finch shows the mini tool's ownname/displayNamehere, same as everywhere else the mini tool is listed. Only set it when this Panel App genuinely needs a different label than the mini tool itself, and if you do, supply the same language overrides viai18n/<locale>.json→appPanel.title. See "Entry naming consistency" indocs/minitool-app-view.md— this applies toappView.titletoo, and the two should not silently diverge.- Theme adaptation without FinchUI: ahead of a full component kit, every eligible
appPanelandappViewpage (packagedlocalalways;urlonly when it already qualifies for the Bridge) automatically gets Finch's resolved theme as--finch-*CSS variables (--finch-bg-root,--finch-text-primary,--finch-accent,--finch-border,--finch-radius-md,--finch-shadow-sm,--finch-font-body,--finch-message-font-size,--finch-message-line-height,--finch-code-font-size,--finch-code-line-height, …, plus--finch-theme-mode:'light'/'dark') — pure CSS, no Bridge message needed, re-injected automatically whenever the user's theme/skin or font-size setting changes. SeeAppPanelThemeVarinreference/finch.d.tsfor the full list; just reference them directly, e.g.body { font-size: var(--finch-message-font-size); line-height: var(--finch-message-line-height); background: var(--finch-bg-main); color: var(--finch-text-primary); }.- Use light/dark-aware standalone fallbacks: Finch keeps an embedded page hidden until its first
dom-readytheme injection settles, so a hardcoded light fallback can no longer flash through inside Finch. Still wrap fallback colors in@media (prefers-color-scheme: dark)so the packaged page also looks correct when opened directly in a browser, and as defense in depth for older Finch versions. Seeexamples/extensions/webview-panel-lab/src/panel.htmlfor the pattern: define intermediate custom properties like--df-bg-root: var(--finch-bg-root, <light>)at:root, then override the same names with dark literals inside the media query.
- Use light/dark-aware standalone fallbacks: Finch keeps an embedded page hidden until its first
toolbar(optional): a staticAppPanelToolbarItem[]for the panel's own toolbar row, rendered directly under the tab bar for as long as this panel is active — not tucked behind a dropdown, mirroring the built-in Browser panel's own address/action bar. Mix a statictype: 'title'item (requiredid+icon+label, display-only), plain buttons, atype: 'menu'button that opens its own dropdown (items: AppPanelMenuItem[]), atype: 'separator'divider, and atype: 'spacer'flexible blank that pushes everything after it to the trailing edge:
Every click (a top-level button or dropdown row) is sent directly to the page as"toolbar": [ { "type": "title", "id": "section-title", "icon": "book-open", "label": "Library" }, { "id": "reload", "icon": "rotate-cw", "tooltip": "Reload" }, { "type": "separator" }, { "id": "share", "label": "Share", "icon": "share-2" }, { "id": "wrap", "label": "Wrap", "icon": "wrap-text", "checked": true }, { "type": "spacer" }, { "type": "menu", "id": "more", "icon": "ellipsis", "items": [ { "id": "clear-log", "label": "Clear log" }, { "id": "sep", "label": "", "separator": true }, { "id": "about", "label": "About", "icon": "sparkles" } ] } ]{ type: 'finch:menu', itemId }; listen withwindow.finch.onMessage. The page may update its tab withwindow.finch.panel.setTitle()andsetIcon(). From the backendAppPanelhandle, usesetToolbar(items)to atomically replace the whole row, orupdateToolbarItem(id, { label, icon, disabled, checked })to update one top-level item; title items need anidfor the latter. Top-levelbutton/menuitems also takedisabled(greys it out, blocks clicks) andchecked(renders a pressed/accent-highlighted "on" state witharia-pressed, for toggle-style buttons like "wrap text" or "show line numbers"). Neither is self-managing — track the underlying state yourself (in-memory,ctx.storage, whatever fits) and callupdateToolbarItem(id, { checked: nextValue })/updateToolbarItem(id, { disabled: nextValue })after handling thefinch:menuclick that toggles it.- The app opens in the current Panel scope: a Session, Home, session container, or another Panel-capable view.
ctx.ui.createPanel()therefore does not require an active Session.window.finch.composer.addContexts()works on'session'scope (writes into that Session's draft) and'home'scope (writes into the current Space's Home Composer draft) — it only rejects on'container'scope or an unrecognized scope, neither of which has a Composer draft to attach into. Checkfinch:env'sviewfield before calling it if the behavior needs to differ. Nativeui.toast()/ui.confirm()and tab title/icon updates work in every scope. Finch automatically sends{ type: 'finch:env', cwd, sessionId, view, spaceId, spaceName, locale, payload };sessionIdis empty outside a real Agent Session,viewis'session' | 'home' | 'container' | '',spaceId/spaceNameare empty strings when the scope has no active Space (same value backend code already gets viactx's workspace context — now exposed to the page itself, no round trip needed), andlocaleis the same value asctx.app.getInfo().locale, sent for every Panel scope (not justappView). The payload is retained with the Session's Panel tab and delivered again when Finch recreates the page.finch:envis a repeatable signal, not a one-time bootstrap value — Finch re-sends it whenever this tab's owning scope changes without the page reloading (e.g. the user starts a brand-new Session straight from Home while this Panel App is already open there: the tab and its live<webview>move over intact, only the env fields update). Keep thefinch.onMessagelistener subscribed for the page's whole lifetime and treat everyfinch:envmessage as "refresh my env-derived state", not just the first one — do not read it once into a local variable and stop listening. ctx.ui.onDidOpenPanel(listener)receives each live Panel App instance opened by the launcher, ComposerAction, Delivery, orcreatePanel(). Use it to attachpanel.onDidReceiveMessage()once perpanel.id; subscribing also replays currently live instances, and frontend disposal unregisters the Host handle. Handles returned bycreatePanel()remain directly usable and expose the current opening context aspanel.payload. The handle also carriespanel.sessionId/panel.view/panel.spaceId/panel.spaceName— the same classification the page gets viafinch:env, but available to backend code immediately on open, soonDidOpenPanelcan record/log which Session opened the panel without waiting on a page round trip (seeexamples/extensions/webview-panel-labfor a demo that persists these intoctx.storageand offers a ComposerAction menu to jump back to a recorded Session viactx.navigation.openSession).panel.sessionId/panel.vieware live, not frozen at open time — if the tab's owning scope changes later (the Home → new-Session case above), they read the current scope on every access, so it's safe to read them fresh inside eachonDidReceiveMessagehandler (e.g. before callingctx.ui.openFilePreview()/ctx.browser.open()/ctx.ui.openDiff(), which route by the panel's current scope) instead of capturing them into a closure once atonDidOpenPaneltime.panel.visible/panel.onDidChangeVisibilitydescribe the real user-facing state: the app is the selected tab in an expanded, non-auto-hidden Panel. Usefalseto pause expensive backend work. Do not treattrueas guest readiness — a recreated page may not have installed its Bridge listener yet. For state restoration, installwindow.finch.onMessagefirst, send a page-originatedready/init, and have the backend reply with the current snapshot. ApostMessage()failure during Session/Space navigation is transient unlessonDidDisposehas fired; never permanently disable the handle because of one failed send.ctx.ui.delivery.set()contributes the Session's one Delivery row. Its optional JSONpayloadis forwarded when the row opens this declared Panel App; there is no separatetargetPanelViewType.
Icon rule (mandatory): Before setting the icon field, read reference/icons.md §2 and confirm the id appears in the built-in table. If it does not, register a runtime SVG pack (§3) and use ext:<iconId> for an icon in your own mini tool; Finch expands it to the correct fully-qualified pack id. Only cross-mini-tool references need ext:<packId>/<iconId>. An unrecognised bare id silently renders as plain text — there is no warning.
Manifest i18n rule (mandatory): AI-generated and hand-authored manifests must use plain English strings as defaults for every user-visible field. Never embed LocalizedString language maps in a manifest. Put all localized copy in i18n/<locale>.json, using stable IDs, keys, option values, or documented array indexes to override the default. Every new user-visible manifest field must ship with a corresponding i18n override design; inline localization is not an acceptable fallback.
i18n JSON shape trap (mandatory read before writing any i18n/<locale>.json): nested manifest fields (contributes.appView.description, contributes.appPanel.title, composerActions[].tooltip, sessionContainers[].*, …) must be overridden with the same nested object, e.g. {"appView": {"description": "…"}} — never a flattened literal key like {"appView.description": "…"}. The flat form is valid JSON but parses as an unrelated top-level key; Finch silently falls back to the English default with no warning, while unrelated flat fields like top-level name keep working, making it look like an app bug. See reference/manifest.md §10.1 for the full right/wrong example before authoring or debugging any locale file.
App View declaration (contributes.appView)
Use contributes.appView when a mini tool needs a persistent, application-level workspace rather than a right-side Panel tab. Finch adds the entry immediately above Toolcase in the left sidebar and opens it as a full route with the native navigation header.
"contributes": {
"appView": {
"icon": "square-terminal",
"source": { "type": "local", "path": "dist/dashboard.html" }
}
}
- One mini tool may declare at most one
appView. It is application-level and single-instance by extension id; it has noviewType,instanceMode,showInLauncher, or manifest toolbar. - Packaged
localpages use the same static server, isolated Webview partition, and--finch-*theme CSS variables as Panel Apps. They can load local filesystem images/Markdown/code/text withfinch-file://local?path=${encodeURIComponent(absolutePath)}exactly as documented in the Panel App section above. Publicurlpages are allowed but never receive the Bridge. - An App View is not an active Panel scope. Do not call
ctx.ui.createPanel()to open it — butctx.ui.onDidOpenPanel()does fire for it, adopted the moment the user opens the App View, withpanel.view === 'appView'andpanel.sessionId/spaceId/spaceNamealwaysundefined. It is application-level and single-instance by extension id, so re-opening it re-adopts the samepanel.id. - The page can use user-gesture-gated
window.finch.ui.toast(),window.finch.ui.confirm(),window.finch.capture.capturePage(), andwindow.finch.navigation.openSession(sessionId)(pure renderer-side, no backend involved).openSession()performs host-owned navigation in the current Finch window; never render a raw<a href="finch://open?...">inside a guest page, because letting the webview load a custom protocol can blank it and handing the link to the OS can activate a different window. The page also gets the reserved{ type: 'finch:env', view: 'appView', cwd, sessionId: '', spaceId: '', spaceName: '', locale }bridge message automatically on load —cwdis always the app's default/free workspace, never whichever Space happens to be active, and there is no bound Session/Space. It can usewindow.finch.postMessage()/onMessage()— routed to whatever backend listener attachedpanel.onDidReceiveMessage()viactx.ui.onDidOpenPanel()for this instance, same channel a Panel App tab uses. It cannot usewindow.finch.composer.addContexts()(no Composer draft to attach into), Panel toolbar messages (appViewhas notoolbarfield yet), or Delivery targeting. - The page can call
window.finch.panel.setTitle()/setIcon()— same calls a Panel App tab uses — to update the leading{icon} {title}segment of the App View's own小程序 > {icon} {title}breadcrumb header. This is purely local display state, independent of the adoptedAppPanelhandle's backendtitle/icon; it resets to the manifest-declaredappView.title/iconon every reload or navigation away and back. Clicking the breadcrumb's leading小程序segment does not leave this App View — it forces the<webview>back to this same mini tool's own entry page (appView.source), discarding any in-page navigation the page had drifted into, and resets this same local title/icon override. titleis optional — omit it (recommended) so this entry inherits the mini tool's ownname/displayName, matchingappPaneland every other place the mini tool is listed. If you do set it, override per-locale viai18n/<locale>.json→appView.title, and keep it in sync with anyappPanel.titleoverride — see "Entry naming consistency" indocs/minitool-app-view.md. Follow the icon rule below before choosingicon.descriptionis optional — a longer sentence shown as the sidebar entry's tooltip. Without it, the tooltip just falls back to the mini tool's own name. Override per-locale viai18n/<locale>.json→appView.description.- Host preview & navigation stack —
window.finch.appView.openPreview(path)andopenDiff(request)open Finch's native file/Diff UI and always follow the user's global「改动与文件预览」Panel/弹窗 setting; a mini tool must not offer or pass its own presentation choice.openDiff({ type: 'files', leftPath, rightPath })compares two absolute local paths;openDiff({ type: 'git', repoPath, base, target })displays the multi-file difference between two Git commit/refs. These host previews do not add App View breadcrumb levels.openBrowser(url)andopenApp(extensionId)still push child levels onto the App View breadcrumb; anopenApptarget must declare"appView": { "embeddable": true, ... }, the stack is bounded to 3 pushed levels, blocks cycles, and destroys child state when popped. Seedocs/panel-app-navigation-stack.mdin the Finch repo for the full design.
API access
All runtime capabilities go through ctx:
ctx.tools— Agent tools; eachexecute(input, exec)call can useexec.progress.report(...)for live long-task progress. ForToolContent.image, return raw base64 indataplus a separatemimeType; never include adata:image/...;base64,prefix.ctx.composerActions— Composer toolbar contributions. The callback argument'sactions.navigationremains for compatibility but is deprecated; capturectxduringactivate()and usectx.navigationinstead.ctx.navigation.openSession(sessionId)— host-owned navigation to an existing Session in the current Finch window. This is the canonical backend API; Webview pages use the same namespace aswindow.finch.navigation.openSession(sessionId).ctx.browser.open(url)— opens a newhttp:/https:URL in Finch's built-in Browser Panel within the current Panel scope; it never invokes the system external browser.ctx.ui— native Finch dialogs, Canvas windows, native previews/Diff, and Panel Apps. Usectx.ui.openFilePreview(absolutePath)for previewable text; HTML/HTM defaults to Browser, or pass{ htmlPreview: 'code' }for source preview. Usectx.ui.openDiff({ type: 'files' | 'git', ... })for a two-file or Git-ref Diff; both need an active Panel scope, follow the user's global「改动与文件预览」Panel/弹窗 setting, need noappPanel, and never return file content to the mini tool. Declare the mini tool's one embedded app withcontributes.appPanel; Finch serves packagedlocalpages from its static server so ESM/fetch work, while publicurlpages never receive the Bridge. Open that declaration withcreatePanel({ instanceMode });singlereuses one instance in the current Panel scope andmultipleopens independently. Finch automatically sends{ type: 'finch:env', cwd, sessionId }; do not reusefinch:for business messages. Manifest toolbar clicks arrive at the page asfinch:menu, and the page controls its tab title/icon throughwindow.finch.panel. Bridge Composer writes require a page user gesture and only add removable draft contexts. AwaitCanvasWindow.ready, observestate/visiblerather than guessing lifecycle, and use atomicupdate()for mode changes. Canvas continuous animation defaults to 30 FPS; userender()+finch.canvas.invalidate()for static/event-driven content, cap high-DPI pixels withmaxDevicePixelRatio, and useCanvasWindow.startMotion()instead of frame-by-framesetPosition().showModalDialog().messagesupports standaloneimages for UI-only previews such as QR codes; use HTTPS or supported base64 image data URLs, neverToolContent.image, when the image is only for the user. The returned Modal handle remains awaitable and addsclose(action?), so background success can close the visible dialog and resolve the same action path.- Two ways to collect manual text/token input, same field grid, different lifetime. Both use the identical
MiniToolFormField[]shape (text/password/textarea/number/select/boolean/link, withrequired/secret/width/default/options), so pick based on when you need the input, not how to render it:exec.ui.requestForm(spec)(only inside a tool'sexecute(input, exec)) — pops a form card in the Composer waiting area. Tied to the running tool call; only appears while the Agent is mid-turn and actually invoked your tool. Good for "the model needs one more piece of info to finish this tool call".ctx.ui.showModalDialog({ ..., fields })(available anywhere offMiniToolContext.ui— ComposerAction handlers,sessionContainerssettings-menuexecute(), evenactivate()) — pops a native modal with the same fields plus your ownactionsbuttons. No tool call or Agent turn required. This is the right choice for "user clicks a settings button and manually types an API key/token" — it never depends on the AI deciding to call a tool. Whenfieldsis set, the firstvariant: 'primary'action is disabled until required fields are filled, and the resolvedModalDialogResult.valuescarries what the user typed.
ctx.ui.pickFile(options?)— native file picker for importing files from the current Space/workspace directory tree, reusing the sameModalShellchrome asshowModalDialog(). Requires manifestpermissions.filesystem: 'read' | 'readwrite'— calling it without that permission throws. Users can browse the lazily-loaded directory tree or fuzzy-search by name (identical scope/behavior to Composer's@file mention); the returnedFilePickerResult.filesare absolute paths only (plusrelativePath/name/isDir/spaceId/spaceName) — no file content is read or sent by the host, so read whatever you need yourself with Nodefsafter the dialog resolves.options.multipletoggles single vs. multi-select,options.filter.extensionshides non-matching files by default (directories always stay visible for browsing; a "show filtered files" toggle in the dialog's sort menu lets the user reveal them again),options.rootoverrides the default root (current Space directory, else the global workspace), andoptions.allowSpaceSwitch: trueadds a Space/workspace dropdown to the dialog header — set this only when you know the call happens from anappViewpage or another context not bound to one Session/Space, since the host cannot infer that automatically. LikeshowModalDialog(), the returnedFilePickerHandleis directly awaitable and also exposesclose()to dismiss it programmatically.- Example — a settings-menu "Configure API Key" action that never touches the Agent:
const result = await ctx.ui.showModalDialog({ title: 'Configure API Key', actions: [{ id: 'cancel', label: 'Cancel' }, { id: 'save', label: 'Save', variant: 'primary' }], fields: [{ key: 'apiKey', label: 'API Key', type: 'password', secret: true, required: true }], }); if (result.action === 'save') await ctx.secrets.set('apiKey', String(result.values?.apiKey ?? ''));
- Two ways to collect manual text/token input, same field grid, different lifetime. Both use the identical
ctx.storage— plaintext JSON for ordinary extension state only; never store API keys, tokens, passwords, or credentials herectx.secrets— manifest-authorizedget/set/deletebacked by Keychain, DPAPI, Secret Service, or KWallet; declare exact keys or a trailing wildcard inpermissions.secretsctx.oauth— isolated Authorization Code + PKCE or Device Flow login and brokered authorized requests. Standard providers ship a packaged PNG throughOAuthProviderConfig.icon; advancedinitiateAuthorization()protocol flows use their separate trustedproviderIconURL. Users only complete login, and raw tokens are never exposedctx.loggerctx.app— read Finch app info such as version/build/platform/assistantName (user-customized assistant name, e.g. "帕亚"; use it to personalize tool output)ctx.api— probe the Mini Tool API surface exposed by the current runtime withctx.api.supports('ui.createCanvasWindow'). The path is relative toMiniToolContext;truemeans the API exists, not that manifest permissions or the current Session/Panel context allow the call. KeepminVersionat least high enough to providectx.api.supports()itself, then use the probe for APIs added by later Finch versions.ctx.status— aggregated runtime status, including latest current unread session metadatactx.sessions— owner-scoped Session creation, reliable FIFO text/filesend(), live response events, cursor recovery, and race-safewaitForTurn()for one exact terminal result without sleep/polling. UseonDidReceiveEvent()for long-lived observation,waitForTurn()for request/response orchestration, andlistEvents()for history/recovery. All owner-scoped Sessions default toacceptCalls;create()may explicitly chooseask, andsetPermissionMode(sessionId, mode)persistently switches an existing owned Session betweenaskandacceptCallswithout losing history. The setter synchronizes a live Runner, never acceptsauto, and never letsacceptCallsapprove dangerous operations; those still require a human in Finch Desktop. All calls requirepermissions.sessions.create()picks one of three placements:containerId(must match a declaredcontributes.sessionContainersid),space: { spaceId }(a normal Space conversation, resolved viactx.spaces.list()— see next bullet), or neither for a plain chat Session (same as a user-created "New Chat", still owned by your mini tool);containerIdandspaceare mutually exclusive. Container titles/descriptions andstarterPromptscards supportLocalizedString; the container home shows at most four cards and sends the selected card'spromptin a new container Session. Background container Sessions use a quiet red-dot reminder instead of system notifications. Users may choose a default model per container; Finch applies it automatically to futurecreate({ containerId })calls and falls back to the global default when unset or unavailable. To give a container's Sessions a persona, declarecontributes.agentProfiles[]and point that container'sagentProfileat the profile id — required inassistantmode, optional but usually wanted ininboxmode. The binding is per container, so every Session created there carries it automatically (both the Finch UI's "New chat" entry and your owncreate({ containerId })); never pass the deprecatedcreate({ profileId }), which is ignored. Sessions created into aspace, and ordinary user conversations, never carry a profile. When a Session stops mid-turn on a permission / question / form card, the owner receivesturn.waitingwith a fullwaitsnapshot and arequestId, andturn.wait_resolvedwhen it settles;listWaits()/waitForWait()(covered bypermissions.sessions) read what is blocking, andrespondToWait()answers it — that one call additionally requirespermissions.sessionInteractions. Relay the question to your real user and pass the answer directly to the existing card instead of creating a new turn. Destructive permissions may be rejected programmatically so work can continue, but only a human in Finch may approve them. Delegated answers never writeremember; a human answering first yieldsstale, and background Sessions never produce waits at all.ctx.spaces— read-only Space directory (list()→{ id, name, alias?, directoryPath? }[]), gated by the samepermissions.sessionsasctx.sessions. Use it to discover aspaceId/name before callingctx.sessions.create({ space: { spaceId } }), without needing to already be running inside that Space. The same data is available to a staticappPanelpage with no backend tool call via the JS Bridge:window.finch.spaces.list().ctx.settingsMenu— the mini tool's one unified settings menu, rendered in container headers and in Toolcase. The manifest must declarecontributes.settingsMenu; callregister({ getMenu, execute })once and push its handle toctx.subscriptions. The static declaration reserves the button (an empty or failedgetMenu()never hides it); Finch callsgetMenu()every time it opens, passingsurfaceand, on containers,containerId. Return status rows and clickable actions as separate items (for example disabled “Status · Signed out” plus actionable “Sign in”), usectx.ui.showModalDialog()fromexecute(), and call the handle'snotifyUpdate()when background login state changes.ctx.sessionContainers— deprecated.registerSettingsMenu(containerId, { getMenu, execute })only renders inside that one container's header. Kept working for already-shipped mini tools; new mini tools usectx.settingsMenu.register()instead.ctx.i18n— put localized runtime copy ini18n/<locale>.json(zh-CN,zh-HK, oren-US) and read it withctx.i18n.t(). Keep the manifest in one default language; locale files overridename,description,systemPrompt,promptGuides, Composer action tooltips,appPanel.title,appView.title/description,sessionContainers(includingstarterPromptsby index),agentProfiles, andsettings.fieldsby stablekey. Settings overrides also support select option labels and listitemFields. These nested overrides need matching nested JSON — see the i18n JSON shape trap above.ctx.capabilities— cross-extension collaboration; seereference/capabilities.mdctx.minitool— this mini tool's own metadata (id/displayName/version/scope…). The oldctx.extensionis deprecated — usectx.minitoolin new code.ctx.minitools— snapshot of enabled mini tools' manifest contributions (listContributions(point)). The oldctx.extensionsis deprecated — usectx.minitoolsin new code.ctx.events— read-only Agent runtime event subscription (onAgentEvent), for status display or light telemetry; best-effort push, listener errors never break the Agent flowctx.notifications— Finch user-visible notification eventsctx.session— read-only snapshot of the current Sessionctx.workspace— read-only current Space / Workspace info (spaceId/spaceName/directoryPath…)ctx.storagePath— absolute path to the mini tool's private persistent storage directory, pre-created by Finch; write complex state directly with Nodefsctx.settings— settings the user configured on the mini tool detail page (declared by manifestsettingsschema), read-only; the mini tool reloads after the user savesctx.icons— runtime SVG icon pack registration (icons.register(packId, { id: { svg } })), used with manifestcontributes.iconPacks
Install and debug
Recommended flow:
- Build the mini tool.
- Run
npx @finchtoys/minitools doctor .; it rejects malformedminVersionvalues. Desktop Finch performs the authoritative current-app compatibility check. - Install with
npx @finchtoys/minitools add .. - Enable it in Finch.
- Check logs if activation fails.
Mini Tool API compatibility checklist:
- Use manifest
minVersionfor APIs the mini tool cannot run without. - For optional enhancements introduced after that baseline, call
ctx.api.supports('<ctx-relative.path>')before using them; for examplectx.api.supports('ui.createCanvasWindow'). - Do not infer API support by comparing
ctx.app.getInfo().version; capability probes avoid coupling a mini tool to Finch release history. - A successful probe does not grant permissions and does not guarantee that a Session-, Panel-, or user-gesture-dependent call is valid in the current context.
- When debugging
sessions.setPermissionMode, confirm the Session is owned by this mini tool,permissions.sessionsis granted, and the Session isinteractiveif a remote Bot expectsaskwaits. Probe optional compatibility withctx.api.supports('sessions.setPermissionMode');acceptCallsnever approves dangerous operations. - Use
ctx.capabilities.has()for capabilities provided by other mini tools; it is separate from Mini Tool API surface probing.
Panel App / App View debug checklist:
- Use
ctx.ui.openFilePreview(absolutePath)for text previews orctx.ui.openDiff({ type: 'files' | 'git', ... })for native Diff. Both are host surfaces, do not requirecontributes.appPanel, never expose file content to the mini tool, and reject calls without an active Panel scope. - Declare exactly one valid
contributes.appPanelwhen you need a right-side Panel;ctx.ui.createPanel()only opens that declaration and works in Session, Home, and other active Panel scopes. - Declare exactly one valid
contributes.appViewfor an application-level sidebar entry. It has no Composer scope, tab toolbar, or Delivery integration — but it does supportwindow.finch.panel.setTitle()/setIcon()for its own breadcrumb header (purely local page state), andctx.ui.onDidOpenPanel()+window.finch.postMessage()/onMessage()work the same as a Panel App tab. - In an App View or Panel WebView, call
window.finch.appView.openPreview(path),openBrowser(url), oropenDiff({ type: 'files' | 'git', ... })only from a real user gesture. Confirm both Panel and modal preferences: the call must follow Finch's setting and has no presentation option.openBrowser()/openApp()remain breadcrumb child navigation. - Use
source.type: 'local'for packaged UI and keep the path below the mini tool root. Finch serves it from the platform static server, neverfile://. Publicurlsources do not receive the Bridge; inline HTML is unsupported. - For local filesystem media/text in a Panel App or App View, use
finch-file://local?path=${encodeURIComponent(absolutePath)}— do not add a private localhost relay server. Confirm the path is absolute, the extension is supported, and handle403/404/415; the general-purpose Browser webview intentionally does not receive this protocol handler. - Verify toolbar actions in the page's
window.finch.onMessagehandler forfinch:menu. Update the current tab throughwindow.finch.panel.setTitle()/setIcon(). - Environment arrives through
finch:env; for a Panel App tab,sessionIdis empty outside a real Agent Session andspaceId/spaceNameare empty outside an active Space — theAppPanelhandle fromctx.ui.createPanel()/onDidOpenPanelalready carries all four so backend code doesn't need to wait for it. For an App View,viewis always'appView'andsessionId/spaceId/spaceNameare always empty;cwdis always the app's default/free workspace regardless of the active Space. - For backend-owned state, verify the page installs
window.finch.onMessagebefore sendingready/init, and that the backend replies with a complete current snapshot. Visibility events are for pausing/resuming work, not guest readiness; a navigation-time send failure must remain retryable untilonDidDispose. - Add Composer contexts only from a click or other real user gesture; never auto-submit on page load.
- Push runtime panel handles into
ctx.subscriptions, stop expensive work while hidden, and test disable/uninstall disposal. - File ranges are 1-based; image regions use normalized
0..1coordinates.
Canvas debug checklist:
- Hidden Canvas windows must stop business frames; do not add a separate timer that defeats lifecycle pausing.
- Static content should use
render()+finch.canvas.invalidate(), not an always-runningframe(). - Keep the window close to visible content and set
maxDevicePixelRatiodeliberately for large/high-DPI surfaces. - Continuous native movement uses
startMotion()/stopMotion(); do not loopsetPosition()from PluginHost. - Never allocate images, gradients,
Path2D, large arrays, or text layout inside a hotframe()callback.
i18n debug checklist (symptom: one field stays in the default/English language while the rest of the manifest localizes fine — this looks like an app bug but is almost always a locale-file shape mistake):
- Open the affected
i18n/<locale>.jsonand confirm nested manifest fields (appView,appPanel,composerActions,sessionContainers,agentProfiles,settings.fields, …) are written as nested objects, not flattened dotted keys — see §4 "i18n JSON shape trap" andreference/manifest.md§10.1. - Confirm the override key matches the manifest's stable id/key exactly (e.g.
composerActions["my-btn"].tooltip, not the button's display label or a made-up id). - i18n files are read straight from disk on every extension scan — no rebuild is needed for
i18n/*.jsonchanges, only reinstall/reload of the mini tool (disable+enable, or restart Finch) to force a rescan.
Secret-storage debug checklist:
- A password field with
secret: trueprotects the form/model boundary only; it does not persist or encrypt the value. - Every key passed to
ctx.secretsmust matchpermissions.secretsexactly or through a trailing wildcard such asservice.*. - Search generated
storage.json, settings files, logs, and tool results to confirm no credential value appears in plaintext. - When migrating an old
ctx.storagecredential, write it toctx.secretssuccessfully before deleting the old storage field. - Never fall back to
ctx.storagewhen system secure storage is unavailable; report setup failure instead.
MCP debug checklist (see reference/mcp.md §6 and §9):
- A server showing "connected" only proves
initialize/tool discovery — a failingtools/callmay still be a protocol-layer problem. - Distinguish SDK version (
@modelcontextprotocol/client2.0) from the negotiated protocol version (e.g.2026-07-28); Finch negotiates automatically viaversionNegotiation: { mode: 'auto' }and falls back for legacy servers. MCP-Protocol-Version,Mcp-Method,Mcp-Name,Mcp-Param-*are standard protocol headers generated by the transport per request — never suggest configuring them as fixedheaders; a fixedMcp-Namecannot match the request body.headers/contributes.mcpServers[].headersare only for business headers (Authorization,X-Api-Key,X-Tenant-Id).- On a
HeaderMismatcherror (-32020, HTTP 400) or a missing-Mcp-Nameerror, trace: negotiated version → request type →params.name/params.urivs sent headers → SDK version/lockfile → transport wrappers → only last, the mini tool's custom headers.
For long-running tools, verify progress and timeout behavior before publishing:
- Set
progressMode: 'indeterminate'on a tool only when it should show an initial indeterminate bar before it can report progress. Do not set it on ordinary tools. exec.progress.report({ message: 'Working…' })renders indeterminate progress.exec.progress.report({ message: 'Working…', percent: 35 })renders determinate progress.exec.progress.report({ message: 'Generating…', kind: 'image', image: { resolution: [1024, 1024] } })renders the dedicated image-generation visual (animated canvas + shimmering label) instead of the default progress bar — use it for image/video generation tools;image.resolutionis an optional[width, height]tuple shown as a badge and used to size the canvas itself (scaled to roughly the same area as the square case, so a landscape/portrait resolution renders a proportionally wider/taller canvas — matching the aicss.dev reference's own per-instance sizing), so pass the actual pixel size your model will generate rather than a fixed[1, 1].- The tool still returns one final
ToolResult; progress updates are not results. - A tool call is cut off after 2 minutes unless the tool declares
timeoutMs. Image/video generation, remote job polling, and other slow work must set it explicitly, e.g.timeoutMs: 300000. Finch clamps the value to 15 s – 10 min; a tool parameter such astimeout_secondsininputSchemadoes NOT change the platform timeout — onlytimeoutMson the tool definition does. - Prefer the hybrid pattern over blocking for the whole window: wait synchronously for a short period (60–100 s), and if the job is still running, return its task id and tell the model to query it later with a separate
status/checkaction. Blocking the full timeout freezes the turn and leaves the user with no output. - When a call does time out, Finch tells the model the work may still be running in the background and to look up the existing task instead of re-submitting. Make that possible: give every long-running tool a way to list or query the task it just created, and keep the submit path idempotent where you can.
References
reference/finch.d.ts— full API reference and type definitions.reference/README.md— detailed authoring guide and patterns.reference/tools.md— Agent tool naming, inputSchema, risk levels, forms, and common mistakes. Read this before registering any tool.reference/composer-actions.md— Composer button manifest fields, runtime providers, menu-itemhoverText, menus, and debugging rules.reference/icons.md— built-in icon list, runtime SVG packs,IconRefformat, and SVG rules. Read this before setting anyiconfield.reference/session.md— owner-scoped Sessions, containers, Space placement, events, and limits. Read this before usingctx.sessions.reference/mcp.md— local MCP server setup for on-demand tool loading; protocol-version negotiation and standard request headers (§6), and MCP troubleshooting (§9).reference/oauth.md— OAuth permissions, provider config, Authorization Code + PKCE, Device Flow, brokered requests, and security boundaries.reference/ui.md— Toast, dialog, Canvas window, Webview Panel/Bridge, Composer annotations, and native window-level guidance.reference/capabilities.md—ctx.capabilitiesprovide/get for cross-extension collaboration.reference/publish.md— packaging, npm publishing, and community listing.- Use
@finchtoys/minitool-apiin new mini tools; do not pointpathsat a local Finch repo checkout or the user's environment directory.
When you need exact fields, method signatures, examples, or edge cases, read the reference files directly. The §0 pre-flight table tells you which files apply to your feature — read them all before writing code.