diff --git a/.opencode/.gitignore b/.opencode/.gitignore index c072cfe07099..7dfdc80c68e6 100644 --- a/.opencode/.gitignore +++ b/.opencode/.gitignore @@ -5,3 +5,4 @@ bun.lock .gitignore package-lock.json references/ +artifacts/ diff --git a/.opencode/command/teach.md b/.opencode/command/teach.md new file mode 100644 index 000000000000..efadf5953a2c --- /dev/null +++ b/.opencode/command/teach.md @@ -0,0 +1,26 @@ +--- +description: Teach and onboard codebase insights, new commands, and patterns to AGENTS.md for long-term memory persistence +--- + +Analyze this session or the provided subject, and onboard the learnings and commands into `AGENTS.md` files for long-term memory persistence. + +AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible: + +- Project-wide learnings & global commands → root `AGENTS.md` +- Package/module-specific → `packages//AGENTS.md` +- Feature-specific → `src//AGENTS.md` + +What counts as a learning: +- Newly introduced commands, workflows, or CLI capabilities (e.g. `/visualize`, `opencode visualize`) +- Execution flows and non-obvious architecture relationships +- Output targets, file artifact behaviors, and default conventions +- Testing and typecheck invariants +- Core constraints and design patterns + +Process: +1. Review session for discoveries, new commands, or architectural conventions +2. Determine scope - which AGENTS.md file is responsible +3. Update the target AGENTS.md with concise, high-signal rules +4. Summarize the onboarding updates + +$ARGUMENTS diff --git a/.opencode/command/visualize.md b/.opencode/command/visualize.md new file mode 100644 index 000000000000..daed5745c4a2 --- /dev/null +++ b/.opencode/command/visualize.md @@ -0,0 +1,106 @@ +--- +description: Visualize architecture, workflows, schemas, and execution flows with interactive confirmation +--- + +You are the OpenCode Visualizer assistant. You help developers generate visual architectural topologies, sequence flows, ERD database schemas, dependency graphs, and state machines without cluttering the terminal. + +ARGUMENTS: $ARGUMENTS + +## 1. HELP FLAG CHECK (`--help`, `-h`, `help`) + +If `$ARGUMENTS` contains `--help`, `-h`, or is exactly `help`, output the visualizer manual and reference guide below immediately. Do NOT run any diagrams or diff checks when `--help` is requested. + +```text +OpenCode Visualizer (`/visualize`) +Intent-aware visual architecture, sequence flow, and schema diagramming. + +USAGE: + /visualize [target] [options] + +TARGETS: + (empty) Inspect recent conversation context or git diff to infer diagram + architecture High-level component, package, and service topology + flow Sequence / data flow for an execution path or interaction + schema Entity Relationship Diagram (ERD) from DB schema, ORM, or types + deps [package|file] Dependency and import blast-radius graph + state Finite state machine, lifecycle transitions, and edge cases + +OPTIONS: + -t, --target Rendering target destination: + browser - Ephemeral interactive HTML/Mermaid canvas + terminal - Inline Mermaid syntax block in terminal + ascii - Clean Unicode / ASCII box-drawing graph + file - Save persistent diagram file artifact + -f, --format Diagram markup format: mermaid (default), ascii, svg + -o, --output Output file path when --target=file (e.g. docs/diagrams/flow.mmd) + -y, --yes Skip the interactive confirmation loop and render immediately + -h, --help Display this help message and exit + +INTERACTIVE CONFIRMATION LOOP: + When invoked without flags (or during proactive suggestions), OpenCode prompts: + Would you like to visualize this flow? + > [1] Open interactive diagram in browser + [2] Render Mermaid directly in terminal + [3] Render Unicode / ASCII graph + [4] Export persistent file artifact + [5] Skip (No) + +EXAMPLES: + /visualize --help + /visualize + /visualize architecture + /visualize flow auth-pipeline --target=browser + /visualize schema packages/core/src/database/schema.ts + /visualize deps packages/core --format=ascii + /visualize state session-lifecycle -o docs/diagrams/session.mmd +``` + +Exit immediately after displaying the help message if `--help`, `-h`, or `help` was provided. + +--- + +## 2. WORKFLOW & EXECUTION STEPS (When not --help) + +### Step 1: Context & Target Analysis +1. If an explicit target was provided (e.g., `architecture`, `flow `, `schema `, `deps`, `state`): + - Inspect the relevant files, directories, types, or symbols using repo exploration tools. + - For `schema`, locate table definitions, relational foreign keys, or Drizzle/Prisma/SQL schemas. + - For `flow`, trace function calls, message routing, or event dispatching. + - For `architecture`, identify high-level services, packages, protocols, and boundaries. +2. If no target was provided (`/visualize` alone): + - Check the recent session messages and current `git diff`. + - Identify whether the active discussion involves high-cardinality entities, transitions, or multi-service routing. + - Summarize the inferred subject in 1-2 lines before proceeding. + +### Step 2: Interactive Confirmation Step +Unless the user explicitly specified `--yes`, `-y`, or a direct `--target` flag: +Present the interactive confirmation selection: +```text +Would you like to visualize this [architecture | sequence flow | schema | state]? +> [1] Open interactive diagram in browser (ephemeral HTML/Mermaid canvas) + [2] Render Mermaid directly in terminal + [3] Render Unicode / ASCII box-drawing graph + [4] Save persistent file artifact (.mmd / .svg) + [5] Skip (No) +``` +Wait for user selection or proceed if they have already expressed their choice. + +### Step 3: Diagram Generation by Target Type + +#### If Option [1] / `browser`: +- Generate a standalone self-contained HTML file (e.g., in `.opencode/artifacts/` or system temp). +- Include Mermaid.js via CDN (`https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js`), initialized with a dark theme (`theme: 'dark'`) matching OpenCode aesthetics (bg `#121214`, accent `#6366f1`, smooth rounded borders). +- Include pan, zoom, and SVG export controls. +- Provide a clickable link `file://` or instruct to open in browser. + +#### If Option [2] / `terminal`: +- Output a clean, syntax-highlighted ````mermaid ... ```` block. +- Keep the syntax concise and readable with meaningful node labels and clear directionality (`graph TD`, `sequenceDiagram`, `erDiagram`, or `stateDiagram-v2`). + +#### If Option [3] / `ascii`: +- Output a clean Unicode box-drawing diagram using characters `┌─┐`, `└─┘`, `│`, `▼`, `──►`, `▲`. +- Ideal for quick terminal glance without markdown rendering engines. + +#### If Option [4] / `file`: +- Write the diagram specification to the requested path (default: `docs/diagrams/.mmd` or `.svg`). +- Confirm the written file path and provide a link. diff --git a/AGENTS.md b/AGENTS.md index cd2327e88811..716e2c5a555e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -159,3 +159,10 @@ const table = sqliteTable("session", { - Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once. - Keep EventV2 replay owner claims separate from clustered Session execution ownership. - Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned. + +## Visualizer Commands + +- Use `opencode visualize [target] [subject]` (or `/visualize` in TUI sessions) to diagram architecture, execution flows, ERD schemas, dependencies, state machines, or System Context. +- Supported targets: `context` (OpenCode Session Runtime & System Context pipeline), `architecture` (package/layer topology), `flow ` (sequence diagrams), `schema ` (database ERD), `deps` (workspace dependencies), `state` (Session drain/turn lifecycle), and empty target (inferred from `git status` diff). +- When running in CLI mode, an interactive self-contained HTML artifact (`.opencode/artifacts/visualize--.html`) is always generated and presented as clickable output (`file:///...`) to the end-user alongside any terminal rendering. +- Rendering destinations: `browser` (opens interactive dark-mode Mermaid canvas with pan/zoom and SVG export), `terminal` (inline Mermaid markdown block), `ascii` (Unicode box-drawing), and `file` (writes `.mmd` / `.svg` to disk). diff --git a/packages/opencode/src/cli/cmd/visualize.ts b/packages/opencode/src/cli/cmd/visualize.ts new file mode 100644 index 000000000000..7379e842ab5d --- /dev/null +++ b/packages/opencode/src/cli/cmd/visualize.ts @@ -0,0 +1,756 @@ +import { Effect } from "effect" +import { UI } from "../ui" +import { effectCmd } from "../effect-cmd" +import { intro, select, isCancel, outro, log } from "@clack/prompts" +import open from "open" +import path from "path" +import os from "os" + +interface VisualizeArgs { + target?: string + subject?: string + target_dest?: string + format?: string + output?: string + yes?: boolean +} + +export const VisualizeCommand = effectCmd({ + command: "visualize [target] [subject]", + describe: "visualize architecture, execution flows, schemas, or session context", + instance: false, + builder: (yargs) => + yargs + .positional("target", { + describe: "visualizer target: context, architecture, flow, schema, deps, state", + type: "string", + }) + .positional("subject", { + describe: "specific module, function, or path to diagram (e.g. auth-pipeline, @models)", + type: "string", + }) + .option("dest", { + alias: "t", + describe: "rendering target destination: browser, terminal, ascii, file", + type: "string", + choices: ["browser", "terminal", "ascii", "file"], + }) + .option("format", { + alias: "f", + describe: "diagram markup format: mermaid, ascii, svg, html", + type: "string", + choices: ["mermaid", "ascii", "svg", "html"], + default: "mermaid", + }) + .option("output", { + alias: "o", + describe: "output file path when saving to disk", + type: "string", + }) + .option("yes", { + alias: "y", + describe: "skip interactive confirmation prompt and render immediately", + type: "boolean", + default: false, + }), + handler: Effect.fn("Cli.visualize")(function* (args) { + const rawTarget = args.target ? String(args.target).toLowerCase() : "" + const subject = args.subject ? String(args.subject) : "" + + const resolved = resolveTargetDiagram(rawTarget, subject) + const explicitDest = args.dest ? String(args.dest).toLowerCase() : undefined + + let chosenDest = explicitDest + if (!args.yes && !explicitDest) { + UI.empty() + intro(UI.Style.TEXT_HIGHLIGHT_BOLD + "OpenCode Visualizer" + UI.Style.TEXT_NORMAL) + const promptResult = yield* Effect.promise(() => + select({ + message: `Would you like to visualize ${resolved.title}?`, + options: [ + { label: "Open interactive diagram in browser (Mermaid canvas)", value: "browser" }, + { label: "Render Mermaid directly in terminal", value: "terminal" }, + { label: "Render Unicode / ASCII box-drawing graph", value: "ascii" }, + { label: "Export persistent file artifact", value: "file" }, + { label: "Skip (No)", value: "skip" }, + ], + }), + ) + + if (isCancel(promptResult) || promptResult === "skip") { + outro(UI.Style.TEXT_DIM + "Visualization cancelled." + UI.Style.TEXT_NORMAL) + return + } + chosenDest = String(promptResult) + } + + const finalDest = chosenDest || "browser" + + // When running in CLI, always generate the interactive HTML file and present as output to end user + const artifactDir = path.join(process.cwd(), ".opencode", "artifacts") + const fallbackDir = os.tmpdir() + const hasPkg = yield* Effect.promise(() => Bun.file(path.join(process.cwd(), "package.json")).exists()) + const targetDir = hasPkg ? artifactDir : fallbackDir + const htmlFile = path.join(targetDir, `visualize-${resolved.key}-${Date.now()}.html`) + const htmlContent = buildInteractiveHtml(resolved.title, resolved.mermaid) + + yield* Effect.promise(() => Bun.write(htmlFile, htmlContent)) + const fileUri = "file:///" + htmlFile.replace(/\\/g, "/") + + if (finalDest === "browser") { + UI.empty() + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✔ " + UI.Style.TEXT_NORMAL + "Generated interactive HTML diagram") + UI.println(UI.Style.TEXT_INFO_BOLD + " Output File: " + UI.Style.TEXT_NORMAL + htmlFile) + UI.println(UI.Style.TEXT_INFO_BOLD + " Browser View: " + UI.Style.TEXT_NORMAL + fileUri) + UI.empty() + yield* Effect.promise(() => open(htmlFile).catch(() => { })) + return + } + + if (finalDest === "ascii") { + UI.empty() + UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + `── ${resolved.title} (ASCII / Unicode) ──` + UI.Style.TEXT_NORMAL) + UI.empty() + UI.println(resolved.ascii) + UI.empty() + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✔ " + UI.Style.TEXT_NORMAL + "Interactive HTML diagram presented:") + UI.println(UI.Style.TEXT_INFO_BOLD + " Output File: " + UI.Style.TEXT_NORMAL + htmlFile) + UI.println(UI.Style.TEXT_INFO_BOLD + " Browser Link: " + UI.Style.TEXT_NORMAL + fileUri) + UI.empty() + return + } + + if (finalDest === "file") { + const outputPath = args.output || path.join("docs", "diagrams", `${resolved.key}.mmd`) + yield* Effect.promise(() => Bun.write(outputPath, resolved.mermaid)) + UI.empty() + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✔ " + UI.Style.TEXT_NORMAL + "Saved diagram artifact: " + outputPath) + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✔ " + UI.Style.TEXT_NORMAL + "Interactive HTML diagram presented:") + UI.println(UI.Style.TEXT_INFO_BOLD + " Output File: " + UI.Style.TEXT_NORMAL + htmlFile) + UI.println(UI.Style.TEXT_INFO_BOLD + " Browser Link: " + UI.Style.TEXT_NORMAL + fileUri) + UI.empty() + return + } + + // Inline terminal + UI.empty() + UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + `── ${resolved.title} (Mermaid) ──` + UI.Style.TEXT_NORMAL) + UI.println("```mermaid") + UI.println(resolved.mermaid) + UI.println("```") + UI.empty() + UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✔ " + UI.Style.TEXT_NORMAL + "Interactive HTML diagram presented:") + UI.println(UI.Style.TEXT_INFO_BOLD + " Output File: " + UI.Style.TEXT_NORMAL + htmlFile) + UI.println(UI.Style.TEXT_INFO_BOLD + " Browser Link: " + UI.Style.TEXT_NORMAL + fileUri) + UI.empty() + }), +}) + +export interface DiagramPayload { + key: string + title: string + mermaid: string + ascii: string +} + +export function resolveTargetDiagram(target: string, subject: string): DiagramPayload { + if (target === "context" || target === "system-context") { + return { + key: "context", + title: "OpenCode Session Runtime & System Context", + mermaid: `flowchart TD + subgraph Sources["Context Sources (Location-Scoped)"] + S1["Instruction Context (AGENTS.md, Rules)"] + S2["Skills & Custom Guidance"] + S3["Tools & Location Registry"] + S4["Environment & Git Metadata"] + end + + subgraph Registry["System Context Registry"] + SCR["Location-Scoped Registry"] + end + + subgraph SessionRuntime["Session Execution Engine"] + EP["Context Epoch (Immutable Cache Baseline)"] + DIFF["Safe Provider-Turn Boundary Check"] + MID["Mid-Conversation System Message"] + HIST["Session History (Promoted Inputs + Messages)"] + end + + subgraph ProviderTurn["Model Execution"] + LLM["Provider Turn (LLM Stream)"] + DRAIN["Session Drain (Process-Local)"] + end + + Sources --> SCR + SCR --> EP + EP --> DIFF + DIFF -->|Context Changed| MID + MID --> HIST + DIFF -->|No Change| HIST + HIST --> LLM + LLM --> DRAIN`, + ascii: `┌─────────────────────────────────────────────────────────────┐ +│ CONTEXT SOURCES │ +│ [AGENTS.md] [Skills & Plugins] [Tools] [Environment] │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SYSTEM CONTEXT REGISTRY │ +│ Assembles stable-keyed scoped context contributions │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CONTEXT EPOCH (Provider Cache Baseline) │ +│ Maintains immutable baseline until session compaction │ +└──────────────────────────────┬──────────────────────────────┘ + │ + [Safe Provider-Turn Boundary] + ┌──────────────┴─────────────┐ + │ Changed? │ + ▼ (Yes) ▼ (No) + ┌──────────────────────────┐ ┌──────────────────────────┐ + │ Mid-Conversation Message │ │ Existing Baseline Cached │ + └────────────┬─────────────┘ └─────────────┬────────────┘ + └──────────────┬─────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SESSION HISTORY & PROVIDER TURN │ +│ llm.stream(request) ──► Model Tool Output ──► Next Turn │ +└─────────────────────────────────────────────────────────────┘`, + } + } + + if (target === "architecture") { + return { + key: "architecture", + title: "OpenCode System Architecture Topology", + mermaid: `flowchart TD + subgraph ClientLayer["Presentation & Clients"] + TUI["OpenCode TUI (@opencode-ai/tui)"] + CLI["OpenCode CLI (packages/opencode)"] + WEB["OpenCode Web / Desktop"] + SDK["SDK & Generated Client"] + end + + subgraph ServerLayer["Server & Network"] + SERVER["HttpApi Server (@opencode-ai/server)"] + ROUTER["Router & In-Memory Transport"] + end + + subgraph CoreLayer["Runtime Core (@opencode-ai/core)"] + SESSION["SessionV2 & Execution Coordinator"] + SYSCTX["System Context & Sources"] + PLUGIN["Plugin & Command Host"] + STORE["Session & Database Store (SQLite)"] + end + + subgraph ProtocolLayer["Protocols & Contracts"] + PROTO["@opencode-ai/protocol"] + SCHEMA["@opencode-ai/schema"] + end + + TUI --> CLI + CLI --> SERVER + WEB --> SERVER + SDK --> PROTO + SERVER --> CoreLayer + CoreLayer --> ProtocolLayer`, + ascii: `┌─────────────────────────────────────────────────────────────┐ +│ PRESENTATION LAYER │ +│ [OpenCode TUI] [OpenCode CLI] [Desktop / Web] │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SERVER & PROTOCOL ADAPTERS │ +│ HttpApi Server ── Generated Client Transport │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ CORE RUNTIME ENGINE │ +│ Session Execution │ System Context │ Plugin Host │ +│ Durable Storage │ Tool Registry │ Location Scope │ +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ SCHEMA & PROTOCOLS │ +│ Drizzle ORM Tables │ Effect Schema │ +└─────────────────────────────────────────────────────────────┘`, + } + } + + if (target === "schema") { + return { + key: "schema", + title: "Data Models & Schema Relationships (ERD)", + mermaid: `erDiagram + PROJECT ||--o{ SESSION : owns + SESSION ||--o{ MESSAGE : contains + SESSION ||--o{ SESSION_INPUT : queues + SESSION ||--o{ CONTEXT_EPOCH : tracks + MESSAGE ||--o{ MESSAGE_PART : composed_of + + PROJECT { + string id PK + string directory + integer created_at + } + SESSION { + string id PK + string project_id FK + string location + integer created_at + } + SESSION_INPUT { + string id PK + string session_id FK + string mode + string content + } + MESSAGE { + string id PK + string session_id FK + string role + integer sequence + } + MESSAGE_PART { + string id PK + string message_id FK + string type + string text + }`, + ascii: `┌──────────────┐ 1..n ┌──────────────┐ +│ PROJECT │ ───────────── │ SESSION │ +│ id (PK) │ │ id (PK) │ +│ directory │ │ project_id │ +└──────────────┘ └──────┬───────┘ + │ 1..n + ┌────────────────────────────┼────────────────────────────┐ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ SESSION_INPUT │ │ MESSAGE │ │ CONTEXT_EPOCH │ +│ id (PK) │ │ id (PK) │ │ id (PK) │ +│ mode (admitted) │ │ role │ │ snapshot_json │ +└──────────────────┘ └────────┬─────────┘ └──────────────────┘ + │ 1..n + ▼ + ┌──────────────────┐ + │ MESSAGE_PART │ + │ id (PK) │ + │ type, content │ + └──────────────────┘`, + } + } + + if (target === "flow") { + const flowTitle = subject ? `Execution Flow: ${subject}` : "Prompt Admission & Execution Flow" + return { + key: "flow", + title: flowTitle, + mermaid: `sequenceDiagram + autonumber + actor Dev as Developer + participant Inbox as Durable Session Inbox + participant Coord as SessionRunCoordinator + participant Runner as Location SessionRunner + participant LLM as Provider Turn (Stream) + + Dev->>Inbox: Submit Prompt (SessionV2.prompt) + Inbox->>Coord: Advisory Wake (SessionExecution.wake) + Coord->>Runner: Coordinate Placement & Lock + Runner->>Runner: Safe Provider-Turn Boundary Check + Runner->>LLM: Stream Provider Request (llm.stream) + LLM-->>Runner: Yield Response Chunks & Tool Calls + Runner-->>Dev: Real-time TUI Projection + Runner->>Inbox: Durable Completion & State Update`, + ascii: `Developer Durable Inbox Coordinator SessionRunner LLM Provider + │ │ │ │ │ + ├─ 1. Submit Prompt ─► │ │ │ + │ ├─ 2. Advisory Wake─► │ │ + │ │ ├─ 3. Acquire Lock ─► │ + │ │ │ ├─ 4. Boundary Check│ + │ │ │ ├─ 5. llm.stream ───► + │ │ │ │ │ + │ │ │ │◄── 6. Stream Chunk─┤ + │◄── 7. Projected UI ────────────────────────────────────────┤ │ + │ │ │ │ │ + │ │ │ ├─ 8. Settle Tools ─┤ + │ │◄── 9. State Saved ────────────────────┤ │`, + } + } + + if (target === "deps") { + return { + key: "deps", + title: "Workspace Package Dependency Graph", + mermaid: `flowchart LR + CLI["packages/opencode"] + TUI["packages/tui"] + SERVER["packages/server"] + CORE["packages/core"] + PROTO["packages/protocol"] + SCHEMA["packages/schema"] + + CLI --> SERVER + TUI --> CLI + SERVER --> CORE + SERVER --> PROTO + CORE --> PROTO + CORE --> SCHEMA + PROTO --> SCHEMA`, + ascii: `[packages/tui] ──► [packages/opencode] ──► [packages/server] + │ + ▼ + [packages/core] + │ + ┌─────────────────┴─────────────────┐ + ▼ ▼ + [packages/protocol] ────────────────► [packages/schema]`, + } + } + + if (target === "state") { + return { + key: "state", + title: "Session Execution & Drain State Machine", + mermaid: `stateDiagram-v2 + [*] --> Idle + Idle --> AdmittingPrompt : User Input Received + AdmittingPrompt --> PendingInInbox : Durable Row Persisted + PendingInInbox --> RunningDrain : Advisory Wakeup + RunningDrain --> EvaluatingBoundary : Next Provider Turn + EvaluatingBoundary --> StreamingResponse : Boundary Safe + StreamingResponse --> SettlingTools : Tool Call Requested + SettlingTools --> EvaluatingBoundary : Tool Output Ready + StreamingResponse --> RunningDrain : Turn Complete + RunningDrain --> Idle : Inbox Drained & Complete + RunningDrain --> Interrupted : User Interrupt + Interrupted --> Idle : Reset Ownership Chain`, + ascii: ` [*] + │ + ▼ +┌───────────┐ Input ┌──────────────────┐ +│ Idle ├─────────────────►│ Admitting Prompt │ +└─────▲─────┘ └────────┬─────────┘ + │ │ + │ All Drained ▼ +┌─────┴──────────┐ Wakeup ┌──────────────────┐ +│ Running Drain │◄────────────┤ Pending In Inbox │ +└─────┬──────────┘ └──────────────────┘ + │ + ├───────────────────────┬────────────────────────┐ + ▼ ▼ ▼ +┌───────────┐ ┌───────────┐ ┌───────────┐ +│ Streaming │ │ Settling │ │ Interupt │ +│ Response │ │ Tools │ │ Chain │ +└───────────┘ └───────────┘ └───────────┘`, + } + } + + // Default: inspect git status / context + const diffResult = Bun.spawnSync(["git", "status", "--short"], { stdout: "pipe" }) + const statusText = diffResult.stdout.toString().trim() + + if (statusText.length > 0) { + const lines = statusText.split("\n").slice(0, 8) + const formattedFiles = lines + .map((l, i) => ` F${i}["${l.trim().replace(/["\\]/g, "")}"]`) + .join("\n") + + return { + key: "diff-impact", + title: "Current Working Tree Change Impact", + mermaid: `flowchart TD + subgraph WorkingTree["Active Modified Files"] +${formattedFiles} + end + + subgraph Impact["Validation & Pipeline Impact"] + TYPE["Typecheck (bun typecheck)"] + TEST["Test Suite (bun test)"] + RUNTIME["Runtime & TUI Behavior"] + end + + WorkingTree --> TYPE + WorkingTree --> TEST + TYPE --> RUNTIME + TEST --> RUNTIME`, + ascii: `┌─────────────────────────────────────────────────────────────┐ +│ ACTIVE WORKING TREE CHANGES │ +│${lines.map((l) => " " + l.trim()).join("\n")} +└──────────────────────────────┬──────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ VERIFICATION & IMPACT CHAIN │ +│ bun typecheck ──► bun test ──► Runtime / TUI │ +└─────────────────────────────────────────────────────────────┘`, + } + } + + // If clean, default to OpenCode System Context + return resolveTargetDiagram("context", "") +} + +export function buildInteractiveHtml(title: string, mermaidCode: string): string { + return ` + + + + + ${escapeHtml(title)} - OpenCode Visualizer + + + + + + + +
+
+ OpenCode +

${escapeHtml(title)}

+
+
+ + + + +
+
+ +
+
+
+${mermaidCode} +
+
+
+ +
+ Tip: Click and drag to pan • Scroll to zoom + Rendered with Mermaid.js +
+ + + +` +} + +function escapeHtml(str: string): string { + return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """) +} + diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 13540a73a36f..b2cab2bea7b4 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -26,6 +26,7 @@ import { WebCommand } from "./cli/cmd/web" import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" +import { VisualizeCommand } from "./cli/cmd/visualize" import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" @@ -101,6 +102,7 @@ const cli = yargs(args) .command(SessionCommand) .command(PluginCommand) .command(DbCommand) + .command(VisualizeCommand) .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/opencode/test/cli/visualize.test.ts b/packages/opencode/test/cli/visualize.test.ts new file mode 100644 index 000000000000..a7286ecadadf --- /dev/null +++ b/packages/opencode/test/cli/visualize.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "bun:test" +import { resolveTargetDiagram, buildInteractiveHtml } from "../../src/cli/cmd/visualize" + +describe("visualize command", () => { + it("resolves context target with System Context pipeline", () => { + const res = resolveTargetDiagram("context", "") + expect(res.key).toBe("context") + expect(res.mermaid).toContain("flowchart TD") + expect(res.mermaid).toContain("Context Sources") + expect(res.mermaid).toContain("System Context Registry") + expect(res.mermaid).toContain("Context Epoch") + expect(res.ascii).toContain("CONTEXT SOURCES") + expect(res.ascii).toContain("SYSTEM CONTEXT REGISTRY") + }) + + it("resolves architecture target with layers", () => { + const res = resolveTargetDiagram("architecture", "") + expect(res.key).toBe("architecture") + expect(res.mermaid).toContain("ClientLayer") + expect(res.mermaid).toContain("ServerLayer") + expect(res.mermaid).toContain("CoreLayer") + expect(res.ascii).toContain("PRESENTATION LAYER") + }) + + it("resolves schema target with entity relationship diagram", () => { + const res = resolveTargetDiagram("schema", "") + expect(res.key).toBe("schema") + expect(res.mermaid).toContain("erDiagram") + expect(res.mermaid).toContain("PROJECT") + expect(res.mermaid).toContain("SESSION") + expect(res.ascii).toContain("PROJECT") + expect(res.ascii).toContain("SESSION") + }) + + it("resolves execution flow target with sequence diagram", () => { + const res = resolveTargetDiagram("flow", "auth-pipeline") + expect(res.key).toBe("flow") + expect(res.title).toContain("auth-pipeline") + expect(res.mermaid).toContain("sequenceDiagram") + expect(res.ascii).toContain("Developer") + }) + + it("resolves dependency graph target", () => { + const res = resolveTargetDiagram("deps", "") + expect(res.key).toBe("deps") + expect(res.mermaid).toContain("flowchart LR") + expect(res.ascii).toContain("packages/core") + }) + + it("resolves state machine target", () => { + const res = resolveTargetDiagram("state", "") + expect(res.key).toBe("state") + expect(res.mermaid).toContain("stateDiagram-v2") + expect(res.ascii).toContain("Running Drain") + }) + + it("builds self-contained interactive HTML with Mermaid and pan-zoom controls", () => { + const html = buildInteractiveHtml("Test Diagram", "flowchart TD\n A --> B") + expect(html).toContain("") + expect(html).toContain("mermaid.initialize") + expect(html).toContain("Download SVG") + expect(html).toContain("zoomIn()") + expect(html).toContain("Test Diagram") + expect(html).toContain("A --> B") + }) +}) diff --git a/specs/rfc-visualize-command.md b/specs/rfc-visualize-command.md new file mode 100644 index 000000000000..cfdbd2ca8c10 --- /dev/null +++ b/specs/rfc-visualize-command.md @@ -0,0 +1,140 @@ +# RFC: `/visualize` Command & Interactive Confirmation Loop + +- **Author**: Feature Proposal +- **Target**: `anomalyco/opencode` (TUI, Core, Commands) +- **Status**: Draft / Proposed + +--- + +## 1. Problem Statement + +Textual explanations of complex workflows, distributed pipelines, and schemas often overload the terminal context and dramatically increase cognitive load. Conversely, blindly dumping massive ASCII schemas or 150+ lines of raw Mermaid markup into the terminal when a developer only needed a quick summary produces distracting noise and scroll fatigue. + +In CLI and TUI coding agents, developers face an all-or-nothing dichotomy: +- **Wall of prose**: Difficulty conceptualizing complex multi-service flows, state machine transitions, or database relationships. +- **Unsolicited markup dumps**: Terminal clutter when raw diagram syntax is emitted without checking user intent or terminal capabilities. + +## 2. Proposed Solution + +Introduce a native `/visualize` command paired with an **ambient, intent-aware confirmation loop**. + +1. **Explicit Invocation (`/visualize`)**: Direct command execution by the user with flexible targets (e.g. `/visualize architecture`, `/visualize flow `, `/visualize schema `). +2. **Ambient Proactive Confirmation**: When standard conversations detect multi-hop routing, high-cardinality state transitions, or relational schemas, the agent offers a non-intrusive interactive confirmation prompt before rendering. +3. **Multi-Target Rendering**: Flexible outputs supporting inline terminal Mermaid, clean Unicode/ASCII flowcharts, browser-based interactive canvas (zoom/pan), or persistent committed file artifacts. + +--- + +## 3. Workflow Specification + +```mermaid +flowchart TD + A[Invocation] -->|Explicit /visualize| B[Inspect Target / Diff / Context] + A -->|Proactive Agent Detection| B + B --> C{Flags Provided?} + C -->|--help / -h| D[Display visualizer manual & exit] + C -->|--yes / -y or direct target| E[Render Diagram to Target] + C -->|No explicit flags| F["Interactive Confirmation Picker + [1] Browser Canvas + [2] Terminal Mermaid + [3] ASCII Box-Drawing + [4] File Artifact + [5] Skip"] + F -->|Option [1]| G[Open Ephemeral Browser View] + F -->|Option [2]| H[Inline Mermaid Block in TUI] + F -->|Option [3]| I[Inline Unicode/ASCII Graph] + F -->|Option [4]| J[Write .mmd / .svg File] + F -->|Option [5]| K[Abort / Continue Chat] +``` + +### Step 1: Context & Entity Detection +- For explicit invocations: Agent scans specified modules, ORM models, or git diffs. +- For ambient triggers: Triggered only when cardinality threshold is reached (e.g., ≥3 services interacting, ≥4 state transitions, or ≥3 relational models). + +### Step 2: Interactive Terminal Confirmation Picker +The agent prompts the user with an interactive terminal selection: +```text +Would you like to visualize this flow? +> [1] Open interactive sequence diagram (Browser) + [2] Render Mermaid directly in terminal + [3] Render Unicode / ASCII graph + [4] Export persistent file artifact + [5] Skip (No) +``` + +### Step 3: Destination Dispatch +- **Browser Popout**: Spins up an ephemeral local server (`http://localhost:`) or creates a self-contained HTML artifact opened via the default browser (`open` / `xdg-open` / `start`). Uses Mermaid.js or D3 with dark-mode aesthetic, pan/zoom, and SVG export. +- **Terminal Mermaid**: Renders ````mermaid ... ```` directly in the TUI stream for terminals with native markdown/diagram support. +- **Unicode/ASCII**: Renders box-drawing flowcharts using standard Unicode glyphs (`┌─┐`, `└─┘`, `│`, `▼`, `──►`). +- **File Artifact**: Writes diagram source or SVG directly to disk (e.g., `docs/diagrams/auth-pipeline.mmd`). + +--- + +## 4. Supported Diagram Types & Heuristics + +| Diagram Type | Best Trigger Context | Primary Target | +| :--- | :--- | :--- | +| **Sequence Diagram** | Multi-service calls, async message passing, auth flows | Trace execution across services or functions | +| **Entity Relationship (ERD)** | DB migrations, ORM schemas, type definitions | Visualizing table relations and foreign keys | +| **Dependency / Import Graph** | Refactoring, circular dependency checks | Package/module hierarchy and blast radius | +| **State Machine Diagram** | Finite state machines, checkout/order lifecycles | Visualizing states, transitions, and edge cases | + +--- + +## 5. Implementation Strategy + +1. **Phase 1 (Custom Command)**: + - Shipped via `.opencode/command/visualize.md` (or `.opencode/commands/visualize.md`). + - Supports `$ARGUMENTS`, parsing `--help`, targets (`architecture`, `flow`, `schema`, `deps`, `state`), and output formats. +2. **Phase 2 (Core / TUI Integration)**: + - Register `/visualize` as a first-class built-in command in `@opencode-ai/core/plugin/command.ts`. + - Add native TUI component for interactive single-key selection `[1-5]`. + - Ephemeral HTTP or static HTML viewer in OpenCode Desktop / Webview. + +--- + +## 6. Command Reference (`visualize --help`) + +```text +OpenCode Visualizer (`/visualize`) +Intent-aware visual architecture, sequence flow, and schema diagramming. + +USAGE: + /visualize [target] [options] + +TARGETS: + (empty) Inspect recent conversation context or git diff to infer diagram + architecture High-level component, package, and service topology + flow Sequence / data flow for an execution path or interaction + schema Entity Relationship Diagram (ERD) from DB schema, ORM, or types + deps [package|file] Dependency and import blast-radius graph + state Finite state machine, lifecycle transitions, and edge cases + +OPTIONS: + -t, --target Rendering target destination: + browser - Ephemeral interactive HTML/Mermaid canvas + terminal - Inline Mermaid syntax block in terminal + ascii - Clean Unicode / ASCII box-drawing graph + file - Save persistent diagram file artifact + -f, --format Diagram markup format: mermaid (default), ascii, svg + -o, --output Output file path when --target=file (e.g. docs/diagrams/flow.mmd) + -y, --yes Skip the interactive confirmation loop and render immediately + -h, --help Display this help message and exit + +INTERACTIVE CONFIRMATION LOOP: + When invoked without flags (or during proactive suggestions), OpenCode prompts: + Would you like to visualize this flow? + > [1] Open interactive diagram in browser + [2] Render Mermaid directly in terminal + [3] Render Unicode / ASCII graph + [4] Export persistent file artifact + [5] Skip (No) + +EXAMPLES: + /visualize --help + /visualize + /visualize architecture + /visualize flow auth-pipeline --target=browser + /visualize schema packages/core/src/database/schema.ts + /visualize deps packages/core --format=ascii + /visualize state session-lifecycle -o docs/diagrams/session.mmd +```