Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
import type React from 'react';
|
|
import { useCallback, useMemo, useRef } from 'react';
|
|
|
|
import { retainStableAiPanelContexts } from '../../domain/aiPanelContextsEqual';
|
|
import {
|
|
buildAITerminalSessionInfo,
|
|
type AIPanelContext,
|
|
} from '../../domain/buildAITerminalSessionInfo';
|
|
import { collectSessionIds } from '../../domain/workspace';
|
|
import type { TerminalContextReader } from '../../domain/terminalContextRead';
|
|
import { detectLocalOs } from '../../lib/localShell';
|
|
import type { Host, PortForwardingRule, TerminalSession, Workspace } from '../../types';
|
|
|
|
interface UseTerminalAiContextsOptions {
|
|
hosts: Host[];
|
|
hostsRef: React.MutableRefObject<Host[]>;
|
|
portForwardingRules: PortForwardingRule[];
|
|
portForwardingRulesRef: React.MutableRefObject<PortForwardingRule[]>;
|
|
mountedAiTabIds: string[];
|
|
sessionHostsMap: Map<string, Host>;
|
|
sessions: TerminalSession[];
|
|
sessionsRef: React.MutableRefObject<TerminalSession[]>;
|
|
terminalContextReadersRef: React.MutableRefObject<Map<string, TerminalContextReader>>;
|
|
workspaces: Workspace[];
|
|
workspacesRef: React.MutableRefObject<Workspace[]>;
|
|
}
|
|
|
|
export function useTerminalAiContexts({
|
|
hosts,
|
|
hostsRef,
|
|
portForwardingRules,
|
|
portForwardingRulesRef,
|
|
mountedAiTabIds,
|
|
sessionHostsMap,
|
|
sessions,
|
|
sessionsRef,
|
|
terminalContextReadersRef,
|
|
workspaces,
|
|
workspacesRef,
|
|
}: UseTerminalAiContextsOptions) {
|
|
const previousAiContextsRef = useRef<Map<string, AIPanelContext> | null>(null);
|
|
const aiContextsByTabId = useMemo(() => {
|
|
const localOs = detectLocalOs(navigator.userAgent || navigator.platform);
|
|
const sessionById = new Map<string, TerminalSession>(sessions.map((session) => [session.id, session]));
|
|
const workspaceById = new Map<string, Workspace>(workspaces.map((workspace) => [workspace.id, workspace]));
|
|
const allHosts = hosts;
|
|
const tabIds = new Set<string>(mountedAiTabIds);
|
|
|
|
const contexts = new Map<string, AIPanelContext>();
|
|
|
|
for (const tabId of tabIds) {
|
|
const workspace = workspaceById.get(tabId);
|
|
if (workspace) {
|
|
const sessionIds = collectSessionIds(workspace.root);
|
|
contexts.set(tabId, {
|
|
scopeType: 'workspace',
|
|
scopeTargetId: workspace.id,
|
|
scopeHostIds: sessionIds
|
|
.map((sessionId) => sessionById.get(sessionId)?.hostId)
|
|
.filter((hostId): hostId is string => !!hostId),
|
|
scopeLabel: workspace.title,
|
|
focusedSessionId: workspace.focusedSessionId,
|
|
terminalSessions: sessionIds.map((sessionId) =>
|
|
buildAITerminalSessionInfo(
|
|
sessionById.get(sessionId),
|
|
sessionHostsMap.get(sessionId),
|
|
localOs,
|
|
{ allHosts, portForwardingRules },
|
|
),
|
|
),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const session = sessionById.get(tabId);
|
|
if (!session) continue;
|
|
|
|
contexts.set(tabId, {
|
|
scopeType: 'terminal',
|
|
scopeTargetId: session.id,
|
|
scopeHostIds: session.hostId ? [session.hostId] : [],
|
|
scopeLabel: session.hostLabel ?? '',
|
|
terminalSessions: [
|
|
buildAITerminalSessionInfo(
|
|
session,
|
|
sessionHostsMap.get(session.id),
|
|
localOs,
|
|
{ allHosts, portForwardingRules },
|
|
),
|
|
],
|
|
});
|
|
}
|
|
|
|
const retained = retainStableAiPanelContexts(previousAiContextsRef.current, contexts);
|
|
previousAiContextsRef.current = retained;
|
|
return retained;
|
|
}, [sessions, workspaces, mountedAiTabIds, sessionHostsMap, hosts, portForwardingRules]);
|
|
|
|
const resolveAIExecutorContext = useCallback((scope: {
|
|
type: 'terminal' | 'workspace';
|
|
targetId?: string;
|
|
label?: string;
|
|
}) => {
|
|
const latestWorkspaces = workspacesRef.current;
|
|
const latestSessions = sessionsRef.current;
|
|
const latestHosts = hostsRef.current;
|
|
const localOs = detectLocalOs(navigator.userAgent || navigator.platform);
|
|
const sessionIds = scope.type === 'workspace'
|
|
? (() => {
|
|
const workspace = scope.targetId ? latestWorkspaces.find((w) => w.id === scope.targetId) : undefined;
|
|
return workspace?.root ? collectSessionIds(workspace.root) : [];
|
|
})()
|
|
: scope.targetId ? [scope.targetId] : [];
|
|
|
|
const workspaceName = scope.type === 'workspace'
|
|
? latestWorkspaces.find((w) => w.id === scope.targetId)?.title ?? scope.label
|
|
: undefined;
|
|
|
|
return {
|
|
sessions: sessionIds.map((sid) => {
|
|
const session = latestSessions.find((s) => s.id === sid);
|
|
const host = session ? sessionHostsMap.get(session.id) : undefined;
|
|
return buildAITerminalSessionInfo(session, host, localOs, {
|
|
allHosts: latestHosts,
|
|
portForwardingRules: portForwardingRulesRef.current,
|
|
});
|
|
}),
|
|
workspaceId: scope.type === 'workspace' ? scope.targetId : undefined,
|
|
workspaceName,
|
|
readTerminalContext: (request: Parameters<TerminalContextReader>[0]) => {
|
|
const reader = terminalContextReadersRef.current.get(request.sessionId);
|
|
if (!reader) {
|
|
return Promise.resolve({
|
|
ok: false as const,
|
|
error: `Terminal session "${request.sessionId}" has no readable terminal buffer.`,
|
|
});
|
|
}
|
|
return reader(request);
|
|
},
|
|
};
|
|
}, [hostsRef, portForwardingRulesRef, sessionHostsMap, sessionsRef, terminalContextReadersRef, workspacesRef]);
|
|
|
|
return {
|
|
aiContextsByTabId,
|
|
resolveAIExecutorContext,
|
|
};
|
|
}
|