Skip to content

Commit d7ae256

Browse files
authored
Merge pull request #622 from Opencode-DCP/dev
merge dev into main
2 parents 11f6517 + 7a9c38e commit d7ae256

77 files changed

Lines changed: 11118 additions & 1289 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/pr-checks.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
cache: "npm"
2121

2222
- name: Install dependencies
23-
run: npm ci
23+
run: npm ci --legacy-peer-deps
2424

2525
- name: Format check
2626
run: npm run format:check
@@ -31,6 +31,9 @@ jobs:
3131
- name: Build
3232
run: npm run build
3333

34+
- name: Test
35+
run: npm test
36+
3437
- name: Security audit
3538
run: npm audit --audit-level=high
3639
continue-on-error: false

‎.github/workflows/publish.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
4949
- name: Install dependencies
5050
if: steps.package.outputs.published == 'false'
51-
run: npm ci
51+
run: npm ci --legacy-peer-deps
5252

5353
- name: Format check
5454
if: steps.package.outputs.published == 'false'

‎CONTRIBUTING.md‎

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,183 @@ This arrangement ensures the project remains Open Source while providing a path
2323
4. Ensure all tests pass and the code is formatted.
2424
5. Submit a Pull Request.
2525

26-
We look forward to your contributions!
26+
## Development Setup
27+
28+
Use Node.js and npm. From your checkout:
29+
30+
```sh
31+
npm ci --legacy-peer-deps
32+
npm run build
33+
```
34+
35+
The install flag allows development against the different OpenTUI peer versions
36+
used by OpenCode V1 and V2. This also installs the bundled test logger's
37+
dependencies through the [tests/logger](tests/logger/) npm workspace.
38+
39+
Run the checks relevant to your changes before submitting a pull request:
40+
41+
```sh
42+
npm test # DCP and request-logger tests
43+
npm run typecheck # TypeScript validation
44+
npm run check:package # Build and validate the npm package
45+
npm run format:check # Formatting
46+
```
47+
48+
## Compatibility
49+
50+
DCP provides server and terminal integrations for OpenCode V1 and V2, using shared
51+
package entrypoints and `dcp.jsonc` settings. Exercise both hosts when changing
52+
shared behavior.
53+
54+
Use [package.json](package.json) for dependency requirements and
55+
[the lab Dockerfile](tests/lab/Dockerfile) for pinned integration-test versions.
56+
Host-specific behavior is implemented in [index.ts](index.ts),
57+
[tui.tsx](tui.tsx), and [lib/v2/](lib/v2/).
58+
59+
V2 uses `@4@` message IDs and `@b1@` summary IDs. Compression inputs include the
60+
whole marker; range summaries use `@b1@` placeholders for nested summaries.
61+
Message-mode priority labels look like `@4@ [high]`; `@blocked@` marks content
62+
that cannot be selected. V1 uses XML ID tags. Custom prompt overrides must describe
63+
the ID format of the host they run on.
64+
65+
## Local Installation
66+
67+
After building, add this checkout's absolute path to your OpenCode configuration.
68+
69+
For **V2**, use `opencode.json`:
70+
71+
```jsonc
72+
{
73+
"plugins": [{ "package": "/absolute/path/to/opencode-dynamic-context-pruning" }],
74+
"permissions": [{ "action": "compress", "resource": "*", "effect": "allow" }],
75+
}
76+
```
77+
78+
For **V1**, add the following to both `opencode.json` (server plugin) and the
79+
separate `tui.json` (panel):
80+
81+
```jsonc
82+
{ "plugin": ["/absolute/path/to/opencode-dynamic-context-pruning"] }
83+
```
84+
85+
## Manual Sandbox
86+
87+
The sandbox requires Docker, Node/npm, and saved OpenCode authentication.
88+
The request logger is included in [tests/logger](tests/logger/); only the DCP
89+
checkout is needed. Complete [Development Setup](#development-setup), then run:
90+
91+
```sh
92+
npm run sandbox # OpenCode V2
93+
npm run sandbox -- --v1 # OpenCode V1
94+
```
95+
96+
Each launch uses the latest stable OpenCode release for the selected major version,
97+
rebuilds DCP and the test logger, and prepares a clean Docker image. Run
98+
`npm run sandbox -- --help` for available options and defaults. Each launch copies
99+
all saved authentication from the matching host version: V1's `auth.json`, or V2's
100+
credential records and account selections. OpenCode handles provider authentication
101+
normally inside the container; copied credentials can be refreshed there without
102+
writing back to the host.
103+
104+
V1's auth file is under `$XDG_DATA_HOME/opencode` (normally
105+
`~/.local/share/opencode`). V2's database is located with `opencode2 debug paths db`,
106+
or the standard data directory when that command is unavailable. Set `DCP_AUTH_PATH`
107+
to select a different V1 auth file or V2 database. Credentials embedded in host
108+
configuration or environment variables are not copied. Custom provider definitions
109+
can be added to `opencode.json` in the sandbox's scratch workspace.
110+
111+
The sandbox has its own sessions, scratch workspace, and configuration under
112+
`~/.local/state/dcp-sandbox/`. V1 uses the `v1/` subdirectory, with a separate
113+
database. Your host project and normal OpenCode configuration are not mounted.
114+
Try `/dcp` for the panel or `/dcp-compress` for a compression pass.
115+
116+
```sh
117+
npm run sandbox -- --fresh # New profile; keep old runs
118+
npm run sandbox -- --logs # Latest log paths and capture counts
119+
npm run sandbox -- --path # Current profile's host directory
120+
npm run sandbox -- -- --continue # Resume a session
121+
npm run sandbox -- --opencode VERSION # Use an exact release for this launch
122+
npm run sandbox -- --transport http # Select V2's transport
123+
npm run sandbox -- --model PROVIDER/MODEL # Select a model available to your account
124+
```
125+
126+
Replace `VERSION`, `PROVIDER`, and `MODEL` with the release and model you want to test.
127+
Without a saved model choice, OpenCode selects its default. A V2 transport override
128+
applies to the selected model.
129+
Add `--v1` to manage the V1 sandbox. Model and transport choices persist.
130+
An exact version override applies only to that launch; otherwise the latest stable
131+
release is selected. `--fresh` selects a new profile for subsequent launches.
132+
You can edit `dcp.jsonc` and CLI preferences; `opencode.json` is launcher-managed.
133+
Set `DCP_SANDBOX_DIR` to choose another state directory.
134+
135+
For a shortcut on Linux, run from the checkout:
136+
137+
```sh
138+
mkdir -p ~/.local/bin
139+
ln -s "$PWD/scripts/sandbox.mjs" ~/.local/bin/dcp-sandbox
140+
dcp-sandbox
141+
```
142+
143+
### Request Logs
144+
145+
The logger is development-only tooling and is excluded from DCP's published npm
146+
package. Each launch has `raw/` and `readable/` directories under its timestamped log folder.
147+
The launcher manages the WebSocket relay and readable-log watcher. Requests appear
148+
as they are sent; assembled responses appear when they finish, while the session
149+
stays open. `--logs` only shows paths and capture counts.
150+
151+
Start at `readable/index.json`, then a session's numbered request folders:
152+
153+
```text
154+
readable/<session>/0001_primary_websocket/
155+
request.json # Pretty-printed body actually sent
156+
response.json # Assistant content, parsed tool calls, token totals, errors
157+
meta.json # Timing, completion, transport, raw source, continuation ID
158+
```
159+
160+
V2's full pre-transport snapshots are in each session's `context/` directory.
161+
WebSocket continuation requests remain deltas with `previous_response_id`.
162+
Partial and failed responses are marked in metadata. Full provider metadata,
163+
original HTTP bytes, and WebSocket frames remain available in `raw/`.
164+
165+
## Integration Tests
166+
167+
The containerized lab exercises packed plugins on V1 and V2, including saved-auth
168+
copying, HTTP and WebSocket compression, commands, permissions, concurrent sessions,
169+
persistence, and native compaction. It uses a local mock provider without live credentials.
170+
171+
After [Development Setup](#development-setup), build
172+
[tests/lab/Dockerfile](tests/lab/Dockerfile) using the image tag expected by
173+
[scripts/lab.mjs](scripts/lab.mjs), then run:
174+
175+
```sh
176+
node scripts/lab.mjs
177+
```
178+
179+
The runner prints its output directory under `/tmp/opencode/dcp-lab/`. Set
180+
`DCP_LAB_DIR` to override it. Add `--built` to reuse an existing DCP build.
181+
For real-provider checks, `node scripts/lab.mjs --live` uses the current build and
182+
saved V2 authentication. Its OpenAI Responses scenarios require access to the model
183+
configured in [tests/lab/live.mjs](tests/lab/live.mjs).
184+
185+
Inspect capture summaries without opening large transcripts:
186+
187+
```sh
188+
node tests/lab/inspect.mjs <log-directory>
189+
```
190+
191+
Terminal-panel checks require `uv` and reuse a completed lab run:
192+
193+
```sh
194+
uv run --with pexpect --with pyte tests/lab/ui.py <lab-output-directory> v2
195+
uv run --with pexpect --with pyte tests/lab/ui.py <lab-output-directory> v1
196+
```
197+
198+
These check the panel, Context, Stats, persisted manual-mode toggle, and closing
199+
the dialog. They also check mouse-wheel scrolling and resizing down to 20 rows,
200+
with back/close buttons remaining visible. Terminal transcripts and screen
201+
snapshots are saved in the lab output.
202+
203+
To check another V2 release, build the lab image with `--build-arg V2=VERSION`
204+
and pass that image's tag as the final argument to `ui.py`. Use a separate copy of
205+
the lab output when testing different releases so their databases stay independent.

‎README.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ opencode plugin @tarquinen/opencode-dcp@latest --global
1717

1818
This installs the package and adds it to your global OpenCode config.
1919

20-
## Project Status
20+
## Related Project
2121

22-
Development on DCP has slowed because most new context-management work has moved to [Sleev](https://sleev.ai) and the `sleev` CLI. Sleev is a local proxy for Claude Code, Codex, and OpenCode that builds on DCP's core ideas with newer context-management features and will work with any harness/client.
23-
24-
DCP remains available for OpenCode plugin users, but new features are landing in Sleev first. If you are starting fresh, we recommend trying Sleev:
22+
[Sleev](https://sleev.ai) is a local proxy for coding agents, including Claude Code,
23+
Codex, and OpenCode. It provides context management through the `sleev` CLI:
2524

2625
```bash
2726
npm i -g sleev
@@ -49,7 +48,7 @@ Identifies repeated tool calls (same tool, same arguments) and keeps only the mo
4948

5049
### Purge Errors
5150

52-
Prunes inputs from errored tool calls after a configurable number of turns (default: 4). Error messages are preserved; only the potentially large input content is removed. Recalculated on compress tool use.
51+
Prunes inputs from errored tool calls after a configurable number of turns. Error messages are preserved; only the potentially large input content is removed. Recalculated on compress tool use.
5352

5453
## Configuration
5554

@@ -62,7 +61,7 @@ DCP uses its own config file, searched in order:
6261
Each level overrides the previous, so project settings take priority over global. Restart OpenCode after making config changes.
6362

6463
> [!NOTE]
65-
> If you use models with smaller context windows, such as GitHub Copilot models or local models, lower `compress.minContextLimit` and `compress.maxContextLimit` in your configuration to match the available context.
64+
> If your model has a smaller context window, lower `compress.minContextLimit` and `compress.maxContextLimit` in your configuration to match the available context.
6665
6766
> [!IMPORTANT]
6867
> Defaults are applied automatically. Expand this if you want to review or override settings.
@@ -138,14 +137,12 @@ Each level overrides the previous, so project settings take priority over global
138137
// Accepts: number or "X%".
139138
// Example:
140139
// "modelMaxLimits": {
141-
// "openai/gpt-5.3-codex": 120000,
142-
// "anthropic/claude-sonnet-4.6": "80%"
140+
// "provider/model": "80%"
143141
// },
144142
// Optional per-model override for minContextLimit.
145143
// If present, this wins over the global minContextLimit.
146144
// "modelMinLimits": {
147-
// "openai/gpt-5.3-codex": 50000,
148-
// "anthropic/claude-sonnet-4.6": "25%"
145+
// "provider/model": "25%"
149146
// },
150147
// How often the context-limit nudge fires (1 = every fetch, 5 = every 5th)
151148
"nudgeFrequency": 5,
@@ -194,7 +191,7 @@ DCP provides a TUI panel and one prompt-producing slash command:
194191

195192
### Prompt Overrides
196193

197-
DCP exposes six editable prompts:
194+
DCP exposes the following editable prompts:
198195

199196
- `system`
200197
- `compress-range`
@@ -224,15 +221,18 @@ For the `compress` tool, `compress.protectedTools` ensures specific tool outputs
224221

225222
LLM providers cache prompts based on exact prefix matching. When DCP prunes content, it changes messages, which invalidates cached prefixes from that point forward.
226223

227-
**Trade-off:** You lose some cache reads but gain token savings from reduced context size and fewer hallucinations from stale context. In most cases, especially in long sessions, the savings outweigh the cache miss cost.
228-
229-
> [!NOTE]
230-
> In testing, cache hit rates were approximately 85% with DCP vs 90% without.
224+
**Trade-off:** Pruning reduces context size but can increase cache misses. The cost
225+
balance depends on your conversation, compression frequency, and provider pricing.
231226

232227
**No impact for:**
233228

234229
- **Request-based billing** — Some providers charge per request, not tokens.
235-
- **Uniform token pricing** — Providers like Cerebras that bill cached and uncached tokens at the same rate.
230+
- **Uniform token pricing** — Providers that bill cached and uncached tokens at the same rate.
231+
232+
## Contributing
233+
234+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, local installation,
235+
and testing with the V1/V2 sandbox.
236236

237237
## License
238238

‎index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "./lib/hooks"
1919
import { configureClientAuth, isSecureMode } from "./lib/auth"
2020
import { startAutoUpdate } from "./lib/update"
21+
import { setup } from "./lib/v2"
2122

2223
const server: Plugin = (async (ctx) => {
2324
const config = getConfig(ctx)
@@ -134,4 +135,4 @@ const server: Plugin = (async (ctx) => {
134135
}
135136
}) satisfies Plugin
136137

137-
export default server
138+
export default { id: "opencode-dcp", setup, server }

‎lib/commands/decompress.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface DecompressCommandContext {
2323

2424
function parseBlockIdArg(arg: string): number | null {
2525
const normalized = arg.trim().toLowerCase()
26-
const blockRef = parseBlockRef(normalized)
26+
const blockRef = parseBlockRef(normalized) ?? parseBlockRef(normalized, "compact")
2727
if (blockRef !== null) {
2828
return blockRef
2929
}

‎lib/commands/recompress.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface RecompressCommandContext {
2323

2424
function parseBlockIdArg(arg: string): number | null {
2525
const normalized = arg.trim().toLowerCase()
26-
const blockRef = parseBlockRef(normalized)
26+
const blockRef = parseBlockRef(normalized) ?? parseBlockRef(normalized, "compact")
2727
if (blockRef !== null) {
2828
return blockRef
2929
}

‎lib/commands/sweep.ts‎

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { isIgnoredUserMessage } from "../messages/query"
1717
import { buildToolIdList } from "../messages/utils"
1818
import { saveSessionState } from "../state/persistence"
1919
import { isMessageCompacted } from "../state/utils"
20-
import {
21-
getFilePathsFromParameters,
22-
isFilePathProtected,
23-
isToolNameProtected,
24-
} from "../protected-patterns"
20+
import { isToolProtected } from "../protected-patterns"
2521
import { syncToolCache } from "../state/tool-cache"
2622

2723
export interface SweepCommandContext {
@@ -176,15 +172,18 @@ export async function handleSweepCommand(ctx: SweepCommandContext): Promise<void
176172
if (!entry) {
177173
return true
178174
}
179-
if (isToolNameProtected(entry.tool, protectedTools)) {
175+
if (
176+
isToolProtected(
177+
entry.tool,
178+
entry.parameters,
179+
protectedTools,
180+
config.protectedFilePatterns,
181+
entry.metadata,
182+
)
183+
) {
180184
logger.debug(`Sweep: skipping protected tool ${entry.tool} (${id})`)
181185
return false
182186
}
183-
const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters)
184-
if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
185-
logger.debug(`Sweep: skipping protected file path(s) ${filePaths.join(", ")} (${id})`)
186-
return false
187-
}
188187
return true
189188
})
190189

@@ -194,14 +193,13 @@ export async function handleSweepCommand(ctx: SweepCommandContext): Promise<void
194193
if (!entry) {
195194
return false
196195
}
197-
if (isToolNameProtected(entry.tool, protectedTools)) {
198-
return true
199-
}
200-
const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters)
201-
if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
202-
return true
203-
}
204-
return false
196+
return isToolProtected(
197+
entry.tool,
198+
entry.parameters,
199+
protectedTools,
200+
config.protectedFilePatterns,
201+
entry.metadata,
202+
)
205203
}).length
206204

207205
if (newToolIds.length === 0) {

0 commit comments

Comments
 (0)