Project Structure
After scaffolding, your instrument looks like this:
my-instrument/├── package.json # Manifest + dependencies├── tsconfig.json # TypeScript config├── .gitignore├── src/│ ├── index.tsx # Frontend entry point│ └── backend.ts # Backend entry point (optional)├── dist/ # Build output (git-ignored)│ ├── index.js│ └── backend.js└── tango-env.d.ts # Generated types (git-ignored)package.json — The manifest
Section titled “package.json — The manifest”The tango.instrument field is the manifest that tells Tango how to load your instrument:
{ "name": "tango-instrument-my-instrument", "version": "0.1.0", "private": true, "type": "module", "main": "dist/index.js", "scripts": { "dev": "bun node_modules/tango-api/src/cli.ts dev", "build": "bun node_modules/tango-api/src/cli.ts build", "sync": "bun node_modules/tango-api/src/cli.ts sync", "validate": "bun node_modules/tango-api/src/cli.ts validate" }, "dependencies": { "tango-api": "github:MartinGonzalez/tango-api#v0.0.1" }, "devDependencies": { "@types/react": "^18.0.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, "tango": { "instrument": { "id": "my-instrument", "name": "My Instrument", "group": "Custom", "runtime": "react", "entrypoint": "./dist/index.js", "backendEntrypoint": "./dist/backend.js", "hostApiVersion": "2.0.0", "launcher": { "sidebarShortcut": { "enabled": true, "label": "My Instrument", "icon": "puzzle", "order": 100 } }, "panels": { "sidebar": true, "first": false, "second": true, "right": false }, "permissions": ["storage.properties"] } }}Required manifest fields
Section titled “Required manifest fields”| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the instrument |
name | string | Display name shown in Tango UI |
entrypoint | string | Path to the built frontend bundle |
panels | object | Which panel slots are enabled |
Optional manifest fields
Section titled “Optional manifest fields”| Field | Type | Description |
|---|---|---|
group | string | Category for grouping in the UI |
runtime | "react" | "vanilla" | Rendering runtime (default: "react") |
backendEntrypoint | string | Path to the built backend bundle |
hostApiVersion | string | SDK version for compatibility |
permissions | string[] | Capabilities the instrument needs |
settings | array | User-configurable settings schema |
launcher | object | Sidebar shortcut configuration. Required for the instrument to appear in the sidebar. |
See the Manifest Reference for full details.
src/index.tsx — Frontend entry
Section titled “src/index.tsx — Frontend entry”This is a React component that Tango renders inside a panel. It must default-export a TangoInstrumentDefinition created by defineReactInstrument:
import { defineReactInstrument, useInstrumentApi, useHostEvent, UIRoot, UIPanelHeader, UISection, UIEmptyState,} from "tango-api";
function SidebarPanel() { const api = useInstrumentApi(); return ( <UIRoot> <UIPanelHeader title="My Instrument" subtitle="sidebar panel" /> <UISection> <UIEmptyState title="Welcome" description="Start building" /> </UISection> </UIRoot> );}
export default defineReactInstrument({ defaults: { visible: { sidebar: true, second: true }, }, panels: { sidebar: SidebarPanel, },});Subscribing to host events
Section titled “Subscribing to host events”Use useHostEvent to react to system events like stage changes, session activity, and more:
import { useHostEvent } from "tango-api";import { useCallback } from "react";
// Fires when the user switches to a different stageuseHostEvent("stage.selected", useCallback((stageInfo) => { // stageInfo.path, stageInfo.branch, stageInfo.headSha // stageInfo.hasVersionControl, stageInfo.hasChanges // stageInfo.additions, stageInfo.deletions}, []));
// Fires when the active stage's metadata refreshes (post-commit, file changes)useHostEvent("stage.updated", useCallback((stageInfo) => { // Same StageInfo payload as stage.selected}, []));
// Fires for each Claude streaming chunkuseHostEvent("session.stream", useCallback(({ sessionId, event }) => { // event: ClaudeStreamEvent}, []));See the Events & Hooks tutorial for the full list of available events.
src/backend.ts — Backend entry (optional)
Section titled “src/backend.ts — Backend entry (optional)”Runs in the host Bun process. Define actions that the frontend can call:
import { defineBackend, type InstrumentBackendContext } from "tango-api/backend";
export default defineBackend({ kind: "tango.instrument.backend.v2", actions: { hello: { input: { type: "object", properties: { name: { type: "string" } } }, output: { type: "object", properties: { greeting: { type: "string" } }, required: ["greeting"] }, handler: async (ctx: InstrumentBackendContext, input?: { name?: string }) => { return { greeting: `Hello, ${input?.name ?? "world"}!` }; }, }, },});tango-env.d.ts — Generated types
Section titled “tango-env.d.ts — Generated types”Run bun run sync to generate this file. It provides type-safe access to your instrument’s settings values. This file is regenerated on each sync and should be git-ignored.
dist/ — Build output
Section titled “dist/ — Build output”The bun run build command compiles:
src/index.tsx→dist/index.js(ESM, browser target)src/backend.ts→dist/backend.js(ESM, bun target)
Tango loads these files at runtime. You never need to touch dist/ directly.