mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-09 01:51:39 +00:00
feat(opencode): support mid-turn steering (#1981)
* feat(opencode): support mid-turn steering Register the per-message Queue/Steer RPC on the OpenCode remote launcher so a waiting-queue row can be delivered into the active turn as a concurrent ACP session/prompt (soft inject, no cancel, no mode/model/effort mutation), reusing the existing reservation/delivery-state protocol shared with Codex and Cursor ACP. The steer gate is closed synchronously by every cancelling or terminal path (normal-prompt finally, abort, switch/exit, teardown, stderr stall cancel), each steer operation is tracked from the moment its reservation is taken until completion plus all restore/consumed bookkeeping settles, and the foreground prompt drains those operations — abort-aware through the original turn signal — before ready, thinking=false or the next batch. steerability is published through the existing agentState.steeringActive flag so the web's queued Steer button is reachable. Gate the feature on the shared steering flavors (now including opencode) and update the hub's supported-flavor wording. * fix(opencode): acknowledge native steering acceptance * fix(opencode): preserve steering delivery outcomes * docs(opencode): remove obsolete abort-race comment * fix(opencode): preserve steering resolution ownership * fix(opencode): isolate ambiguous steering attempts * fix(opencode): compare resolved steering settings * fix(opencode): reconcile steering receipts after dispatch * fix(opencode): distinguish pending steering delivery outcomes * fix(opencode): restore receipts after successful native responses Track native input requests separately from detached steer operations. Resume receipt correlation after inputs quarantined by cancellation return their own successful responses, while keeping uncertain writes on completion-based confirmation. * fix(opencode): bound receipt proof to unambiguous native inputs * feat(opencode): steer v2 inputs through native inbox * fix(opencode): preserve v2 restoration and cancellation ownership * docs(opencode): describe v2 inbox steering confirmation * fix(steer): expire Claude confirmation with its owning worker
This commit is contained in:
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildCliArgs } from '@/runner/run';
|
||||
import { parseRemoteAgentCommandOptions } from '@/commands/agentCommandOptions';
|
||||
import { OPENCODE_PERMISSION_MODES } from '@hapi/protocol/modes';
|
||||
import type { MessageQueue2 } from '@/utils/MessageQueue2';
|
||||
import type { OpencodeMode } from './types';
|
||||
|
||||
const mockOpencodeSession = vi.hoisted(() => ({
|
||||
setModel: vi.fn(),
|
||||
@@ -471,6 +473,20 @@ describe('runOpencode set-session-config handler', () => {
|
||||
expect(harness.session.sendAgentMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delegates native v2 cancellation before generic queue removal and clears the delegate on teardown', async () => {
|
||||
await runOpencode({ startedBy: 'runner' });
|
||||
const bind = harness.opencodeLoopArgs[0].onSteerCancelHandlerChange as
|
||||
(handler: import('./utils/opencodeV2Steer').OpencodeSteerCancelHandler | null) => void;
|
||||
const cancel = harness.session.onCancelQueuedMessage.mock.calls[0][0] as
|
||||
(id: string) => boolean | Promise<unknown>;
|
||||
const native = vi.fn((id: string) => id === 'native' ? Promise.resolve('in-flight' as const) : undefined);
|
||||
bind(native);
|
||||
await expect(cancel('native')).resolves.toBe('in-flight');
|
||||
expect(native).toHaveBeenCalledWith('native');
|
||||
bind(null);
|
||||
expect(cancel('missing')).toBe(false);
|
||||
});
|
||||
|
||||
it('releases the clear transition latch when the queued /clear is cancelled', async () => {
|
||||
await runOpencode({ startedBy: 'runner' });
|
||||
|
||||
@@ -894,4 +910,25 @@ describe('runOpencode set-session-config handler', () => {
|
||||
expect(isLocalIdCancelled!(lastLocalId)).toBe(true);
|
||||
expect(isLocalIdCancelled!(lastLocalId)).toBe(false);
|
||||
});
|
||||
it('preserves dispatch and consumption outcomes through the registered cancel callback', async () => {
|
||||
await runOpencode({});
|
||||
const queue = harness.opencodeLoopArgs[0]?.messageQueue as MessageQueue2<OpencodeMode>;
|
||||
const cancel = harness.session.onCancelQueuedMessage.mock.calls[0][0] as
|
||||
(id: string) => boolean | 'in-flight' | 'indeterminate' | 'consumed';
|
||||
queue.push('correction', { permissionMode: 'default', model: null }, 'reserved');
|
||||
const reservation = queue.takeByLocalId('reserved')!;
|
||||
expect(queue.beginReservationDispatch(reservation)).toBe(true);
|
||||
expect(cancel('reserved')).toBe('in-flight');
|
||||
expect(cancel('reserved')).toBe('in-flight');
|
||||
expect(queue.commitReservation(reservation)).toBe(true);
|
||||
expect(cancel('reserved')).toBe('consumed');
|
||||
vi.spyOn(queue, 'cancelByLocalId').mockReturnValueOnce('indeterminate');
|
||||
expect(cancel('unresolved')).toBe('indeterminate');
|
||||
queue.push('user-resolvable', { permissionMode: 'default', model: null }, 'unknown');
|
||||
const unknown = queue.takeByLocalId('unknown')!;
|
||||
queue.beginReservationDispatch(unknown);
|
||||
queue.markReservationIndeterminate(unknown);
|
||||
expect(cancel('unknown')).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user