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
23 changes: 16 additions & 7 deletions src/plugins/opencode/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,23 @@ function applyModeChange(mode) {
safeWriteFlag(flagPath, mode);
}

// Session-start logic — extracted so the `event` dispatcher (opencode >= 1.15)
// and any direct-key fallback share one implementation.
function handleSessionCreated() {
const mode = getDefaultMode();
if (mode === 'off') {
try { if (existsSync(flagPath)) unlinkSync(flagPath); } catch (e) {}
return;
}
safeWriteFlag(flagPath, mode);
}

export const CavemanPlugin = async (_ctx) => ({
'session.created': async () => {
const mode = getDefaultMode();
if (mode === 'off') {
try { if (existsSync(flagPath)) unlinkSync(flagPath); } catch (e) {}
return;
}
safeWriteFlag(flagPath, mode);
// opencode >= 1.15 dispatches session/lifecycle events through a single
// `event` handler; the older direct top-level `'session.created'` key is
// silently ignored. See https://opencode.ai/docs/plugins#events.
event: async ({ event }) => {
if (event && event.type === 'session.created') handleSessionCreated();
},

// opencode's TUI prompt-append hook fires before the prompt is sent to the
Expand Down
40 changes: 38 additions & 2 deletions tests/installer/opencode.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,47 @@ test('opencode plugin handles /caveman ultra and stop caveman via tui.prompt.app
assert.equal(fs.existsSync(flagPath), false, 'flag should be deleted after deactivation');
assert.equal(out2, undefined, 'no reinforcement when flag absent');

// session.created writes default mode
await handlers['session.created']();
// session.created (via opencode >= 1.15 `event` dispatcher) writes default mode
assert.equal(typeof handlers.event, 'function', 'event handler missing');
assert.equal(handlers['session.created'], undefined, 'direct session.created key must not be used (silently ignored in opencode >= 1.15)');
await handlers.event({ event: { type: 'session.created' } });
assert.equal(fs.readFileSync(flagPath, 'utf8'), 'full');
} finally {
fs.rmSync(xdg, { recursive: true, force: true });
fs.rmSync(shimDir, { recursive: true, force: true });
}
});

// ── 6. `event` dispatcher ignores unrelated event types ───────────────────
test('opencode event handler dispatches session.created and ignores other event types', async () => {
const xdg = freshTmpDir();
const shimDir = shimOpencode();
try {
const env = { ...process.env, XDG_CONFIG_HOME: xdg, PATH: pathWith(shimDir), NO_COLOR: '1' };
const r = runInstaller(['--only', 'opencode'], env);
assert.notEqual(r.status, 2);

const pluginPath = path.join(xdg, 'opencode', 'plugins', 'caveman', 'plugin.js');
const flagPath = path.join(xdg, 'opencode', '.caveman-active');
process.env.XDG_CONFIG_HOME = xdg;

const mod = await import(pathToFileURL(pluginPath).href + '?v=event-dispatch');
const factory = mod.default || mod.CavemanPlugin;
const handlers = await factory({});

// Unrelated event types must NOT write the flag.
await handlers.event({ event: { type: 'session.idle' } });
assert.equal(fs.existsSync(flagPath), false, 'unrelated event type must not write flag');
await handlers.event({ event: { type: 'message.completed' } });
assert.equal(fs.existsSync(flagPath), false, 'unrelated event type must not write flag');
await handlers.event({});
assert.equal(fs.existsSync(flagPath), false, 'missing event payload must not write flag');

// session.created must write the flag.
await handlers.event({ event: { type: 'session.created' } });
assert.equal(fs.readFileSync(flagPath, 'utf8'), 'full', 'session.created event must write flag');
} finally {
fs.rmSync(xdg, { recursive: true, force: true });
fs.rmSync(shimDir, { recursive: true, force: true });
}
});