Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Choose the instructions that match the CLI you run:
| OpenCode version | How to identify it | Instructions |
| --- | --- | --- |
| OpenCode 1 stable | You run `opencode` and `opencode --version` prints `1.x` | [OpenCode 1](#opencode-1-stable) |
| OpenCode 2 preview | You run `opencode2` | [OpenCode 2](#opencode-2-preview) |
| OpenCode 2 | You run `opencode` and `opencode --version` prints `2.x` | [OpenCode 2](#opencode-2) |

Do not mix the configuration formats. OpenCode 1 uses `plugin` and `tui.json`; OpenCode 2 uses `plugins` and the global `~/.config/opencode/cli.json`.

Expand Down Expand Up @@ -63,9 +63,15 @@ OpenCode detects both package entrypoints and writes the plugin into the server
}
```

### OpenCode 2 Preview
### OpenCode 2

This plugin supports OpenCode 2 preview `0.0.0-next-17055` while remaining compatible with OpenCode 1. Add the package to both V2 plugin lists.
This plugin supports OpenCode 2 (`2.x`) while remaining compatible with OpenCode 1.

```bash
opencode plugin add @prevalentware/opencode-loop-plugin
```

For manual installation, add the package to both V2 plugin lists.

`opencode.json`:

Expand All @@ -85,7 +91,7 @@ This plugin supports OpenCode 2 preview `0.0.0-next-17055` while remaining compa

OpenCode 2 does not read the V1 `tui.json` file. The server entrypoint is loaded from `opencode.json`; the sidebar and palette integration are loaded from the global `~/.config/opencode/cli.json`.

The OpenCode 2 plugin API is still in preview. This release targets the exact preview above; later previews may require a plugin update. V2 supports the `/loop` command, all loop tools, persistent state, idle scheduling, dynamic loops, Plan-mode safety, session cleanup, and TUI sidebar/palette integration. The dedicated V1 compaction-context hook has no V2 equivalent in this preview. V2 still injects the active-loop system reminder before model dispatch, but cannot append the separate loop block directly to a compaction operation.
V2 supports the `/loop` command, all loop tools, persistent state, idle scheduling, dynamic loops, Plan-mode safety, session cleanup, and TUI sidebar/palette integration. The dedicated V1 compaction-context hook has no V2 equivalent in this release. V2 still injects the active-loop system reminder before model dispatch, but cannot append the separate loop block directly to a compaction operation.

## Usage

Expand Down
545 changes: 508 additions & 37 deletions bun.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions dist/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ async function createLoop(sessionID, options) {
const maxLoops = positiveIntegerOrNull(options.maxLoopsPerSession) ?? DEFAULT_MAX_LOOPS_PER_SESSION;
const agent = typeof options.agent === "string" && options.agent.trim() ? options.agent.trim() : null;
return mutate((state) => {
const open = Object.values(state.loops).filter((loop2) => loop2.sessionID === sessionID && isOpen(loop2.status));
const open = Object.values(state.loops).filter((loop) => loop.sessionID === sessionID && isOpen(loop.status));
if (open.length >= maxLoops) {
throw new Error(`this session already has ${open.length} open loop(s); stop one before creating another (limit ${maxLoops})`);
}
Expand Down Expand Up @@ -687,9 +687,9 @@ var server = async ({ client }, options) => {
}
async function runDueForSession(sessionID) {
const loops = await activeLoops(sessionID);
const now2 = Date.now();
const now = Date.now();
for (const loop of loops) {
if (loop.nextRunAt == null || loop.nextRunAt > now2)
if (loop.nextRunAt == null || loop.nextRunAt > now)
continue;
await runDue(loop.id);
if (busySessions.has(sessionID))
Expand Down Expand Up @@ -1084,9 +1084,9 @@ async function setupV2(context) {
}
async function runDueForSession(sessionID) {
const loops = await activeLoops(sessionID);
const now2 = Date.now();
const now = Date.now();
for (const loop of loops) {
if (loop.nextRunAt == null || loop.nextRunAt > now2)
if (loop.nextRunAt == null || loop.nextRunAt > now)
continue;
await runDue(loop.id);
if (busySessions.has(sessionID))
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@opencode-ai/plugin-v2": "npm:@opencode-ai/plugin@0.0.0-beta-18593",
"@opencode-ai/theme": "0.0.0-beta-18593",
"@opencode/plugin": "2.0.16",
"@opencode/client": "2.0.16",
"@opencode/theme": "2.0.16",
"@opentui/core": "0.5.9",
"@types/bun": "^1.3.13",
"eslint": "^10.3.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2"
},
"engines": {
"opencode": ">=1.17.1"
"opencode": ">=1.17.1 || >=2.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Config, Plugin } from "@opencode-ai/plugin"
import type * as PluginV2 from "@opencode-ai/plugin-v2"
import type { Info as ToolV2Info } from "@opencode-ai/plugin-v2/promise/tool"
import type * as PluginV2 from "@opencode/plugin"
import type { Info as ToolV2Info } from "@opencode/plugin/promise/tool"
import type { Tool as ToolSchema } from "@opencode-ai/schema/tool"
import { z } from "zod"
import {
Expand Down
8 changes: 4 additions & 4 deletions src/tui.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @jsxImportSource @opentui/solid */
import type { TuiCommand, TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
import type { Plugin as TuiPluginV2 } from "@opencode-ai/plugin-v2/tui"
import type { SessionMessageInfo } from "@opencode-ai/client"
import type { Plugin as TuiPluginV2 } from "@opencode/plugin/tui"
import type { SessionMessageInfo } from "@opencode/client"
import { createElement, insert, setProp } from "@opentui/solid"
import { createEffect, createMemo, createSignal, For, onCleanup, Show, untrack } from "solid-js"

Expand Down Expand Up @@ -458,9 +458,9 @@ function LoopSidebarV2(api: TuiPluginV2.Context, sessionID: string) {
const loops = open()
if (loops.length === 0) return null
return box({}, [
text({ fg: api.theme.text.default }, ["Loops"]),
text({ fg: api.theme.text.base }, ["Loops"]),
...loops.map((loop) =>
text({ fg: api.theme.text.subdued }, [
text({ fg: api.theme.text.muted }, [
`${loop.status === "paused" ? "paused " : ""}${loopLine(loop, nowMs())}`,
]),
),
Expand Down
2 changes: 1 addition & 1 deletion test/tui-v2.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import type { SessionMessageAssistantTool, SessionMessageInfo } from "@opencode-ai/client"
import type { SessionMessageAssistantTool, SessionMessageInfo } from "@opencode/client"
import { createSignal } from "solid-js"
import { createStore, type Store } from "solid-js/store"
import plugin, { loopsFromV2Messages, setupTuiV2 } from "../src/tui"
Expand Down