blob: 0cb3dc8a3a6ddf4164c043cde3f380ed91bfb1ba [file]
import type { CuAction, CuPoint } from '@maka/core';
import type { CuOverlayHook, CuOverlayHookContext, CuPresentationFence } from '@maka/runtime';
export type CursorActionKind = 'move' | 'click' | 'drag' | 'scroll';
export interface CursorMoveInput {
actionId: string;
sessionId: string;
screenX: number;
screenY: number;
kind: CursorActionKind;
pressed?: boolean;
instant?: boolean;
/**
* Keep the cursor at its top window level once this motion settles, instead
* of letting it sink toward the target's own layer. Only an explicit `false`
* lets it sink: a caller that says nothing has offered no evidence that the
* target is exposed, and an unseen cursor is the worse failure.
*/
keepElevated?: boolean;
/**
* The window a resting cursor should be ordered directly above.
*
* Absent when the action is not bound to a window, in which case the cursor
* falls back to resting at a fixed level.
*/
targetWindowId?: number;
}
export interface CursorCompleteInput extends CursorMoveInput {
pulse: boolean;
}
export interface CursorCancelInput {
actionId: string;
sessionId: string;
}
export interface OverlayCursorSink {
ensure(sessionId: string): void;
move(input: CursorMoveInput): CuPresentationFence | void;
complete(input: CursorCompleteInput): void;
cancel(input: CursorCancelInput): void;
}
const RESOLVED_PRESENTATION_FENCE: CuPresentationFence = {
readyForInteraction: Promise.resolve(),
finished: Promise.resolve(),
};
function beginCoordinateOf(action: CuAction): CuPoint | undefined {
switch (action.type) {
case 'left_click_drag':
return action.startCoordinate;
case 'mouse_move':
case 'left_click':
case 'right_click':
case 'middle_click':
case 'double_click':
case 'triple_click':
case 'left_mouse_down':
case 'left_mouse_up':
case 'scroll':
return action.coordinate;
default:
return undefined;
}
}
function endCoordinateOf(action: CuAction): CuPoint | undefined {
switch (action.type) {
case 'mouse_move':
case 'left_click':
case 'right_click':
case 'middle_click':
case 'double_click':
case 'triple_click':
case 'left_mouse_down':
case 'left_mouse_up':
case 'scroll':
case 'left_click_drag':
return action.coordinate;
default:
return undefined;
}
}
function kindOf(action: CuAction): CursorActionKind {
switch (action.type) {
case 'left_click':
case 'right_click':
case 'middle_click':
case 'double_click':
case 'triple_click':
case 'left_mouse_down':
case 'left_mouse_up':
return 'click';
case 'left_click_drag':
return 'drag';
case 'scroll':
return 'scroll';
default:
return 'move';
}
}
/**
* Codex keeps its cursor above every other window while any of two reasons
* holds — the target app is frontmost (or has a menu open), and the cursor has
* just been launched to a new position — and only lets it sink into the
* target's own layer once the target is genuinely the window under the cursor.
*
* Maka has one of those two reasons available and not the other. The launch
* half is the sink's own business (the presentation layer is what knows when a
* motion settles). The frontmost/covered half needs a per-observation record of
* what is stacked over the target, which the runtime does not collect, so it is
* deliberately not modelled here rather than declared as a field nothing sets.
*
* What is left is the ordering: with a window id to order against, the cursor
* does not need a level to stay visible — its position in the target's own
* z-order is what keeps it readable, exactly as it is for Codex. Staying
* elevated on top of that is what put the cursor over the user's own windows.
* With no window to order against there is nothing to sink to, so it stays up:
* an unseen cursor is the failure this whole path exists to avoid.
*/
function keepElevated(context: CuOverlayHookContext): boolean {
return context.targetWindowId === undefined;
}
export function createComputerUseOverlayHook(controller: OverlayCursorSink): CuOverlayHook {
return {
onActionBegin(action, context) {
const declaredPoint = beginCoordinateOf(action);
const screenPoint = context.presentationScreenPoint;
if (!declaredPoint || !screenPoint) {
controller.ensure(context.sessionId);
return RESOLVED_PRESENTATION_FENCE;
}
return controller.move({
actionId: context.toolCallId,
sessionId: context.sessionId,
screenX: screenPoint.x,
screenY: screenPoint.y,
kind: kindOf(action),
instant: action.type !== 'mouse_move',
keepElevated: keepElevated(context),
...(context.targetWindowId !== undefined ? { targetWindowId: context.targetWindowId } : {}),
});
},
onActionEnd(action, result, context) {
if (!endCoordinateOf(action)) return;
if (!result?.outcome.ok) {
controller.cancel({
actionId: context.toolCallId,
sessionId: context.sessionId,
});
return;
}
// Where the executor says the pointer ended, and failing that, where the
// cursor was sent.
//
// Only the coordinate paths report a landing point; `runSemantic` returns
// none, because an element action never resolves to a pointer position at
// all. Requiring one meant every semantic action — the whole accessibility
// path, which is the only path Maka dispatches on by default — ended in
// `cancel()`. The cursor flew to the control and was then wiped instead of
// landing on it, so what a person saw was an arrow crossing the screen and
// vanishing, never touching anything.
//
// The fallback is not a guess: `presentationScreenPoint` is the point this
// same action was addressed to, computed from the element's own frame.
const screenPoint = result.resolvedScreenPoint ?? context.presentationScreenPoint;
if (!screenPoint) {
controller.cancel({
actionId: context.toolCallId,
sessionId: context.sessionId,
});
return;
}
const kind = kindOf(action);
controller.complete({
actionId: context.toolCallId,
sessionId: context.sessionId,
screenX: screenPoint.x,
screenY: screenPoint.y,
kind,
pulse: result.outcome.ok && (kind === 'click' || kind === 'drag'),
// `complete` raises the cursor for the landing, so it has to know
// where to come back down to.
...(context.targetWindowId !== undefined ? { targetWindowId: context.targetWindowId } : {}),
});
},
};
}