Bladeren bron

Remove Divoom display integration

Alvin Unreal 1 maand geleden
bovenliggende
commit
1d427bdcec

+ 1 - 2
README.ja-JP.md

@@ -506,8 +506,7 @@ ping all agents
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[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)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
 | **[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 |
 | **[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 |
-| **[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 |
 
 ### ⚙️ Config & Reference

+ 0 - 1
README.zh-CN.md

@@ -502,7 +502,6 @@ ping all agents
 | **[代码地图 (Codemap) (docs/codemap.md)](docs/codemap.md)** | 生成层级代码地图,快速理解大型代码库 |
 | **[克隆依赖 (Clonedeps) (docs/clonedeps.md)](docs/clonedeps.md)** | 将选定的依赖源码克隆到被忽略的本地工作区中以供检查 |
 | **[访谈式生成 (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.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 |
-| `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.layout` | string | `"main-vertical"` | Legacy alias for `multiplexer.layout` |
 | `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
 > 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 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": {
       "type": "object",
       "properties": {

+ 1 - 2
package.json

@@ -52,8 +52,7 @@
     "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: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",
     "contributors:add": "all-contributors add",
     "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.d.ts',
   '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',
   'src/skills/simplify/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),
     interview: deepMerge(base.interview, override.interview),
     backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
-    divoom: deepMerge(base.divoom, override.divoom),
     fallback: deepMerge(base.fallback, override.fallback),
     council: deepMerge(base.council, override.council),
     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 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({
   enabled: z.boolean().default(true),
   timeoutMs: z.number().min(0).default(15000),
@@ -309,7 +285,6 @@ export const PluginConfigSchema = z
     websearch: WebsearchConfigSchema.optional(),
     interview: InterviewConfigSchema.optional(),
     backgroundJobs: BackgroundJobsConfigSchema.optional(),
-    divoom: DivoomConfigSchema.optional(),
     fallback: FailoverConfigSchema.optional(),
     council: CouncilConfigSchema.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,
 } from './config/runtime-preset';
 import { CouncilManager } from './council';
-import { createDivoomManager } from './divoom/manager';
 import {
   createApplyPatchHook,
   createAutoUpdateCheckerHook,
@@ -142,7 +141,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   let interviewManager: ReturnType<typeof createInterviewManager>;
   let presetManager: ReturnType<typeof createPresetManager>;
   let companionManager: CompanionManager;
-  let divoomManager: ReturnType<typeof createDivoomManager>;
   let councilTools: Record<string, unknown>;
   let cancelTaskTools: Record<string, unknown>;
   let webfetch: ReturnType<typeof createWebfetchTool>;
@@ -327,7 +325,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       ctx.directory,
       config.companion,
     );
-    divoomManager = createDivoomManager(config.divoom);
     cancelTaskTools = createCancelTaskTool({
       client: ctx.client,
       backgroundJobBoard,
@@ -396,7 +393,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     }
   });
 
-  divoomManager.onPluginLoad();
   companionManager.onLoad();
 
   return {
@@ -813,13 +809,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         event.type === 'permission.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();
       }
 
@@ -828,13 +817,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         event.type === 'question.replied' ||
         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();
       }
 
@@ -843,14 +825,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           | { sessionID?: string; status?: { type?: string } }
           | undefined;
         const sessionID = props?.sessionID;
-        const isOrch = sessionID
-          ? sessionAgentMap.get(sessionID) === 'orchestrator'
-          : false;
-        divoomManager.onOrchestratorStatus({
-          sessionId: sessionID,
-          status: props?.status?.type,
-          isOrchestrator: isOrch,
-        });
         companionManager.onSessionStatus({
           sessionId: sessionID,
           agent: sessionID ? sessionAgentMap.get(sessionID) : undefined,
@@ -863,13 +837,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           | { info?: { id?: string }; sessionID?: string }
           | undefined;
         const sessionID = props?.info?.id ?? props?.sessionID;
-        const isOrch = sessionID
-          ? sessionAgentMap.get(sessionID) === 'orchestrator'
-          : false;
-        divoomManager.onSessionDeleted({
-          sessionId: sessionID,
-          isOrchestrator: isOrch,
-        });
         companionManager.onSessionDeleted(sessionID);
       }
 
@@ -910,13 +877,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         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) => {
@@ -1159,13 +1120,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
           output as { output: unknown },
         ),
       );
-
-      if (input.tool.toLowerCase() === 'task') {
-        divoomManager.onTaskEnd({
-          parentSessionId: input.sessionID,
-          callId: input.callID,
-        });
-      }
     },
   };
 };