Browse Source

Remove Divoom display integration

Alvin Unreal 1 month ago
parent
commit
1d427bdcec

+ 1 - 2
README.ja-JP.md

@@ -506,8 +506,7 @@ ping all agents
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |
 | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |
-| **[Interview](docs/interview.md)** | ブラウザベースの Q&A フローで、ざっくりとしたアイデアを構造化された Markdown 仕様に変換します |
-| **[Divoom Display](docs/divoom.md)** | Orchestrator および専門エージェントの動作を Divoom MiniToo Bluetooth ディスプレイにミラーリングします |
+| **[Interview](docs/interview.md)** | ブラウザベース의 Q&A フローで、ざっくりとしたアイデアを構造化された Markdown 仕様に変換します |
 
 
 ### ⚙️ 設定 & リファレンス
 ### ⚙️ 設定 & リファレンス
 
 

+ 0 - 1
README.ko-KR.md

@@ -504,7 +504,6 @@ ping all agents
 | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
 | **[Interview](docs/interview.md)** | 브라우저 기반 Q&A 흐름을 통해 거친 아이디어를 구조화된 마크다운 명세로 변환 |
 | **[Interview](docs/interview.md)** | 브라우저 기반 Q&A 흐름을 통해 거친 아이디어를 구조화된 마크다운 명세로 변환 |
-| **[Divoom Display](docs/divoom.md)** | 오케스트레이터와 전문 에이전트 활동을 Divoom MiniToo 블루투스 디스플레이에 미러링 |
 
 
 ### ⚙️ 설정 & 레퍼런스
 ### ⚙️ 설정 & 레퍼런스
 
 

+ 0 - 1
README.md

@@ -575,7 +575,6 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
 | **[Interview](docs/interview.md)** | Turn rough ideas into a structured markdown spec through a browser-based Q&A flow |
-| **[Divoom Display](docs/divoom.md)** | Mirror orchestrator and specialist-agent activity to a Divoom MiniToo Bluetooth display |
 | **[Companion](docs/companion.md)** | Floating window companion for parsing, help, and types |
 | **[Companion](docs/companion.md)** | Floating window companion for parsing, help, and types |
 
 
 ### ⚙️ Config & Reference
 ### ⚙️ Config & Reference

+ 0 - 1
README.zh-CN.md

@@ -502,7 +502,6 @@ ping all agents
 | **[代码地图 (Codemap) (docs/codemap.md)](docs/codemap.md)** | 生成层级代码地图,快速理解大型代码库 |
 | **[代码地图 (Codemap) (docs/codemap.md)](docs/codemap.md)** | 生成层级代码地图,快速理解大型代码库 |
 | **[克隆依赖 (Clonedeps) (docs/clonedeps.md)](docs/clonedeps.md)** | 将选定的依赖源码克隆到被忽略的本地工作区中以供检查 |
 | **[克隆依赖 (Clonedeps) (docs/clonedeps.md)](docs/clonedeps.md)** | 将选定的依赖源码克隆到被忽略的本地工作区中以供检查 |
 | **[访谈式生成 (Interview) (docs/interview.md)](docs/interview.md)** | 通过基于浏览器的问答流,将粗糙的想法转变为结构化的 Markdown 规范文档 |
 | **[访谈式生成 (Interview) (docs/interview.md)](docs/interview.md)** | 通过基于浏览器的问答流,将粗糙的想法转变为结构化的 Markdown 规范文档 |
-| **[Divoom 显示屏 (docs/divoom.md)](docs/divoom.md)** | 将编排者与专家智能体的活动镜像显示到 Divoom MiniToo 蓝牙显示屏上 |
 
 
 ### ⚙️ 配置与参考
 ### ⚙️ 配置与参考
 
 

+ 0 - 74
docs/configuration.md

@@ -111,15 +111,6 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical`. Tmux applies full layouts; Zellij maps `main-vertical` to right and `main-horizontal` to down |
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical`. Tmux applies full layouts; Zellij maps `main-vertical` to right and `main-horizontal` to down |
 | `multiplexer.main_pane_size` | number | `60` | Main pane size as percentage (20–80) for tmux main layouts; ignored by Zellij |
 | `multiplexer.main_pane_size` | number | `60` | Main pane size as percentage (20–80) for tmux main layouts; ignored by Zellij |
 | `multiplexer.zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `agent-tab` creates/reuses a dedicated `opencode-agents` tab; `current-tab` opens subagents as panes in the tab containing the parent OpenCode pane, falling back to the focused tab if the parent pane cannot be resolved |
 | `multiplexer.zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `agent-tab` creates/reuses a dedicated `opencode-agents` tab; `current-tab` opens subagents as panes in the tab containing the parent OpenCode pane, falling back to the focused tab if the parent pane cannot be resolved |
-| `divoom.enabled` | boolean | `false` | Enable Divoom Bluetooth display status GIFs for plugin load and delegated agent calls |
-| `divoom.python` | string | Divoom MiniToo bundled Python | Python executable used to run Divoom MiniToo's `divoom_send.py` helper |
-| `divoom.script` | string | Divoom MiniToo `divoom_send.py` | Divoom sender script path |
-| `divoom.size` | integer | `128` | Output GIF size passed to `divoom_send.py` |
-| `divoom.fps` | integer | `8` | Output GIF FPS passed to `divoom_send.py` |
-| `divoom.speed` | integer | `125` | Playback speed passed to `divoom_send.py` |
-| `divoom.maxFrames` | integer | `24` | Maximum frames passed to `divoom_send.py` |
-| `divoom.posterizeBits` | integer | `3` | Posterization bits passed to `divoom_send.py` |
-| `divoom.gifs.<agent>` | string | bundled GIF | Optional per-agent GIF filename or absolute path override |
 | `tmux.enabled` | boolean | `false` | Legacy alias for `multiplexer.type = "tmux"` |
 | `tmux.enabled` | boolean | `false` | Legacy alias for `multiplexer.type = "tmux"` |
 | `tmux.layout` | string | `"main-vertical"` | Legacy alias for `multiplexer.layout` |
 | `tmux.layout` | string | `"main-vertical"` | Legacy alias for `multiplexer.layout` |
 | `tmux.main_pane_size` | number | `60` | Legacy alias for `multiplexer.main_pane_size` |
 | `tmux.main_pane_size` | number | `60` | Legacy alias for `multiplexer.main_pane_size` |
@@ -181,71 +172,6 @@ major is available, the plugin shows a migration command instead.
 > `"oh-my-opencode-slim@1.0.1"`) are the true version lock. Those stay pinned
 > `"oh-my-opencode-slim@1.0.1"`) are the true version lock. Those stay pinned
 > regardless of `autoUpdate`.
 > regardless of `autoUpdate`.
 
 
-### Divoom Display Integration
-
-Divoom integration is disabled by default. Install and start the Divoom MiniToo
-macOS daemon from
-[`divoom-minitoo-osx`](https://github.com/alvinunreal/divoom-minitoo-osx)
-first, then enable this plugin integration. See the full
-**[Divoom guide](divoom.md)** for setup, daemon startup, and troubleshooting.
-
-When enabled, the plugin sends bundled GIFs to the Divoom MiniToo app's bundled
-CLI:
-
-- plugin load / waiting for user input: `intro.gif`
-- orchestrator busy: `orchestrator.gif`
-- first active delegated agent: that agent's GIF
-- parallel delegated agents: the first agent keeps the display
-- all delegated agents complete while orchestrator keeps working: `orchestrator.gif`
-- orchestrator idle again: `intro.gif`
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true
-  }
-}
-```
-
-For a one-off run without editing config:
-
-```bash
-OH_MY_OPENCODE_SLIM_DIVOOM=1 opencode
-```
-
-If `divoom.enabled` is explicitly set in config, the config value wins over the
-environment variable.
-
-The defaults target the macOS Divoom MiniToo app bundle:
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true,
-    "python": "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
-    "script": "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
-    "size": 128,
-    "fps": 8,
-    "speed": 125,
-    "maxFrames": 24,
-    "posterizeBits": 3
-  }
-}
-```
-
-To override a GIF, use either a bundled filename or an absolute path:
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true,
-    "gifs": {
-      "oracle": "/Users/me/Pictures/oracle.gif"
-    }
-  }
-}
-```
-
 ### Background Job Management
 ### Background Job Management
 
 
 Background job management is enabled by default and does not need to be present
 Background job management is enabled by default and does not need to be present

+ 0 - 169
docs/divoom.md

@@ -1,169 +0,0 @@
-# Divoom MiniToo Display Integration
-
-oh-my-opencode-slim can mirror agent activity to a Divoom MiniToo Bluetooth
-display. The integration is optional and disabled by default.
-
-## Demo
-
-https://github.com/user-attachments/assets/938c673b-f900-42fa-8131-b69d793b1440
-
-
-## What it shows
-
-When enabled, the plugin sends bundled GIFs as OpenCode changes state:
-
-| OpenCode state | Divoom display |
-|----------------|----------------|
-| Plugin loaded | `intro.gif` |
-| Orchestrator is busy planning or working directly | `orchestrator.gif` |
-| A delegated agent starts | that agent's GIF |
-| Multiple agents run in parallel | first delegated agent keeps the display |
-| Delegated agents finish but orchestrator is still working | `orchestrator.gif` |
-| Orchestrator becomes idle again | `intro.gif` |
-| Permission prompt or question needs a reply | `input.gif` |
-
-Bundled GIFs currently cover `orchestrator`, `explorer`, `librarian`, `oracle`,
-`designer`, `fixer`, `council`, `input`, and `intro`. You can configure
-`divoom.gifs.input` to customize user-input waits; if `input.gif` is not present
-yet, the plugin falls back to `intro.gif`.
-
-## Prerequisites
-
-This plugin does not talk to the Bluetooth device directly. It shells out to the
-Divoom MiniToo macOS sender/daemon tooling from:
-
-https://github.com/alvinunreal/divoom-minitoo-osx
-
-Before enabling the plugin integration:
-
-1. Install the Divoom MiniToo macOS app/tooling from that repository.
-2. Pair the Divoom MiniToo with macOS.
-3. Disconnect the normal macOS audio profile once so the daemon can claim the
-   RFCOMM app channel.
-4. Start the Divoom daemon.
-5. Confirm the bundled sender works manually.
-
-The important operational detail: the daemon should stay running. Once it holds
-the RFCOMM channel open, plugin-triggered GIF sends should not need repeated
-Bluetooth disconnect/reconnect cycles.
-
-## Start the Divoom daemon
-
-Follow the current instructions in
-[`divoom-minitoo-osx`](https://github.com/alvinunreal/divoom-minitoo-osx). The
-validated flow is:
-
-```bash
-# From the divoom-minitoo-osx checkout/app resources, disconnect audio once.
-blueutil --disconnect <DIVOOM_BLUETOOTH_ADDRESS> || true
-
-# Start the daemon on RFCOMM channel 1 / localhost port 40583.
-tools/divoom-daemon <DIVOOM_BLUETOOTH_ADDRESS> 1 40583
-```
-
-If you are using the packaged MiniToo app bundle, use its daemon/menu-bar start
-flow instead. The key is the same: disconnect the audio profile if normal daemon
-startup fails, then start the daemon and leave it running.
-
-## Manual sender smoke test
-
-Before blaming OpenCode, verify the Divoom sender works directly:
-
-```bash
-"/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python" \
-  "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py" \
-  "/path/to/test.gif" \
-  --size 128 \
-  --fps 8 \
-  --speed 125 \
-  --max-frames 24 \
-  --posterize-bits 3 \
-  --out-dir ~/.local/share/opencode/storage/oh-my-opencode-slim/divoom/captures
-```
-
-**Note:** The sender must support the `--out-dir` flag. This requires a recent
-version of the Divoom MiniToo sender (the plugin uses this for temporary
-processing files).
-
-If that updates the display, the OpenCode integration should work once enabled.
-
-**Output directory path:** The plugin writes temporary processing files to
-`$XDG_DATA_HOME/opencode/storage/oh-my-opencode-slim/divoom/captures` when
-`XDG_DATA_HOME` is set to a non-empty absolute path. Otherwise it falls back to
-`~/.local/share/opencode/storage/oh-my-opencode-slim/divoom/captures`.
-
-## Enable in oh-my-opencode-slim
-
-Open your plugin config:
-
-```text
-~/.config/opencode/oh-my-opencode-slim.json
-```
-
-Add:
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true
-  }
-}
-```
-
-Then rebuild/restart OpenCode if you are running from a local checkout.
-
-For one-off runs, you can enable Divoom without changing your config:
-
-```bash
-OH_MY_OPENCODE_SLIM_DIVOOM=1 opencode
-```
-
-Accepted truthy values are `1`, `true`, `yes`, and `on`. The environment
-variable force-enables Divoom for that run, even if `divoom.enabled` is `false`
-in config.
-
-## Tunable settings
-
-The defaults target the macOS Divoom MiniToo app bundle:
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true,
-    "python": "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
-    "script": "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
-    "size": 128,
-    "fps": 8,
-    "speed": 125,
-    "maxFrames": 24,
-    "posterizeBits": 3
-  }
-}
-```
-
-You can also override individual GIFs with either bundled filenames or absolute
-paths:
-
-```jsonc
-{
-  "divoom": {
-    "enabled": true,
-    "gifs": {
-      "oracle": "/Users/me/Pictures/oracle.gif",
-      "fixer": "fixer.gif"
-    }
-  }
-}
-```
-
-## Troubleshooting
-
-- **Nothing changes on the display:** run the manual sender smoke test first.
-- **Daemon cannot open the channel:** disconnect the Divoom audio profile, then
-  start the daemon again.
-- **GIF sends are slow:** reduce `maxFrames`, lower `fps`, or use fewer colors
-  via `posterizeBits`.
-- **A custom GIF does not show:** verify the path exists. Relative names resolve
-  against the bundled Divoom asset directory; absolute paths are used as-is.
-- **Only one of several parallel agents appears:** expected behavior. The first
-  delegated agent keeps the display until all parallel delegated work finishes.

BIN
img/divoom-minitoo.mp4


+ 0 - 59
oh-my-opencode-slim.schema.json

@@ -517,65 +517,6 @@
         }
         }
       }
       }
     },
     },
-    "divoom": {
-      "type": "object",
-      "properties": {
-        "enabled": {
-          "default": false,
-          "type": "boolean"
-        },
-        "python": {
-          "default": "/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python",
-          "type": "string",
-          "minLength": 1
-        },
-        "script": {
-          "default": "/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py",
-          "type": "string",
-          "minLength": 1
-        },
-        "size": {
-          "default": 128,
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 1024
-        },
-        "fps": {
-          "default": 8,
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 60
-        },
-        "speed": {
-          "default": 125,
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 10000
-        },
-        "maxFrames": {
-          "default": 24,
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 500
-        },
-        "posterizeBits": {
-          "default": 3,
-          "type": "integer",
-          "minimum": 1,
-          "maximum": 8
-        },
-        "gifs": {
-          "type": "object",
-          "propertyNames": {
-            "type": "string"
-          },
-          "additionalProperties": {
-            "type": "string",
-            "minLength": 1
-          }
-        }
-      }
-    },
     "fallback": {
     "fallback": {
       "type": "object",
       "type": "object",
       "properties": {
       "properties": {

+ 1 - 2
package.json

@@ -52,8 +52,7 @@
     "clean:dist": "bun -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
     "clean:dist": "bun -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
     "build:plugin": "bun build src/index.ts src/tui.ts --outdir dist --target node --format esm --external @ast-grep/napi --external @opencode-ai/plugin --external @opencode-ai/plugin/* --external @opencode-ai/sdk --external @opencode-ai/sdk/* --external @opentui/core --external @opentui/solid --external jsdom --external zod",
     "build:plugin": "bun build src/index.ts src/tui.ts --outdir dist --target node --format esm --external @ast-grep/napi --external @opencode-ai/plugin --external @opencode-ai/plugin/* --external @opencode-ai/sdk --external @opencode-ai/sdk/* --external @opentui/core --external @opentui/solid --external jsdom --external zod",
     "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target node --format esm --external @ast-grep/napi --external @opencode-ai/plugin --external @opencode-ai/plugin/* --external @opencode-ai/sdk --external @opencode-ai/sdk/* --external jsdom --external zod",
     "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target node --format esm --external @ast-grep/napi --external @opencode-ai/plugin --external @opencode-ai/plugin/* --external @opencode-ai/sdk --external @opencode-ai/sdk/* --external jsdom --external zod",
-    "copy:divoom-assets": "bun run scripts/copy-divoom-assets.ts",
-    "build": "bun run clean:dist && bun run build:plugin && bun run build:cli && bun run copy:divoom-assets && tsc --emitDeclarationOnly && bun run generate-schema",
+    "build": "bun run clean:dist && bun run build:plugin && bun run build:cli && tsc --emitDeclarationOnly && bun run generate-schema",
     "prepare": "bun run build",
     "prepare": "bun run build",
     "contributors:add": "all-contributors add",
     "contributors:add": "all-contributors add",
     "contributors:check": "all-contributors check",
     "contributors:check": "all-contributors check",

+ 0 - 23
scripts/copy-divoom-assets.ts

@@ -1,23 +0,0 @@
-import { copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const repoRoot = path.resolve(__dirname, '..');
-const sourceDir = path.join(repoRoot, 'src', 'divoom');
-const outputDir = path.join(repoRoot, 'dist', 'divoom');
-
-rmSync(outputDir, { recursive: true, force: true });
-mkdirSync(outputDir, { recursive: true });
-
-let copied = 0;
-for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
-  if (!entry.isFile() || !entry.name.endsWith('.gif')) continue;
-  copyFileSync(
-    path.join(sourceDir, entry.name),
-    path.join(outputDir, entry.name),
-  );
-  copied += 1;
-}
-
-console.log(`✅ Copied ${copied} Divoom GIF assets to ${outputDir}`);

+ 0 - 9
scripts/verify-release-artifact.ts

@@ -29,15 +29,6 @@ const packagedRequiredFiles = [
   'dist/index.js',
   'dist/index.js',
   'dist/index.d.ts',
   'dist/index.d.ts',
   'dist/cli/index.js',
   'dist/cli/index.js',
-  'dist/divoom/council.gif',
-  'dist/divoom/designer.gif',
-  'dist/divoom/explorer.gif',
-  'dist/divoom/fixer.gif',
-  'dist/divoom/input.gif',
-  'dist/divoom/intro.gif',
-  'dist/divoom/librarian.gif',
-  'dist/divoom/oracle.gif',
-  'dist/divoom/orchestrator.gif',
   'oh-my-opencode-slim.schema.json',
   'oh-my-opencode-slim.schema.json',
   'src/skills/simplify/SKILL.md',
   'src/skills/simplify/SKILL.md',
   'src/skills/codemap/SKILL.md',
   'src/skills/codemap/SKILL.md',

+ 0 - 1
src/config/loader.ts

@@ -201,7 +201,6 @@ export function mergePluginConfigs(
     multiplexer: deepMerge(base.multiplexer, override.multiplexer),
     multiplexer: deepMerge(base.multiplexer, override.multiplexer),
     interview: deepMerge(base.interview, override.interview),
     interview: deepMerge(base.interview, override.interview),
     backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
     backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
-    divoom: deepMerge(base.divoom, override.divoom),
     fallback: deepMerge(base.fallback, override.fallback),
     fallback: deepMerge(base.fallback, override.fallback),
     council: deepMerge(base.council, override.council),
     council: deepMerge(base.council, override.council),
     companion: deepMerge(
     companion: deepMerge(

+ 0 - 25
src/config/schema.ts

@@ -194,30 +194,6 @@ export const BackgroundJobsConfigSchema = z.object({
 
 
 export type BackgroundJobsConfig = z.infer<typeof BackgroundJobsConfigSchema>;
 export type BackgroundJobsConfig = z.infer<typeof BackgroundJobsConfigSchema>;
 
 
-export const DivoomConfigSchema = z.object({
-  enabled: z.boolean().default(false),
-  python: z
-    .string()
-    .min(1)
-    .default(
-      '/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python',
-    ),
-  script: z
-    .string()
-    .min(1)
-    .default(
-      '/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py',
-    ),
-  size: z.number().int().min(1).max(1024).default(128),
-  fps: z.number().int().min(1).max(60).default(8),
-  speed: z.number().int().min(1).max(10_000).default(125),
-  maxFrames: z.number().int().min(1).max(500).default(24),
-  posterizeBits: z.number().int().min(1).max(8).default(3),
-  gifs: z.record(z.string(), z.string().min(1)).optional(),
-});
-
-export type DivoomConfig = z.infer<typeof DivoomConfigSchema>;
-
 export const FailoverConfigSchema = z.object({
 export const FailoverConfigSchema = z.object({
   enabled: z.boolean().default(true),
   enabled: z.boolean().default(true),
   timeoutMs: z.number().min(0).default(15000),
   timeoutMs: z.number().min(0).default(15000),
@@ -309,7 +285,6 @@ export const PluginConfigSchema = z
     websearch: WebsearchConfigSchema.optional(),
     websearch: WebsearchConfigSchema.optional(),
     interview: InterviewConfigSchema.optional(),
     interview: InterviewConfigSchema.optional(),
     backgroundJobs: BackgroundJobsConfigSchema.optional(),
     backgroundJobs: BackgroundJobsConfigSchema.optional(),
-    divoom: DivoomConfigSchema.optional(),
     fallback: FailoverConfigSchema.optional(),
     fallback: FailoverConfigSchema.optional(),
     council: CouncilConfigSchema.optional(),
     council: CouncilConfigSchema.optional(),
     companion: CompanionConfigSchema.optional(),
     companion: CompanionConfigSchema.optional(),

BIN
src/divoom/council.gif


BIN
src/divoom/designer.gif


BIN
src/divoom/explorer.gif


BIN
src/divoom/fixer.gif


BIN
src/divoom/input.gif


BIN
src/divoom/intro.gif


BIN
src/divoom/librarian.gif


+ 0 - 527
src/divoom/manager.test.ts

@@ -1,527 +0,0 @@
-import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
-import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
-import { tmpdir } from 'node:os';
-import path from 'node:path';
-import {
-  DivoomManager,
-  type DivoomSenderCall,
-  getDivoomOutDir,
-} from './manager';
-
-function createGifAssets(dir: string, names: string[]): void {
-  for (const name of names) {
-    writeFileSync(path.join(dir, name), 'gif');
-  }
-}
-
-describe('DivoomManager', () => {
-  let tempDir: string;
-  let calls: DivoomSenderCall[];
-  let pythonPath: string;
-  let scriptPath: string;
-  let originalDivoomEnv: string | undefined;
-  let originalXdgDataHome: string | undefined;
-
-  beforeEach(() => {
-    originalDivoomEnv = process.env.OH_MY_OPENCODE_SLIM_DIVOOM;
-    originalXdgDataHome = process.env.XDG_DATA_HOME;
-    delete process.env.OH_MY_OPENCODE_SLIM_DIVOOM;
-    tempDir = mkdtempSync(path.join(tmpdir(), 'divoom-test-'));
-    // Set XDG_DATA_HOME to a temp path to avoid writing to real user data directory
-    process.env.XDG_DATA_HOME = path.join(tempDir, 'xdg-data');
-    calls = [];
-    pythonPath = path.join(tempDir, 'python');
-    scriptPath = path.join(tempDir, 'divoom_send.py');
-    writeFileSync(pythonPath, 'python');
-    writeFileSync(scriptPath, 'script');
-    createGifAssets(tempDir, [
-      'intro.gif',
-      'orchestrator.gif',
-      'explorer.gif',
-      'fixer.gif',
-      'input.gif',
-      'oracle.gif',
-    ]);
-  });
-
-  afterEach(() => {
-    if (originalDivoomEnv === undefined) {
-      delete process.env.OH_MY_OPENCODE_SLIM_DIVOOM;
-    } else {
-      process.env.OH_MY_OPENCODE_SLIM_DIVOOM = originalDivoomEnv;
-    }
-    if (originalXdgDataHome === undefined) {
-      delete process.env.XDG_DATA_HOME;
-    } else {
-      process.env.XDG_DATA_HOME = originalXdgDataHome;
-    }
-    rmSync(tempDir, { recursive: true, force: true });
-  });
-
-  function createManager(config: { enabled?: boolean } = {}) {
-    return new DivoomManager(
-      {
-        enabled: config.enabled ?? true,
-        python: pythonPath,
-        script: scriptPath,
-      },
-      (call) => {
-        calls.push(call);
-      },
-      { assetDir: tempDir },
-    );
-  }
-
-  test('does nothing when disabled', async () => {
-    const manager = createManager({ enabled: false });
-
-    manager.onPluginLoad();
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'explorer' },
-    });
-    manager.onTaskEnd({ parentSessionId: 'parent', callId: 'call-1' });
-    await manager.flush();
-
-    expect(calls).toHaveLength(0);
-  });
-
-  test('shows intro on plugin load when enabled', async () => {
-    const manager = createManager();
-
-    manager.onPluginLoad();
-    await manager.flush();
-
-    expect(calls).toHaveLength(1);
-    expect(calls[0].args[1]).toBe(path.join(tempDir, 'intro.gif'));
-  });
-
-  test('can be enabled for one run with env var', async () => {
-    process.env.OH_MY_OPENCODE_SLIM_DIVOOM = '1';
-    const manager = new DivoomManager(
-      {
-        python: pythonPath,
-        script: scriptPath,
-      },
-      (call) => {
-        calls.push(call);
-      },
-      { assetDir: tempDir },
-    );
-
-    manager.onPluginLoad();
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('env var force-enables even when config disables', async () => {
-    process.env.OH_MY_OPENCODE_SLIM_DIVOOM = 'true';
-    const manager = createManager({ enabled: false });
-
-    manager.onPluginLoad();
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('shows task agent then orchestrator after a single task', async () => {
-    const manager = createManager();
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'explorer' },
-    });
-    await manager.flush();
-    manager.onTaskEnd({ parentSessionId: 'parent', callId: 'call-1' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'explorer.gif'),
-      path.join(tempDir, 'orchestrator.gif'),
-    ]);
-  });
-
-  test('shows orchestrator while busy and intro when idle', async () => {
-    const manager = createManager();
-
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'idle',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'orchestrator.gif'),
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('idle clears active child state and returns to intro', async () => {
-    const manager = createManager();
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'explorer' },
-    });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'idle',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onTaskEnd({ parentSessionId: 'parent', callId: 'call-1' });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'idle',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'explorer.gif'),
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('explicit question requests show input until work resumes', async () => {
-    const manager = createManager();
-
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onUserInputRequired({ sessionId: 'parent', requestId: 'q-1' });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onUserInputResolved({ sessionId: 'parent', requestId: 'q-1' });
-    await manager.flush();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'orchestrator.gif'),
-      path.join(tempDir, 'input.gif'),
-      path.join(tempDir, 'orchestrator.gif'),
-    ]);
-  });
-
-  test('child prompt restores delegated agent after reply', async () => {
-    const manager = createManager();
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'explorer' },
-    });
-    await manager.flush();
-    manager.onUserInputRequired({ sessionId: 'child', requestId: 'p-1' });
-    await manager.flush();
-    manager.onUserInputResolved({ sessionId: 'child', requestId: 'p-1' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'explorer.gif'),
-      path.join(tempDir, 'input.gif'),
-      path.join(tempDir, 'explorer.gif'),
-    ]);
-  });
-
-  test('overlapping prompts keep input until all resolve', async () => {
-    const manager = createManager();
-
-    manager.onUserInputRequired({ sessionId: 'one', requestId: 'p-1' });
-    await manager.flush();
-    manager.onUserInputRequired({ sessionId: 'two', requestId: 'p-2' });
-    await manager.flush();
-    manager.onUserInputResolved({ sessionId: 'one', requestId: 'p-1' });
-    await manager.flush();
-    manager.onUserInputResolved({ sessionId: 'two', requestId: 'p-2' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'input.gif'),
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('session deletion clears pending input and rerenders', async () => {
-    const manager = createManager();
-
-    manager.onUserInputRequired({ sessionId: 'child', requestId: 'p-1' });
-    await manager.flush();
-    manager.onSessionDeleted({ sessionId: 'child' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'input.gif'),
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('orchestrator deletion clears busy display', async () => {
-    const manager = createManager();
-
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    await manager.flush();
-    manager.onSessionDeleted({ sessionId: 'parent', isOrchestrator: true });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'orchestrator.gif'),
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('pending user input falls back to intro when input gif is absent', async () => {
-    rmSync(path.join(tempDir, 'input.gif'));
-    const manager = createManager();
-
-    manager.onUserInputRequired({ sessionId: 'parent', requestId: 'q-1' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'intro.gif'),
-    ]);
-  });
-
-  test('keeps first agent visible for parallel tasks', async () => {
-    const manager = createManager();
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'explorer' },
-    });
-    await manager.flush();
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-2',
-      args: { subagent_type: 'fixer' },
-    });
-    await manager.flush();
-    manager.onTaskEnd({ parentSessionId: 'parent', callId: 'call-1' });
-    await manager.flush();
-    manager.onTaskEnd({ parentSessionId: 'parent', callId: 'call-2' });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'explorer.gif'),
-      path.join(tempDir, 'orchestrator.gif'),
-    ]);
-  });
-
-  test('falls back to orchestrator gif for unknown agents', async () => {
-    const manager = createManager();
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'custom-agent' },
-    });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'orchestrator.gif'),
-    ]);
-  });
-
-  test('uses configured sender settings and gif overrides', async () => {
-    const customGif = path.join(tempDir, 'custom-oracle.gif');
-    const customPython = path.join(tempDir, 'custom-python');
-    const customScript = path.join(tempDir, 'custom-divoom-send.py');
-    writeFileSync(customGif, 'gif');
-    writeFileSync(customPython, 'python');
-    writeFileSync(customScript, 'script');
-    const manager = new DivoomManager(
-      {
-        enabled: true,
-        python: customPython,
-        script: customScript,
-        size: 64,
-        fps: 12,
-        speed: 250,
-        maxFrames: 10,
-        posterizeBits: 4,
-        gifs: { oracle: customGif },
-      },
-      (call) => {
-        calls.push(call);
-      },
-      { assetDir: tempDir },
-    );
-
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'oracle' },
-    });
-    await manager.flush();
-
-    expect(calls[0].command).toBe(customPython);
-    expect(calls[0].args).toHaveLength(14);
-    expect(calls[0].args[0]).toBe(customScript);
-    expect(calls[0].args[1]).toBe(customGif);
-    expect(calls[0].args.slice(2, 13)).toEqual([
-      '--size',
-      '64',
-      '--fps',
-      '12',
-      '--speed',
-      '250',
-      '--max-frames',
-      '10',
-      '--posterize-bits',
-      '4',
-      '--out-dir',
-    ]);
-    // Verify out-dir is absolute (last arg)
-    expect(path.isAbsolute(calls[0].args[13])).toBe(true);
-  });
-
-  test('drops stale queued sends and keeps latest requested gif', async () => {
-    const manager = createManager();
-
-    manager.onPluginLoad();
-    manager.onOrchestratorStatus({
-      sessionId: 'parent',
-      status: 'busy',
-      isOrchestrator: true,
-    });
-    manager.onTaskStart({
-      parentSessionId: 'parent',
-      callId: 'call-1',
-      args: { subagent_type: 'fixer' },
-    });
-    await manager.flush();
-
-    expect(calls.map((call) => call.args[1])).toEqual([
-      path.join(tempDir, 'fixer.gif'),
-    ]);
-  });
-
-  test('missing executable does not poison future attempts', async () => {
-    const manager = new DivoomManager(
-      {
-        enabled: true,
-        python: path.join(tempDir, 'missing-python'),
-        script: scriptPath,
-      },
-      (call) => {
-        calls.push(call);
-      },
-      { assetDir: tempDir },
-    );
-
-    manager.onPluginLoad();
-    await manager.flush();
-    manager.onPluginLoad();
-    await manager.flush();
-
-    expect(calls).toHaveLength(0);
-  });
-
-  test('out-dir is absolute and independent of process.cwd', async () => {
-    const originalCwd = process.cwd();
-    const tempCwd = mkdtempSync(path.join(tmpdir(), 'divoom-cwd-test-'));
-
-    try {
-      process.chdir(tempCwd);
-      const manager = createManager();
-
-      manager.onPluginLoad();
-      await manager.flush();
-
-      expect(calls).toHaveLength(1);
-      const outDirArg = calls[0].args[calls[0].args.length - 1];
-      // Must be absolute, not relative to cwd
-      expect(path.isAbsolute(outDirArg)).toBe(true);
-      // Must not start with the temp cwd
-      expect(outDirArg.startsWith(tempCwd)).toBe(false);
-      // Must contain the expected path segments
-      expect(outDirArg).toContain('divoom');
-      expect(outDirArg).toContain('captures');
-    } finally {
-      process.chdir(originalCwd);
-      rmSync(tempCwd, { recursive: true, force: true });
-    }
-  });
-
-  test('empty XDG_DATA_HOME falls back to homedir/.local/share', async () => {
-    // Set empty XDG_DATA_HOME
-    process.env.XDG_DATA_HOME = '';
-    const homeDir = path.join(tempDir, 'home');
-    const outDirArg = getDivoomOutDir(homeDir);
-    // Must be absolute
-    expect(path.isAbsolute(outDirArg)).toBe(true);
-    expect(outDirArg).toStartWith(path.join(tempDir, 'home'));
-    // Must contain the expected path segments
-    expect(outDirArg).toContain('divoom');
-    expect(outDirArg).toContain('captures');
-  });
-
-  test('relative XDG_DATA_HOME falls back to homedir/.local/share', async () => {
-    // Set relative XDG_DATA_HOME (should be rejected)
-    process.env.XDG_DATA_HOME = 'relative/path/to/data';
-    const homeDir = path.join(tempDir, 'home');
-    const outDirArg = getDivoomOutDir(homeDir);
-    // Must be absolute
-    expect(path.isAbsolute(outDirArg)).toBe(true);
-    // Must not start with the relative path
-    expect(outDirArg.startsWith('relative')).toBe(false);
-    expect(outDirArg).toStartWith(path.join(tempDir, 'home'));
-    // Must contain the expected path segments
-    expect(outDirArg).toContain('divoom');
-    expect(outDirArg).toContain('captures');
-  });
-
-  test('whitespace-only XDG_DATA_HOME falls back to homedir/.local/share', async () => {
-    // Set whitespace-only XDG_DATA_HOME (should be rejected after trim)
-    process.env.XDG_DATA_HOME = '   ';
-    const homeDir = path.join(tempDir, 'home');
-    const outDirArg = getDivoomOutDir(homeDir);
-    // Must be absolute
-    expect(path.isAbsolute(outDirArg)).toBe(true);
-    expect(outDirArg).toStartWith(path.join(tempDir, 'home'));
-    // Must contain the expected path segments
-    expect(outDirArg).toContain('divoom');
-    expect(outDirArg).toContain('captures');
-  });
-});

+ 0 - 377
src/divoom/manager.ts

@@ -1,377 +0,0 @@
-import { spawn } from 'node:child_process';
-import { existsSync, mkdirSync } from 'node:fs';
-import * as os from 'node:os';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-import type { DivoomConfig } from '../config';
-import { log } from '../utils/logger';
-
-type TaskArgs = {
-  subagent_type?: unknown;
-};
-
-export type DivoomSenderCall = {
-  command: string;
-  args: string[];
-};
-
-export type DivoomSender = (call: DivoomSenderCall) => Promise<void> | void;
-
-type ParentState = {
-  activeCalls: Map<string, string>;
-  displayedAgent?: string;
-};
-
-const AGENT_GIFS: Record<string, string> = {
-  council: 'council.gif',
-  councillor: 'council.gif',
-  designer: 'designer.gif',
-  explorer: 'explorer.gif',
-  fixer: 'fixer.gif',
-  input: 'input.gif',
-  intro: 'intro.gif',
-  librarian: 'librarian.gif',
-  oracle: 'oracle.gif',
-  orchestrator: 'orchestrator.gif',
-};
-
-const DEFAULT_DIVOOM_CONFIG: DivoomConfig = {
-  enabled: false,
-  python:
-    '/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python',
-  script:
-    '/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py',
-  size: 128,
-  fps: 8,
-  speed: 125,
-  maxFrames: 24,
-  posterizeBits: 3,
-};
-
-const DIVOOM_ENABLE_ENV = 'OH_MY_OPENCODE_SLIM_DIVOOM';
-
-type DivoomManagerOptions = {
-  assetDir?: string | null;
-  sender?: DivoomSender;
-};
-
-function resolveAssetDir(): string | null {
-  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
-  const candidates = [
-    moduleDir,
-    path.resolve(moduleDir, 'divoom'),
-    path.resolve(moduleDir, '../../src/divoom'),
-    path.resolve(process.cwd(), 'src/divoom'),
-  ];
-
-  for (const candidate of candidates) {
-    if (existsSync(path.join(candidate, 'intro.gif'))) {
-      return candidate;
-    }
-  }
-
-  return null;
-}
-
-function normalizeAgentName(value: unknown): string | null {
-  if (typeof value !== 'string') return null;
-  const normalized = value.trim().toLowerCase();
-  return normalized.length > 0 ? normalized : null;
-}
-
-function isEnvEnabled(value: string | undefined): boolean {
-  if (!value) return false;
-  return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
-}
-
-function inputKey(sessionId: string, requestId: string): string {
-  return `${sessionId}:${requestId}`;
-}
-
-export function getDivoomOutDir(homeDir = os.homedir()): string {
-  const xdg = process.env.XDG_DATA_HOME?.trim();
-  const baseDir =
-    xdg && xdg.length > 0 && path.isAbsolute(xdg)
-      ? xdg
-      : path.join(homeDir, '.local', 'share');
-  return path.join(
-    baseDir,
-    'opencode',
-    'storage',
-    'oh-my-opencode-slim',
-    'divoom',
-    'captures',
-  );
-}
-
-export class DivoomManager {
-  private assetDir: string | null;
-  private config: DivoomConfig;
-  private parentStates = new Map<string, ParentState>();
-  private pendingUserInputs = new Set<string>();
-  private orchestratorBusy = false;
-  private latestRequestedGifPath?: string;
-  private lastGifPath?: string;
-  private sendQueue = Promise.resolve();
-
-  constructor(
-    config?: Partial<DivoomConfig>,
-    private sender: DivoomSender = defaultSender,
-    options: DivoomManagerOptions = {},
-  ) {
-    this.config = {
-      ...DEFAULT_DIVOOM_CONFIG,
-      ...config,
-      enabled: isEnvEnabled(process.env[DIVOOM_ENABLE_ENV])
-        ? true
-        : (config?.enabled ?? false),
-      gifs: config?.gifs,
-    };
-    this.assetDir = options.assetDir ?? resolveAssetDir();
-    if (options.sender) {
-      this.sender = options.sender;
-    }
-  }
-
-  onPluginLoad(): void {
-    this.show('intro');
-  }
-
-  onTaskStart(input: {
-    parentSessionId?: string;
-    callId?: string;
-    args?: unknown;
-  }): void {
-    if (!input.parentSessionId || !input.callId) return;
-    if (!isTaskArgs(input.args)) return;
-
-    const agent = normalizeAgentName(input.args.subagent_type);
-    if (!agent) return;
-
-    const state = this.getParentState(input.parentSessionId);
-    const wasIdle = state.activeCalls.size === 0;
-    state.activeCalls.set(input.callId, agent);
-    this.orchestratorBusy = true;
-
-    if (wasIdle && !state.displayedAgent) {
-      state.displayedAgent = agent;
-    }
-
-    this.render();
-  }
-
-  onTaskEnd(input: { parentSessionId?: string; callId?: string }): void {
-    if (!input.parentSessionId || !input.callId) return;
-
-    const state = this.parentStates.get(input.parentSessionId);
-    if (!state) return;
-
-    state.activeCalls.delete(input.callId);
-    if (state.activeCalls.size === 0) {
-      this.parentStates.delete(input.parentSessionId);
-    }
-
-    this.render();
-  }
-
-  onUserInputRequired(input: { sessionId?: string; requestId?: string }): void {
-    if (!input.sessionId || !input.requestId) return;
-
-    this.pendingUserInputs.add(inputKey(input.sessionId, input.requestId));
-    this.render();
-  }
-
-  onUserInputResolved(input: { sessionId?: string; requestId?: string }): void {
-    if (!input.sessionId || !input.requestId) return;
-
-    this.pendingUserInputs.delete(inputKey(input.sessionId, input.requestId));
-    this.render();
-  }
-
-  onOrchestratorStatus(input: {
-    sessionId?: string;
-    status?: string;
-    isOrchestrator?: boolean;
-  }): void {
-    if (!input.sessionId || !input.isOrchestrator) return;
-
-    if (input.status === 'busy') {
-      this.orchestratorBusy = true;
-      this.render();
-      return;
-    }
-
-    if (input.status === 'idle') {
-      this.orchestratorBusy = false;
-      this.parentStates.delete(input.sessionId);
-      this.render();
-    }
-  }
-
-  onSessionDeleted(input: {
-    sessionId?: string;
-    isOrchestrator?: boolean;
-  }): void {
-    const sessionId = input.sessionId;
-    if (!sessionId) return;
-    if (input.isOrchestrator) this.orchestratorBusy = false;
-    this.parentStates.delete(sessionId);
-    this.pendingUserInputs = new Set(
-      Array.from(this.pendingUserInputs).filter(
-        (key) => !key.startsWith(`${sessionId}:`),
-      ),
-    );
-    this.render();
-  }
-
-  private render(): void {
-    if (this.pendingUserInputs.size > 0) {
-      this.show('input');
-      return;
-    }
-
-    const activeAgent = Array.from(this.parentStates.values()).find(
-      (state) => state.displayedAgent && state.activeCalls.size > 0,
-    )?.displayedAgent;
-    if (activeAgent) {
-      this.show(activeAgent);
-      return;
-    }
-
-    this.show(this.orchestratorBusy ? 'orchestrator' : 'intro');
-  }
-
-  private getParentState(parentSessionId: string): ParentState {
-    const existing = this.parentStates.get(parentSessionId);
-    if (existing) return existing;
-
-    const created: ParentState = {
-      activeCalls: new Map(),
-    };
-    this.parentStates.set(parentSessionId, created);
-    return created;
-  }
-
-  private show(agent: string): void {
-    if (!this.config.enabled) return;
-
-    if (!this.assetDir) {
-      log('[divoom] asset directory not found');
-      return;
-    }
-
-    const fileName =
-      this.config.gifs?.[agent] ?? AGENT_GIFS[agent] ?? AGENT_GIFS.orchestrator;
-    const requestedGifPath = path.isAbsolute(fileName)
-      ? fileName
-      : path.join(this.assetDir, fileName);
-    const fallbackGifPath = path.join(this.assetDir, AGENT_GIFS.intro);
-    const gifPath = existsSync(requestedGifPath)
-      ? requestedGifPath
-      : agent === 'input'
-        ? fallbackGifPath
-        : requestedGifPath;
-    if (!existsSync(gifPath)) {
-      log('[divoom] gif not found', { agent, gifPath: requestedGifPath });
-      return;
-    }
-
-    if (gifPath === this.latestRequestedGifPath) return;
-    this.latestRequestedGifPath = gifPath;
-
-    const outDir = getDivoomOutDir();
-    try {
-      mkdirSync(outDir, { recursive: true });
-    } catch (error) {
-      this.clearLatestIfCurrent(gifPath);
-      log('[divoom] output directory not writable', {
-        outDir,
-        error: String(error),
-      });
-      return;
-    }
-
-    const call = {
-      command: this.config.python,
-      args: [
-        this.config.script,
-        gifPath,
-        '--size',
-        String(this.config.size),
-        '--fps',
-        String(this.config.fps),
-        '--speed',
-        String(this.config.speed),
-        '--max-frames',
-        String(this.config.maxFrames),
-        '--posterize-bits',
-        String(this.config.posterizeBits),
-        '--out-dir',
-        outDir,
-      ],
-    };
-
-    this.sendQueue = this.sendQueue
-      .catch(() => {})
-      .then(async () => {
-        if (gifPath !== this.latestRequestedGifPath) return;
-        if (!existsSync(this.config.python)) {
-          this.clearLatestIfCurrent(gifPath);
-          log('[divoom] python executable not found', this.config.python);
-          return;
-        }
-        if (!existsSync(this.config.script)) {
-          this.clearLatestIfCurrent(gifPath);
-          log('[divoom] sender script not found', this.config.script);
-          return;
-        }
-
-        try {
-          await this.sender(call);
-          this.lastGifPath = gifPath;
-          log('[divoom] showing gif', { agent, gifPath });
-        } catch (error) {
-          this.clearLatestIfCurrent(gifPath);
-          log('[divoom] failed to send gif', String(error));
-        }
-      });
-  }
-
-  async flush(): Promise<void> {
-    await this.sendQueue.catch(() => {});
-  }
-
-  private clearLatestIfCurrent(gifPath: string): void {
-    if (this.latestRequestedGifPath === gifPath) {
-      this.latestRequestedGifPath = undefined;
-    }
-    if (this.lastGifPath === gifPath) {
-      this.lastGifPath = undefined;
-    }
-  }
-}
-
-function isTaskArgs(value: unknown): value is TaskArgs {
-  return typeof value === 'object' && value !== null;
-}
-
-function defaultSender(call: DivoomSenderCall): Promise<void> {
-  return new Promise((resolve, reject) => {
-    const child = spawn(call.command, call.args, {
-      detached: true,
-      stdio: 'ignore',
-    });
-
-    child.once('error', reject);
-    child.once('spawn', () => {
-      child.unref();
-      resolve();
-    });
-  });
-}
-
-export function createDivoomManager(
-  config?: Partial<DivoomConfig>,
-): DivoomManager {
-  return new DivoomManager(config);
-}

BIN
src/divoom/oracle.gif


BIN
src/divoom/orchestrator.gif


+ 1 - 47
src/index.ts

@@ -17,7 +17,6 @@ import {
   setActiveRuntimePreset,
   setActiveRuntimePreset,
 } from './config/runtime-preset';
 } from './config/runtime-preset';
 import { CouncilManager } from './council';
 import { CouncilManager } from './council';
-import { createDivoomManager } from './divoom/manager';
 import {
 import {
   createApplyPatchHook,
   createApplyPatchHook,
   createAutoUpdateCheckerHook,
   createAutoUpdateCheckerHook,
@@ -142,7 +141,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   let interviewManager: ReturnType<typeof createInterviewManager>;
   let interviewManager: ReturnType<typeof createInterviewManager>;
   let presetManager: ReturnType<typeof createPresetManager>;
   let presetManager: ReturnType<typeof createPresetManager>;
   let companionManager: CompanionManager;
   let companionManager: CompanionManager;
-  let divoomManager: ReturnType<typeof createDivoomManager>;
   let councilTools: Record<string, unknown>;
   let councilTools: Record<string, unknown>;
   let cancelTaskTools: Record<string, unknown>;
   let cancelTaskTools: Record<string, unknown>;
   let webfetch: ReturnType<typeof createWebfetchTool>;
   let webfetch: ReturnType<typeof createWebfetchTool>;
@@ -327,7 +325,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ctx.directory,
       ctx.directory,
       config.companion,
       config.companion,
     );
     );
-    divoomManager = createDivoomManager(config.divoom);
     cancelTaskTools = createCancelTaskTool({
     cancelTaskTools = createCancelTaskTool({
       client: ctx.client,
       client: ctx.client,
       backgroundJobBoard,
       backgroundJobBoard,
@@ -396,7 +393,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     }
     }
   });
   });
 
 
-  divoomManager.onPluginLoad();
   companionManager.onLoad();
   companionManager.onLoad();
 
 
   return {
   return {
@@ -813,13 +809,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         event.type === 'permission.asked' ||
         event.type === 'permission.asked' ||
         event.type === 'question.asked'
         event.type === 'question.asked'
       ) {
       ) {
-        const props = event.properties as
-          | { sessionID?: string; id?: string; requestID?: string }
-          | undefined;
-        divoomManager.onUserInputRequired({
-          sessionId: props?.sessionID,
-          requestId: props?.id ?? props?.requestID,
-        });
         companionManager.onWaitingInput();
         companionManager.onWaitingInput();
       }
       }
 
 
@@ -828,13 +817,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         event.type === 'question.replied' ||
         event.type === 'question.replied' ||
         event.type === 'question.rejected'
         event.type === 'question.rejected'
       ) {
       ) {
-        const props = event.properties as
-          | { sessionID?: string; requestID?: string; id?: string }
-          | undefined;
-        divoomManager.onUserInputResolved({
-          sessionId: props?.sessionID,
-          requestId: props?.requestID ?? props?.id,
-        });
         companionManager.onInputResolved();
         companionManager.onInputResolved();
       }
       }
 
 
@@ -843,14 +825,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           | { sessionID?: string; status?: { type?: string } }
           | { sessionID?: string; status?: { type?: string } }
           | undefined;
           | undefined;
         const sessionID = props?.sessionID;
         const sessionID = props?.sessionID;
-        const isOrch = sessionID
-          ? sessionAgentMap.get(sessionID) === 'orchestrator'
-          : false;
-        divoomManager.onOrchestratorStatus({
-          sessionId: sessionID,
-          status: props?.status?.type,
-          isOrchestrator: isOrch,
-        });
         companionManager.onSessionStatus({
         companionManager.onSessionStatus({
           sessionId: sessionID,
           sessionId: sessionID,
           agent: sessionID ? sessionAgentMap.get(sessionID) : undefined,
           agent: sessionID ? sessionAgentMap.get(sessionID) : undefined,
@@ -863,13 +837,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           | { info?: { id?: string }; sessionID?: string }
           | { info?: { id?: string }; sessionID?: string }
           | undefined;
           | undefined;
         const sessionID = props?.info?.id ?? props?.sessionID;
         const sessionID = props?.info?.id ?? props?.sessionID;
-        const isOrch = sessionID
-          ? sessionAgentMap.get(sessionID) === 'orchestrator'
-          : false;
-        divoomManager.onSessionDeleted({
-          sessionId: sessionID,
-          isOrchestrator: isOrch,
-        });
         companionManager.onSessionDeleted(sessionID);
         companionManager.onSessionDeleted(sessionID);
       }
       }
 
 
@@ -910,13 +877,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         output as { args?: unknown },
         output as { args?: unknown },
       );
       );
 
 
-      if (input.tool.toLowerCase() === 'task') {
-        divoomManager.onTaskStart({
-          parentSessionId: input.sessionID,
-          callId: input.callID,
-          args: output.args,
-        });
-      }
+      // No-op for divoom
     },
     },
 
 
     'command.execute.before': async (input, output) => {
     'command.execute.before': async (input, output) => {
@@ -1159,13 +1120,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           output as { output: unknown },
           output as { output: unknown },
         ),
         ),
       );
       );
-
-      if (input.tool.toLowerCase() === 'task') {
-        divoomManager.onTaskEnd({
-          parentSessionId: input.sessionID,
-          callId: input.callID,
-        });
-      }
     },
     },
   };
   };
 };
 };