# Agent Guide: opencode-tui-plugin-workflows

## Project Overview

A single-file [OpenCode](https://github.com/anomalyco/opencode) TUI sidebar plugin that displays the active
[workflows MCP](https://mrsimpson.github.io/responsible-vibe-mcp/) phase and workflow name.
Written in TypeScript + Solid.js JSX, consumed raw (no build step) by the
OpenCode TUI host which runs on Bun.

**Single source file:** `workflows-phase.tsx` (76 lines)

---

## Commands

### Package management

```bash
npm install          # install dependencies
```

### Build / Lint / Test

There is **no build step** — the `.tsx` source is exported directly. There are
no configured lint, format, or test commands. The plugin is validated by loading
it inside the OpenCode TUI host.

To type-check manually:

```bash
make typecheck
```

---

## Code Style

### General

- **Indentation:** 2 spaces
- **Quotes:** Single quotes for strings
- **Semicolons:** Yes
- **Line endings:** LF
- **Trailing newline:** Yes — every file ends with a newline
- **Trailing whitespace:** None

### TypeScript

- **Strict mode preferred.** Avoid `any` unless the upstream type is genuinely
  unknown (e.g., untyped event payloads). When `as any` is necessary, keep the
  cast narrow and close to the usage site.
- Use `import type` for type-only imports — never mix values and types in the
  same import statement without the `type` keyword.
- Prefer explicit generic annotations on `createSignal` and similar reactive
  primitives: `createSignal<{ phase: string } | null>(null)`.
- Use nullish coalescing `??` for fallback values; prefer optional chaining `?.`
  over nested `if`-guards for property access on potentially-null values.

### Imports

```tsx
// 1. JSX pragma (must be first line)
/** @jsxImportSource @opentui/solid */

// 2. External ESM imports — values
import { createSignal, onCleanup } from 'solid-js';

// 3. External ESM imports — types only
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';

// 4. Node built-ins (fs, path) must be required() lazily inside function
//    bodies — NOT at the top level. This avoids static import issues in the
//    Bun plugin runtime environment.
const fs = require('fs');
const path = require('path');
```

**Rule:** Never `import fs` or `import path` at the top level. Always use
`require()` inside the function that needs them.

### Naming

| Construct             | Convention          | Example               |
| --------------------- | ------------------- | --------------------- |
| Files                 | `kebab-case.tsx`    | `workflows-phase.tsx` |
| Functions             | `camelCase`         | `readLatestState`     |
| Variables / constants | `camelCase`         | `offPart`, `vibeDir`  |
| Plugin IDs            | `kebab-case` string | `'workflows-phase'`   |
| Solid signals         | `[noun, setNoun]`   | `[state, setState]`   |

### Error Handling

- Wrap all file-system operations in an **outer `try/catch`** that returns
  `null` (never throws to the caller).
- Use **empty inner `try/catch {}`** to silently skip individual files/entries
  that are unreadable — do not log these; partial failure is expected.
- The plugin must never throw. All error states should produce a `null` value
  which collapses the UI via `visible={!!state()}`.

```tsx
function readSomething(): Result | null {
  try {
    // ... filesystem operations ...
    for (const entry of entries) {
      try {
        // per-entry operation that may fail
      } catch {} // intentionally empty
    }
    return result;
  } catch {
    return null; // caller handles null gracefully
  }
}
```

### JSX / OpenTUI

- Use the `/** @jsxImportSource @opentui/solid */` pragma at the top of every
  `.tsx` file — do not rely on a `tsconfig.json` setting.
- Intrinsic OpenTUI elements: `<box>`, `<text>`, `<span>`, `<b>`.
- Pass theme colors via the `fg` prop: `fg={theme().text}`.
- Access the current theme through an accessor: `const theme = () => api.theme.current`
  (reactive — do not destructure).
- Guard visibility with `visible={!!state()}` rather than conditional rendering
  (`{state() && <box>…</box>}`) to avoid layout shift.

### Solid.js Reactive Patterns

- Create local reactive state with `createSignal`.
- Always register event-listener cleanup with `onCleanup` inside the slot
  function body to prevent memory leaks when the slot unmounts.
- Update state only when necessary — filter events by `sessionID` and tool
  name prefix before calling `setState`.

---

## Plugin Architecture

```
TuiPlugin (async function)
  └─ api.slots.register({ order: 5, slots: { sidebar_content } })
       └─ sidebar_content(ctx, props)
            ├─ reactive: createSignal for { phase, workflow } | null
            ├─ event listener: api.event.on('message.part.updated', ...)
            │    └─ filters: sessionID match + tool starts with 'workflows_'
            │    └─ calls readLatestState(api.state.path.directory)
            ├─ onCleanup: unregisters event listener
            └─ JSX: <box visible={!!state()}> ... </box>

readLatestState(sessionDir: string): { phase, workflow } | null
  └─ scans .vibe/conversations/*/state.json
  └─ picks most-recently-modified state.json
  └─ returns { phase: state.currentPhase, workflow: state.workflowName }
```

State is stored in `.vibe/conversations/<id>/state.json` with shape:

```json
{ "currentPhase": "string", "workflowName": "string" }
```

---

## Git Conventions

- **Branch:** Never commit directly to `main`. Create a feature branch first.
- **Commit messages:** Conventional Commits format.
  - `feat:` — new feature or capability
  - `fix:` — bug fix
  - `refactor:` — code restructure without behavior change
  - `chore:` — dependency updates, config, tooling
  - `docs:` — documentation only
- Show `git diff` and confirm message before committing.
- Do not push without explicit instruction.

---

## Peer Dependencies

| Package               | Role                                                       |
| --------------------- | ---------------------------------------------------------- |
| `@opencode-ai/plugin` | Plugin API types (`TuiPlugin`, `TuiPluginModule`, `api.*`) |
| `@opentui/solid`      | OpenTUI JSX runtime + intrinsic element types              |
| `solid-js`            | Reactive primitives (`createSignal`, `onCleanup`)          |

All three must be provided by the OpenCode TUI host — do not bundle them.
