Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
258 lines
7.1 KiB
TypeScript
258 lines
7.1 KiB
TypeScript
import type { AgentActivitySession } from "@tutti-os/agent-activity-core";
|
|
import type {
|
|
AgentGuiScheduledTask,
|
|
AgentGuiScheduler
|
|
} from "../agentGuiScheduler";
|
|
import {
|
|
appendConversationSearchPage,
|
|
EMPTY_CONVERSATION_SEARCH_QUERY_STATE,
|
|
type ConversationSearchQueryState
|
|
} from "./agentConversationRailQuerySnapshot";
|
|
import type { ConversationRailQueryRuntime } from "./agentGuiConversationRailQueryTypes";
|
|
|
|
export const CONVERSATION_SEARCH_DEBOUNCE_MS = 300;
|
|
|
|
type ListSessionsPage = NonNullable<
|
|
ConversationRailQueryRuntime["listSessionsPage"]
|
|
>;
|
|
|
|
export class AgentGUIConversationRailSearchController {
|
|
private agentTargetId = "";
|
|
private abortController: AbortController | null = null;
|
|
private debounceTask: AgentGuiScheduledTask | null = null;
|
|
private publicationState: "idle" | "pending" | "failed" = "idle";
|
|
private query = "";
|
|
private requestKey: string | null = null;
|
|
private requestSequence = 0;
|
|
private state = EMPTY_CONVERSATION_SEARCH_QUERY_STATE;
|
|
|
|
constructor(
|
|
private readonly input: {
|
|
isSectionPublicationBlocked(): boolean;
|
|
listSessionsPage?: ListSessionsPage;
|
|
onChanged(): void;
|
|
scheduler: AgentGuiScheduler;
|
|
upsertSessions(sessions: readonly AgentActivitySession[]): void;
|
|
workspaceId: string;
|
|
}
|
|
) {}
|
|
|
|
get queryState(): ConversationSearchQueryState {
|
|
return this.state;
|
|
}
|
|
|
|
get searchQuery(): string {
|
|
return this.query;
|
|
}
|
|
|
|
get searchRequestKey(): string | null {
|
|
return this.requestKey;
|
|
}
|
|
|
|
get enabled(): boolean {
|
|
return Boolean(this.input.listSessionsPage);
|
|
}
|
|
|
|
get publicationBlocked(): boolean {
|
|
return this.publicationState !== "idle";
|
|
}
|
|
|
|
configureAgentTarget(agentTargetId: string): void {
|
|
this.agentTargetId = agentTargetId;
|
|
if (this.query) this.request();
|
|
}
|
|
|
|
setQuery(value: string): void {
|
|
const query = value.trim();
|
|
if (query === this.query) return;
|
|
this.query = query;
|
|
this.schedule();
|
|
}
|
|
|
|
request(membershipRefresh = false): void {
|
|
this.clearDebounce();
|
|
this.requestKey =
|
|
this.enabled && this.query
|
|
? JSON.stringify([
|
|
this.input.workspaceId,
|
|
this.agentTargetId,
|
|
this.query
|
|
])
|
|
: null;
|
|
this.requestSequence += 1;
|
|
const requestSequence = this.requestSequence;
|
|
this.abortController?.abort();
|
|
this.abortController = null;
|
|
const listSessionsPage = this.input.listSessionsPage;
|
|
if (!listSessionsPage) {
|
|
this.publicationState = "idle";
|
|
this.state = EMPTY_CONVERSATION_SEARCH_QUERY_STATE;
|
|
return;
|
|
}
|
|
if (!this.requestKey) {
|
|
this.publicationState = "idle";
|
|
this.state = EMPTY_CONVERSATION_SEARCH_QUERY_STATE;
|
|
this.input.onChanged();
|
|
return;
|
|
}
|
|
const requestKey = this.requestKey;
|
|
const query = this.query;
|
|
const tracksPublication =
|
|
membershipRefresh ||
|
|
this.publicationBlocked ||
|
|
this.input.isSectionPublicationBlocked();
|
|
const abortController = new AbortController();
|
|
this.abortController = abortController;
|
|
if (tracksPublication) this.publicationState = "pending";
|
|
this.state = {
|
|
...EMPTY_CONVERSATION_SEARCH_QUERY_STATE,
|
|
pending: true,
|
|
requestKey
|
|
};
|
|
this.input.onChanged();
|
|
void listSessionsPage({
|
|
agentTargetId: this.agentTargetId || undefined,
|
|
limit: 100,
|
|
searchQuery: query,
|
|
signal: abortController.signal,
|
|
workspaceId: this.input.workspaceId
|
|
})
|
|
.then((page) => {
|
|
if (
|
|
abortController.signal.aborted ||
|
|
requestSequence !== this.requestSequence ||
|
|
requestKey !== this.requestKey
|
|
) {
|
|
return;
|
|
}
|
|
this.input.upsertSessions(page.sessions);
|
|
this.state = {
|
|
failed: false,
|
|
hasMore: page.hasMore,
|
|
loadingMore: false,
|
|
nextCursor: page.nextCursor ?? null,
|
|
pending: false,
|
|
requestKey,
|
|
resolvedQuery: query,
|
|
sessionIds: page.sessions.map((session) => session.agentSessionId)
|
|
};
|
|
if (tracksPublication) this.publicationState = "idle";
|
|
this.input.onChanged();
|
|
})
|
|
.catch(() => {
|
|
if (
|
|
abortController.signal.aborted ||
|
|
requestSequence !== this.requestSequence ||
|
|
requestKey !== this.requestKey
|
|
) {
|
|
return;
|
|
}
|
|
this.state = {
|
|
...EMPTY_CONVERSATION_SEARCH_QUERY_STATE,
|
|
failed: true,
|
|
requestKey,
|
|
resolvedQuery: query
|
|
};
|
|
if (tracksPublication) this.publicationState = "failed";
|
|
this.input.onChanged();
|
|
});
|
|
}
|
|
|
|
loadMore(): void {
|
|
const listSessionsPage = this.input.listSessionsPage;
|
|
if (
|
|
!this.enabled ||
|
|
!listSessionsPage ||
|
|
this.publicationBlocked ||
|
|
this.input.isSectionPublicationBlocked() ||
|
|
this.state.pending ||
|
|
this.state.loadingMore ||
|
|
!this.state.hasMore ||
|
|
!this.state.nextCursor ||
|
|
this.state.requestKey !== this.requestKey ||
|
|
this.state.resolvedQuery !== this.query
|
|
) {
|
|
return;
|
|
}
|
|
const requestSequence = this.requestSequence;
|
|
const abortController = new AbortController();
|
|
this.abortController?.abort();
|
|
this.abortController = abortController;
|
|
this.state = { ...this.state, loadingMore: true };
|
|
this.input.onChanged();
|
|
void listSessionsPage({
|
|
agentTargetId: this.agentTargetId || undefined,
|
|
cursor: this.state.nextCursor ?? undefined,
|
|
limit: 100,
|
|
searchQuery: this.query,
|
|
signal: abortController.signal,
|
|
workspaceId: this.input.workspaceId
|
|
})
|
|
.then((page) => {
|
|
if (
|
|
abortController.signal.aborted ||
|
|
requestSequence !== this.requestSequence
|
|
) {
|
|
return;
|
|
}
|
|
this.input.upsertSessions(page.sessions);
|
|
this.state = appendConversationSearchPage(this.state, page);
|
|
this.input.onChanged();
|
|
})
|
|
.catch(() => {
|
|
if (
|
|
abortController.signal.aborted ||
|
|
requestSequence !== this.requestSequence
|
|
) {
|
|
return;
|
|
}
|
|
this.state = { ...this.state, failed: true, loadingMore: false };
|
|
this.input.onChanged();
|
|
});
|
|
}
|
|
|
|
retry(): void {
|
|
if (!this.query || !this.enabled) return;
|
|
this.request();
|
|
}
|
|
|
|
resetPublication(): void {
|
|
this.publicationState = "idle";
|
|
}
|
|
|
|
detach(): void {
|
|
if (this.publicationState === "pending") {
|
|
this.publicationState = "failed";
|
|
}
|
|
this.clearDebounce();
|
|
this.requestSequence += 1;
|
|
this.abortController?.abort();
|
|
this.abortController = null;
|
|
}
|
|
|
|
private schedule(): void {
|
|
this.clearDebounce();
|
|
this.requestSequence += 1;
|
|
this.abortController?.abort();
|
|
this.abortController = null;
|
|
if (!this.query || !this.enabled) {
|
|
this.request();
|
|
return;
|
|
}
|
|
this.requestKey = null;
|
|
this.input.onChanged();
|
|
this.debounceTask = this.input.scheduler.schedule(
|
|
CONVERSATION_SEARCH_DEBOUNCE_MS,
|
|
() => {
|
|
this.debounceTask = null;
|
|
this.request();
|
|
}
|
|
);
|
|
}
|
|
|
|
private clearDebounce(): void {
|
|
this.debounceTask?.cancel();
|
|
this.debounceTask = null;
|
|
}
|
|
}
|