refactor: replace store-based keyboard action requests with direct dispatcher and simplify agent attention clearing

This commit is contained in:
Mohamed Boudra
2026-03-06 14:49:06 +07:00
parent 885b575d14
commit 5b270444d5
25 changed files with 617 additions and 1355 deletions
@@ -2,11 +2,28 @@ import { describe, expect, it } from "vitest";
import { __private__ } from "./use-agent-initialization";
describe("useAgentInitialization timeline request policy", () => {
it("uses canonical tail bootstrap when history has not synced yet", () => {
expect(
__private__.deriveInitialTimelineRequest({
cursor: {
epoch: "epoch-1",
seq: 42,
},
hasAuthoritativeHistory: false,
initialTimelineLimit: 200,
})
).toEqual({
direction: "tail",
limit: 200,
projection: "canonical",
});
});
it("uses canonical tail bootstrap when cursor is missing", () => {
expect(
__private__.buildInitialTimelineRequest({
cursor: undefined,
hasLocalTail: false,
__private__.deriveInitialTimelineRequest({
cursor: null,
hasAuthoritativeHistory: true,
initialTimelineLimit: 200,
})
).toEqual({
@@ -16,31 +33,14 @@ describe("useAgentInitialization timeline request policy", () => {
});
});
it("uses canonical tail bootstrap when cursor exists but local tail is empty", () => {
it("uses canonical catch-up after the current cursor once history is synced", () => {
expect(
__private__.buildInitialTimelineRequest({
__private__.deriveInitialTimelineRequest({
cursor: {
epoch: "epoch-1",
endSeq: 42,
seq: 42,
},
hasLocalTail: false,
initialTimelineLimit: 200,
})
).toEqual({
direction: "tail",
limit: 200,
projection: "canonical",
});
});
it("uses canonical catch-up after the current cursor when local tail exists", () => {
expect(
__private__.buildInitialTimelineRequest({
cursor: {
epoch: "epoch-1",
endSeq: 42,
},
hasLocalTail: true,
hasAuthoritativeHistory: true,
initialTimelineLimit: 200,
})
).toEqual({
@@ -53,9 +53,9 @@ describe("useAgentInitialization timeline request policy", () => {
it("supports unbounded tail bootstrap policy", () => {
expect(
__private__.buildInitialTimelineRequest({
cursor: undefined,
hasLocalTail: false,
__private__.deriveInitialTimelineRequest({
cursor: null,
hasAuthoritativeHistory: false,
initialTimelineLimit: 0,
})
).toEqual({
@@ -3,7 +3,6 @@ import { Platform } from "react-native";
import { useSessionStore } from "@/stores/session-store";
import type {
DaemonClient,
FetchAgentTimelineOptions,
} from "@server/client/daemon-client";
import {
attachInitTimeout,
@@ -12,43 +11,12 @@ import {
getInitKey,
rejectInitDeferred,
} from "@/utils/agent-initialization";
import { deriveInitialTimelineRequest } from "@/contexts/session-timeline-bootstrap-policy";
const INIT_TIMEOUT_MS = 5 * 60_000;
const NATIVE_INITIAL_TIMELINE_LIMIT = 200;
const UNBOUNDED_TIMELINE_LIMIT = 0;
type TimelineCursorState = {
epoch: string;
endSeq: number;
};
type BuildInitialTimelineRequestInput = {
cursor: TimelineCursorState | undefined;
hasLocalTail: boolean;
initialTimelineLimit: number;
};
function buildInitialTimelineRequest(
input: BuildInitialTimelineRequestInput
): FetchAgentTimelineOptions {
const { cursor, hasLocalTail, initialTimelineLimit } = input;
if (!cursor || !hasLocalTail) {
return {
direction: "tail",
limit: initialTimelineLimit,
projection: "canonical",
};
}
return {
direction: "after",
cursor: { epoch: cursor.epoch, seq: cursor.endSeq },
// Catch up all missing canonical rows by default.
limit: 0,
projection: "canonical",
};
}
function resolveInitialTimelineLimit(): number {
return Platform.OS === "web"
? UNBOUNDED_TIMELINE_LIMIT
@@ -56,7 +24,7 @@ function resolveInitialTimelineLimit(): number {
}
export const __private__ = {
buildInitialTimelineRequest,
deriveInitialTimelineRequest,
resolveInitialTimelineLimit,
};
@@ -92,11 +60,14 @@ export function useAgentInitialization({
const session = useSessionStore.getState().sessions[serverId];
const cursor = session?.agentTimelineCursor.get(agentId);
const hasLocalTail = (session?.agentStreamTail.get(agentId)?.length ?? 0) > 0;
const initialTimelineLimit = resolveInitialTimelineLimit();
const timelineRequest = buildInitialTimelineRequest({
cursor,
hasLocalTail,
const hasAuthoritativeHistory =
(session?.agentHistorySyncGeneration.get(agentId) ?? -1) >= 0;
const timelineRequest = deriveInitialTimelineRequest({
cursor: cursor
? { epoch: cursor.epoch, seq: cursor.endSeq }
: null,
hasAuthoritativeHistory,
initialTimelineLimit,
});
const initRequestDirection =
+7 -32
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { TextInput } from "react-native";
import { router, usePathname, type Href } from "expo-router";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import {
clearCommandCenterFocusRestoreElement,
@@ -17,10 +18,6 @@ import {
import type { ShortcutKey } from "@/utils/format-shortcut";
import { focusWithRetries } from "@/utils/web-focus";
function agentKey(agent: Pick<AggregatedAgent, "serverId" | "id">): string {
return `${agent.serverId}:${agent.id}`;
}
function isMatch(agent: AggregatedAgent, query: string): boolean {
if (!query) return true;
const q = query.toLowerCase();
@@ -46,14 +43,6 @@ function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
}
function parseAgentKeyFromPathname(pathname: string): string | null {
const match = parseHostAgentRouteFromPathname(pathname);
if (!match) {
return null;
}
return `${match.serverId}:${match.agentId}`;
}
type CommandCenterActionDefinition = {
id: string;
title: string;
@@ -117,9 +106,6 @@ export function useCommandCenter() {
const { agents } = useAggregatedAgents();
const open = useKeyboardShortcutsStore((s) => s.commandCenterOpen);
const setOpen = useKeyboardShortcutsStore((s) => s.setCommandCenterOpen);
const requestMessageInputAction = useKeyboardShortcutsStore(
(s) => s.requestMessageInputAction
);
const inputRef = useRef<TextInput>(null);
const didNavigateRef = useRef(false);
const prevOpenRef = useRef(open);
@@ -134,11 +120,6 @@ export function useCommandCenter() {
const fallbackServerId = agents[0]?.serverId ?? null;
const agentKeyFromPathname = useMemo(
() => parseAgentKeyFromPathname(pathname),
[pathname]
);
const newAgentRoute = useMemo<Href>(() => {
const serverIdFromPath =
parseServerIdFromPathname(pathname) ?? fallbackServerId;
@@ -191,10 +172,6 @@ export function useCommandCenter() {
const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
requestMessageInputAction({
agentKey: agentKey(agent),
kind: "focus",
});
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
@@ -205,7 +182,7 @@ export function useCommandCenter() {
) as Href;
navigate(route);
},
[pathname, requestMessageInputAction, setOpen]
[pathname, setOpen]
);
const handleSelectAction = useCallback((action: CommandCenterActionItem) => {
@@ -245,12 +222,10 @@ export function useCommandCenter() {
focus: () => el?.focus(),
isFocused,
onTimeout: () => {
if (agentKeyFromPathname) {
requestMessageInputAction({
agentKey: agentKeyFromPathname,
kind: "focus",
});
}
keyboardActionDispatcher.dispatch({
id: "message-input.focus",
scope: "message-input",
});
},
});
return cancel;
@@ -265,7 +240,7 @@ export function useCommandCenter() {
inputRef.current?.focus();
}, 0);
return () => clearTimeout(id);
}, [agentKeyFromPathname, open, requestMessageInputAction]);
}, [open]);
useEffect(() => {
if (!open) return;
@@ -0,0 +1,37 @@
import { useEffect } from "react";
import {
keyboardActionDispatcher,
type KeyboardActionDefinition,
type KeyboardActionHandler,
type KeyboardActionId,
} from "@/keyboard/keyboard-action-dispatcher";
type UseKeyboardActionHandlerInput = {
handlerId: string;
actions: readonly KeyboardActionId[];
enabled: boolean;
priority: number;
isActive?: () => boolean;
handle: (action: KeyboardActionDefinition) => boolean;
};
export function useKeyboardActionHandler(input: UseKeyboardActionHandlerInput) {
useEffect(() => {
return keyboardActionDispatcher.registerHandler({
handlerId: input.handlerId,
actions: input.actions,
enabled: input.enabled,
priority: input.priority,
isActive: input.isActive,
handle: input.handle,
});
}, [
input.actions,
input.enabled,
input.handle,
input.handlerId,
input.isActive,
input.priority,
]);
}
@@ -16,10 +16,8 @@ import {
type MessageInputKeyboardActionKind,
type KeyboardShortcutPayload,
} from "@/keyboard/actions";
import {
canToggleFileExplorerShortcut,
resolveSelectedOrRouteAgentKey,
} from "@/keyboard/keyboard-shortcut-routing";
import { canToggleFileExplorerShortcut } from "@/keyboard/keyboard-shortcut-routing";
import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher";
import { resolveKeyboardShortcut } from "@/keyboard/keyboard-shortcuts";
import { resolveKeyboardFocusScope } from "@/keyboard/focus-scope";
import { getShortcutOs } from "@/utils/shortcut-platform";
@@ -121,18 +119,38 @@ export function useKeyboardShortcuts({
return true;
};
const requestMessageInputAction = (
const dispatchMessageInputAction = (
kind: MessageInputKeyboardActionKind
): boolean => {
const agentKey = resolveSelectedOrRouteAgentKey({ selectedAgentId, pathname });
if (!agentKey) {
return false;
switch (kind) {
case "focus":
return keyboardActionDispatcher.dispatch({
id: "message-input.focus",
scope: "message-input",
});
case "dictation-toggle":
return keyboardActionDispatcher.dispatch({
id: "message-input.dictation-toggle",
scope: "message-input",
});
case "dictation-cancel":
return keyboardActionDispatcher.dispatch({
id: "message-input.dictation-cancel",
scope: "message-input",
});
case "voice-toggle":
return keyboardActionDispatcher.dispatch({
id: "message-input.voice-toggle",
scope: "message-input",
});
case "voice-mute-toggle":
return keyboardActionDispatcher.dispatch({
id: "message-input.voice-mute-toggle",
scope: "message-input",
});
default:
return false;
}
useKeyboardShortcutsStore.getState().requestMessageInputAction({
agentKey,
kind,
});
return true;
};
const requestWorkspaceTabAction = (input:
| { kind: "new" | "close-current" }
@@ -232,7 +250,7 @@ export function useKeyboardShortcuts({
if (!input.payload || typeof input.payload !== "object" || !("kind" in input.payload)) {
return false;
}
return requestMessageInputAction(input.payload.kind);
return dispatchMessageInputAction(input.payload.kind);
default:
return false;
}