Merge branch 'main' into read-electron-migration-skills
# Conflicts: # packages/app/src/app/_layout.tsx # packages/app/src/components/left-sidebar.tsx
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-sugg
|
||||
import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { generateDraftId } from '@/stores/draft-keys'
|
||||
import { getHostRuntimeStore, useHostRuntimeSession } from '@/runtime/host-runtime'
|
||||
import { getHostRuntimeStore, useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
|
||||
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
|
||||
import { usePanelStore, type ExplorerCheckoutContext } from '@/stores/panel-store'
|
||||
import { MAX_CONTENT_WIDTH } from '@/constants/layout'
|
||||
@@ -317,9 +317,8 @@ function DraftAgentScreenContent({
|
||||
return collectAgentWorkingDirectorySuggestions([...liveSources, ...fetchedSources])
|
||||
}, [allAgents, sessionAgents])
|
||||
|
||||
const { client: runtimeClient, isConnected: isHostOnline } = useHostRuntimeSession(
|
||||
selectedServerId ?? ''
|
||||
)
|
||||
const runtimeClient = useHostRuntimeClient(selectedServerId ?? '')
|
||||
const isHostOnline = useHostRuntimeIsConnected(selectedServerId ?? '')
|
||||
const sessionClient = runtimeClient
|
||||
const trimmedWorkingDir = workingDir.trim()
|
||||
const shouldInspectRepo = trimmedWorkingDir.length > 0
|
||||
|
||||
@@ -24,7 +24,9 @@ import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
getHostRuntimeStore,
|
||||
isHostRuntimeConnected,
|
||||
useHostRuntimeSession,
|
||||
useHostRuntimeClient,
|
||||
useHostRuntimeIsConnected,
|
||||
useHostRuntimeSnapshot,
|
||||
} from "@/runtime/host-runtime";
|
||||
import { AddHostMethodModal } from "@/components/add-host-method-modal";
|
||||
import { AddHostModal } from "@/components/add-host-modal";
|
||||
@@ -993,9 +995,9 @@ function HostDetailModal({
|
||||
const connections = host?.connections ?? [];
|
||||
|
||||
// Restart logic (moved from DaemonCard)
|
||||
const { snapshot: runtimeSnapshot, client: runtimeClient, isConnected } = useHostRuntimeSession(
|
||||
host?.serverId ?? ""
|
||||
);
|
||||
const runtimeSnapshot = useHostRuntimeSnapshot(host?.serverId ?? "");
|
||||
const runtimeClient = useHostRuntimeClient(host?.serverId ?? "");
|
||||
const isConnected = useHostRuntimeIsConnected(host?.serverId ?? "");
|
||||
const runtime = getHostRuntimeStore();
|
||||
const daemonClient = runtimeClient;
|
||||
const daemonVersion = useSessionStore((state) => host ? (state.sessions[host.serverId]?.serverInfo?.version ?? null) : null);
|
||||
@@ -1390,7 +1392,7 @@ function DaemonCard({
|
||||
onOpenSettings,
|
||||
}: DaemonCardProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { snapshot } = useHostRuntimeSession(daemon.serverId);
|
||||
const snapshot = useHostRuntimeSnapshot(daemon.serverId);
|
||||
const connectionStatus = snapshot?.connectionStatus ?? "connecting";
|
||||
const activeConnection = snapshot?.activeConnection ?? null;
|
||||
const lastError = snapshot?.lastError ?? null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
canOpenAgentTabFromRoute,
|
||||
deriveWorkspaceAgentVisibility,
|
||||
shouldPruneWorkspaceAgentTab,
|
||||
workspaceAgentVisibilityEqual,
|
||||
} from "@/screens/workspace/workspace-agent-visibility";
|
||||
|
||||
function makeAgent(input: {
|
||||
@@ -53,7 +54,7 @@ function makeAgent(input: {
|
||||
}
|
||||
|
||||
describe("workspace agent visibility", () => {
|
||||
it("keeps archived agents hidden from visible list but present in workspace lookup", () => {
|
||||
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
|
||||
const workspaceId = "/repo/worktree";
|
||||
const visible = makeAgent({
|
||||
id: "visible-agent",
|
||||
@@ -82,87 +83,46 @@ describe("workspace agent visibility", () => {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["visible-agent"]);
|
||||
expect(result.lookupById.has("visible-agent")).toBe(true);
|
||||
expect(result.lookupById.has("archived-agent")).toBe(true);
|
||||
expect(result.lookupById.has("other-workspace-agent")).toBe(false);
|
||||
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
|
||||
expect(result.knownAgentIds.has("visible-agent")).toBe(true);
|
||||
expect(result.knownAgentIds.has("archived-agent")).toBe(true);
|
||||
expect(result.knownAgentIds.has("other-workspace-agent")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows explicit route open for archived agent once agents are hydrated", () => {
|
||||
const archivedAgent = makeAgent({
|
||||
id: "archived-agent",
|
||||
cwd: "/repo/worktree",
|
||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
|
||||
const knownAgentIds = new Set(["archived-agent"]);
|
||||
|
||||
expect(
|
||||
canOpenAgentTabFromRoute({
|
||||
agentId: "archived-agent",
|
||||
agentsHydrated: true,
|
||||
workspaceAgentLookup: lookup,
|
||||
knownAgentIds,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not prune archived agent tabs when the workspace lookup still contains the agent", () => {
|
||||
const archivedAgent = makeAgent({
|
||||
id: "archived-agent",
|
||||
cwd: "/repo/worktree",
|
||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
const lookup = new Map<string, Agent>([[archivedAgent.id, archivedAgent]]);
|
||||
it("does not prune archived agent tabs when knownAgentIds contains the agent", () => {
|
||||
const knownAgentIds = new Set(["archived-agent"]);
|
||||
|
||||
expect(
|
||||
shouldPruneWorkspaceAgentTab({
|
||||
agentId: "archived-agent",
|
||||
agentsHydrated: true,
|
||||
workspaceAgentLookup: lookup,
|
||||
knownAgentIds,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prunes agent tabs once agents are hydrated and the agent is missing from the workspace lookup", () => {
|
||||
it("prunes agent tabs once agents are hydrated and the agent is missing from knownAgentIds", () => {
|
||||
expect(
|
||||
shouldPruneWorkspaceAgentTab({
|
||||
agentId: "missing-agent",
|
||||
agentsHydrated: true,
|
||||
workspaceAgentLookup: new Map<string, Agent>(),
|
||||
knownAgentIds: new Set<string>(),
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("sorts same-createdAt agents by lastActivityAt descending", () => {
|
||||
const workspaceId = "/repo/worktree";
|
||||
const createdAt = new Date("2026-03-04T00:00:00.000Z");
|
||||
const newerActivity = makeAgent({
|
||||
id: "newer-activity",
|
||||
cwd: workspaceId,
|
||||
createdAt,
|
||||
lastActivityAt: new Date("2026-03-04T00:05:00.000Z"),
|
||||
});
|
||||
const olderActivity = makeAgent({
|
||||
id: "older-activity",
|
||||
cwd: workspaceId,
|
||||
createdAt,
|
||||
lastActivityAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
[olderActivity.id, olderActivity],
|
||||
[newerActivity.id, newerActivity],
|
||||
]);
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
expect(result.visibleAgents.map((agent) => agent.id)).toEqual([
|
||||
"newer-activity",
|
||||
"older-activity",
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches workspace agents when cwd and route workspace differ only by trailing slash", () => {
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
[
|
||||
@@ -179,7 +139,33 @@ describe("workspace agent visibility", () => {
|
||||
workspaceId: "/Users/moboudra/.paseo/worktrees/1luy0po7/normal-squid",
|
||||
});
|
||||
|
||||
expect(result.visibleAgents.map((agent) => agent.id)).toEqual(["slash-agent"]);
|
||||
expect(result.lookupById.has("slash-agent")).toBe(true);
|
||||
expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
|
||||
expect(result.knownAgentIds.has("slash-agent")).toBe(true);
|
||||
});
|
||||
|
||||
describe("workspaceAgentVisibilityEqual", () => {
|
||||
it("returns true for identical sets", () => {
|
||||
const a = { activeAgentIds: new Set(["a", "b"]), knownAgentIds: new Set(["a", "b", "c"]) };
|
||||
const b = { activeAgentIds: new Set(["a", "b"]), knownAgentIds: new Set(["a", "b", "c"]) };
|
||||
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when activeAgentIds differ", () => {
|
||||
const a = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a"]) };
|
||||
const b = { activeAgentIds: new Set(["b"]), knownAgentIds: new Set(["a"]) };
|
||||
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when knownAgentIds differ", () => {
|
||||
const a = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a"]) };
|
||||
const b = { activeAgentIds: new Set(["a"]), knownAgentIds: new Set(["a", "b"]) };
|
||||
expect(workspaceAgentVisibilityEqual(a, b)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for empty sets", () => {
|
||||
const a = { activeAgentIds: new Set<string>(), knownAgentIds: new Set<string>() };
|
||||
const b = { activeAgentIds: new Set<string>(), knownAgentIds: new Set<string>() };
|
||||
expect(workspaceAgentVisibilityEqual(a, b)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,68 +1,66 @@
|
||||
import type { Agent } from "@/stores/session-store";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
|
||||
function sortAgentsByCreatedAtDescending(agents: Agent[]): Agent[] {
|
||||
const sorted = [...agents];
|
||||
sorted.sort((left, right) => {
|
||||
const createdAtDelta = right.createdAt.getTime() - left.createdAt.getTime();
|
||||
if (createdAtDelta !== 0) {
|
||||
return createdAtDelta;
|
||||
}
|
||||
return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeWorkspaceId(value: string | null | undefined): string {
|
||||
return normalizeWorkspaceIdentity(value) ?? "";
|
||||
}
|
||||
|
||||
export interface WorkspaceAgentVisibility {
|
||||
activeAgentIds: Set<string>;
|
||||
knownAgentIds: Set<string>;
|
||||
}
|
||||
|
||||
export function deriveWorkspaceAgentVisibility(input: {
|
||||
sessionAgents: Map<string, Agent> | undefined;
|
||||
workspaceId: string;
|
||||
}): {
|
||||
visibleAgents: Agent[];
|
||||
lookupById: Map<string, Agent>;
|
||||
} {
|
||||
}): WorkspaceAgentVisibility {
|
||||
const { sessionAgents, workspaceId } = input;
|
||||
const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
|
||||
if (!sessionAgents || !workspaceId) {
|
||||
return {
|
||||
visibleAgents: [],
|
||||
lookupById: new Map<string, Agent>(),
|
||||
activeAgentIds: new Set<string>(),
|
||||
knownAgentIds: new Set<string>(),
|
||||
};
|
||||
}
|
||||
|
||||
const lookupById = new Map<string, Agent>();
|
||||
const visible: Agent[] = [];
|
||||
const activeAgentIds = new Set<string>();
|
||||
const knownAgentIds = new Set<string>();
|
||||
for (const agent of sessionAgents.values()) {
|
||||
if (normalizeWorkspaceId(agent.cwd) !== normalizedWorkspaceId) {
|
||||
continue;
|
||||
}
|
||||
lookupById.set(agent.id, agent);
|
||||
knownAgentIds.add(agent.id);
|
||||
if (!agent.archivedAt) {
|
||||
visible.push(agent);
|
||||
activeAgentIds.add(agent.id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visibleAgents: sortAgentsByCreatedAtDescending(visible),
|
||||
lookupById,
|
||||
};
|
||||
return { activeAgentIds, knownAgentIds };
|
||||
}
|
||||
|
||||
export function workspaceAgentVisibilityEqual(
|
||||
a: WorkspaceAgentVisibility,
|
||||
b: WorkspaceAgentVisibility,
|
||||
): boolean {
|
||||
return setsEqual(a.activeAgentIds, b.activeAgentIds) && setsEqual(a.knownAgentIds, b.knownAgentIds);
|
||||
}
|
||||
|
||||
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const item of a) {
|
||||
if (!b.has(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function canOpenAgentTabFromRoute(input: {
|
||||
agentId: string;
|
||||
agentsHydrated: boolean;
|
||||
workspaceAgentLookup: Map<string, Agent>;
|
||||
knownAgentIds: Set<string>;
|
||||
}): boolean {
|
||||
if (!input.agentId.trim()) {
|
||||
return false;
|
||||
@@ -70,13 +68,13 @@ export function canOpenAgentTabFromRoute(input: {
|
||||
if (!input.agentsHydrated) {
|
||||
return true;
|
||||
}
|
||||
return input.workspaceAgentLookup.has(input.agentId);
|
||||
return input.knownAgentIds.has(input.agentId);
|
||||
}
|
||||
|
||||
export function shouldPruneWorkspaceAgentTab(input: {
|
||||
agentId: string;
|
||||
agentsHydrated: boolean;
|
||||
workspaceAgentLookup: Map<string, Agent>;
|
||||
knownAgentIds: Set<string>;
|
||||
}): boolean {
|
||||
if (!input.agentId.trim()) {
|
||||
return false;
|
||||
@@ -84,5 +82,5 @@ export function shouldPruneWorkspaceAgentTab(input: {
|
||||
if (!input.agentsHydrated) {
|
||||
return false;
|
||||
}
|
||||
return !input.workspaceAgentLookup.has(input.agentId);
|
||||
return !input.knownAgentIds.has(input.agentId);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AgentStreamView } from '@/components/agent-stream-view'
|
||||
import type { ImageAttachment } from '@/components/message-input'
|
||||
import { useAgentFormState } from '@/hooks/use-agent-form-state'
|
||||
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
|
||||
import { useHostRuntimeSession } from '@/runtime/host-runtime'
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from '@/runtime/host-runtime'
|
||||
import type { Agent } from '@/stores/session-store'
|
||||
import { encodeImages } from '@/utils/encode-images'
|
||||
import type { AgentCapabilityFlags, AgentSessionConfig } from '@server/server/agent/agent-sdk-types'
|
||||
@@ -40,7 +40,8 @@ export function WorkspaceDraftAgentTab({
|
||||
onCreated,
|
||||
onOpenWorkspaceFile,
|
||||
}: WorkspaceDraftAgentTabProps) {
|
||||
const { client, isConnected } = useHostRuntimeSession(serverId)
|
||||
const client = useHostRuntimeClient(serverId)
|
||||
const isConnected = useHostRuntimeIsConnected(serverId)
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null)
|
||||
|
||||
const {
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface BuildWorkspacePaneContentModelInput {
|
||||
tab: WorkspaceTabDescriptor;
|
||||
normalizedServerId: string;
|
||||
normalizedWorkspaceId: string;
|
||||
isPaneFocused: boolean;
|
||||
onOpenTab: (target: WorkspaceTabDescriptor["target"]) => void;
|
||||
onCloseCurrentTab: () => void;
|
||||
onRetargetCurrentTab: (target: WorkspaceTabDescriptor["target"]) => void;
|
||||
@@ -25,6 +26,7 @@ export function buildWorkspacePaneContentModel({
|
||||
tab,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
isPaneFocused,
|
||||
onOpenTab,
|
||||
onCloseCurrentTab,
|
||||
onRetargetCurrentTab,
|
||||
@@ -40,6 +42,7 @@ export function buildWorkspacePaneContentModel({
|
||||
serverId: normalizedServerId,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
tabId: tab.tabId,
|
||||
isPaneFocused,
|
||||
target: tab.target,
|
||||
openTab: onOpenTab,
|
||||
closeCurrentTab: onCloseCurrentTab,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -70,7 +71,7 @@ import {
|
||||
normalizeWorkspaceTabTarget,
|
||||
workspaceTabTargetsEqual,
|
||||
} from "@/utils/workspace-tab-identity";
|
||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
|
||||
import {
|
||||
checkoutStatusQueryKey,
|
||||
@@ -96,6 +97,7 @@ import {
|
||||
import {
|
||||
deriveWorkspaceAgentVisibility,
|
||||
shouldPruneWorkspaceAgentTab,
|
||||
workspaceAgentVisibilityEqual,
|
||||
} from "@/screens/workspace/workspace-agent-visibility";
|
||||
import {
|
||||
deriveWorkspacePaneState,
|
||||
@@ -279,6 +281,25 @@ function ResolvedMobileActiveTabTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceDocumentTitleEffect({
|
||||
label,
|
||||
titleState,
|
||||
}: {
|
||||
label: string;
|
||||
titleState: "ready" | "loading";
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web" || typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
const resolvedLabel = label.trim();
|
||||
document.title =
|
||||
titleState === "loading" ? "Loading..." : resolvedLabel || "Workspace";
|
||||
}, [label, titleState]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function MobileWorkspaceTabOption({
|
||||
tab,
|
||||
tabIndex,
|
||||
@@ -519,6 +540,12 @@ export function WorkspaceScreen({
|
||||
workspaceId,
|
||||
openIntent,
|
||||
}: WorkspaceScreenProps) {
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
if (!isFocused) {
|
||||
return <View style={{ flex: 1 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExplorerSidebarAnimationProvider>
|
||||
<WorkspaceScreenContent
|
||||
@@ -539,7 +566,6 @@ function WorkspaceScreenContent({
|
||||
const isDarkMode = useColorScheme() === "dark";
|
||||
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
||||
const toast = useToast();
|
||||
const isScreenFocused = useIsFocused();
|
||||
const isMobile =
|
||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
@@ -555,20 +581,18 @@ function WorkspaceScreenContent({
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { client, isConnected } = useHostRuntimeSession(normalizedServerId);
|
||||
const client = useHostRuntimeClient(normalizedServerId);
|
||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||
|
||||
const sessionAgents = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.agents
|
||||
);
|
||||
const workspaceAgentVisibility = useMemo(
|
||||
() =>
|
||||
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
||||
useSessionStore,
|
||||
(state) =>
|
||||
deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
sessionAgents: state.sessions[normalizedServerId]?.agents,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
}),
|
||||
[normalizedWorkspaceId, sessionAgents]
|
||||
workspaceAgentVisibilityEqual,
|
||||
);
|
||||
const workspaceAgents = workspaceAgentVisibility.visibleAgents;
|
||||
|
||||
const terminalsQueryKey = useMemo(
|
||||
() => ["terminals", normalizedServerId, normalizedWorkspaceId] as const,
|
||||
@@ -798,8 +822,6 @@ function WorkspaceScreenContent({
|
||||
return () => handler.remove();
|
||||
}, [closeToAgent, isExplorerOpen]);
|
||||
|
||||
const agentsById = workspaceAgentVisibility.lookupById;
|
||||
|
||||
const persistenceKey = useMemo(
|
||||
() =>
|
||||
buildWorkspaceTabPersistenceKey({
|
||||
@@ -828,7 +850,6 @@ function WorkspaceScreenContent({
|
||||
const reorderWorkspaceTabsInPane = useWorkspaceLayoutStore((state) => state.reorderTabsInPane);
|
||||
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
||||
const consumedOpenIntentsRef = useRef(new Set<string>());
|
||||
const reopenedArchivedOpenIntentsRef = useRef(new Set<string>());
|
||||
const pendingCloseTabIdsRef = useRef(new Set<string>());
|
||||
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
|
||||
const currentOpenIntentKey = useMemo(
|
||||
@@ -849,6 +870,24 @@ function WorkspaceScreenContent({
|
||||
}),
|
||||
[uiTabs, workspaceLayout]
|
||||
);
|
||||
const setFocusedAgentId = useSessionStore((state) => state.setFocusedAgentId);
|
||||
const focusedPaneAgentId = useMemo(() => {
|
||||
const target = focusedPaneTabState.activeTab?.descriptor.target;
|
||||
if (target?.kind !== "agent") {
|
||||
return null;
|
||||
}
|
||||
return target.agentId;
|
||||
}, [focusedPaneTabState.activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedAgentId(normalizedServerId, focusedPaneAgentId);
|
||||
}, [focusedPaneAgentId, normalizedServerId, setFocusedAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setFocusedAgentId(normalizedServerId, null);
|
||||
};
|
||||
}, [normalizedServerId, setFocusedAgentId]);
|
||||
|
||||
const ensureWorkspaceTab = useCallback(
|
||||
function ensureWorkspaceTab(target: WorkspaceTabTarget): string | null {
|
||||
@@ -914,43 +953,6 @@ function WorkspaceScreenContent({
|
||||
}
|
||||
}, [currentOpenIntentKey, resolvedOpenIntentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!client || !isConnected || openIntent?.kind !== "agent" || !currentOpenIntentKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agentId = openIntent.agentId.trim();
|
||||
if (!agentId) {
|
||||
return;
|
||||
}
|
||||
if (reopenedArchivedOpenIntentsRef.current.has(currentOpenIntentKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = agentsById.get(agentId) ?? null;
|
||||
if (!agent?.archivedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
reopenedArchivedOpenIntentsRef.current.add(currentOpenIntentKey);
|
||||
void client.refreshAgent(agentId).catch((error) => {
|
||||
console.warn("[WorkspaceScreen] Failed to reopen archived agent", {
|
||||
serverId: normalizedServerId,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
agentId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}, [
|
||||
agentsById,
|
||||
client,
|
||||
currentOpenIntentKey,
|
||||
isConnected,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
openIntent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openIntent || !persistenceKey) {
|
||||
return;
|
||||
@@ -1032,12 +1034,12 @@ function WorkspaceScreenContent({
|
||||
return pending?.serverId === normalizedServerId && pending.lifecycle === "active";
|
||||
});
|
||||
|
||||
for (const agent of workspaceAgents) {
|
||||
for (const agentId of workspaceAgentVisibility.activeAgentIds) {
|
||||
const representedByTarget = uiTabs.some(
|
||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === agent.id
|
||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === agentId
|
||||
);
|
||||
const representedByDeterministicTabId = uiTabs.some(
|
||||
(tab) => tab.tabId === `agent_${agent.id}`
|
||||
(tab) => tab.tabId === `agent_${agentId}`
|
||||
);
|
||||
if (
|
||||
hasActivePendingDraftCreateInWorkspace &&
|
||||
@@ -1046,7 +1048,7 @@ function WorkspaceScreenContent({
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
ensureWorkspaceTab({ kind: "agent", agentId: agent.id });
|
||||
ensureWorkspaceTab({ kind: "agent", agentId });
|
||||
}
|
||||
for (const terminal of terminals) {
|
||||
ensureWorkspaceTab({ kind: "terminal", terminalId: terminal.id });
|
||||
@@ -1061,7 +1063,7 @@ function WorkspaceScreenContent({
|
||||
shouldPruneWorkspaceAgentTab({
|
||||
agentId: tab.target.agentId,
|
||||
agentsHydrated: hasHydratedAgents,
|
||||
workspaceAgentLookup: agentsById,
|
||||
knownAgentIds: workspaceAgentVisibility.knownAgentIds,
|
||||
})
|
||||
) {
|
||||
closeWorkspaceTab(persistenceKey, tab.tabId);
|
||||
@@ -1083,7 +1085,7 @@ function WorkspaceScreenContent({
|
||||
terminals,
|
||||
terminalsQuery.isSuccess,
|
||||
uiTabs,
|
||||
workspaceAgents,
|
||||
workspaceAgentVisibility,
|
||||
]);
|
||||
|
||||
const activeTabId = resolvedPaneTabState.activeTabId;
|
||||
@@ -1137,7 +1139,7 @@ function WorkspaceScreenContent({
|
||||
emptyWorkspaceSeedRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (workspaceAgents.length > 0 || terminals.length > 0) {
|
||||
if (workspaceAgentVisibility.activeAgentIds.size > 0 || terminals.length > 0) {
|
||||
emptyWorkspaceSeedRef.current = null;
|
||||
return;
|
||||
}
|
||||
@@ -1159,7 +1161,7 @@ function WorkspaceScreenContent({
|
||||
persistenceKey,
|
||||
terminals.length,
|
||||
tabs.length,
|
||||
workspaceAgents.length,
|
||||
workspaceAgentVisibility.activeAgentIds.size,
|
||||
]);
|
||||
|
||||
const handleOpenFileFromExplorer = useCallback(
|
||||
@@ -1442,7 +1444,7 @@ function WorkspaceScreenContent({
|
||||
const handleCopyResumeCommand = useCallback(
|
||||
async (agentId: string) => {
|
||||
if (!agentId) return;
|
||||
const agent = sessionAgents?.get(agentId) ?? null;
|
||||
const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null;
|
||||
const providerSessionId =
|
||||
agent?.runtimeInfo?.sessionId ?? agent?.persistence?.sessionId ?? null;
|
||||
if (!agent || !providerSessionId) {
|
||||
@@ -1467,7 +1469,7 @@ function WorkspaceScreenContent({
|
||||
toast.error("Copy failed");
|
||||
}
|
||||
},
|
||||
[sessionAgents, toast]
|
||||
[normalizedServerId, toast]
|
||||
);
|
||||
|
||||
const handleCopyWorkspacePath = useCallback(async () => {
|
||||
@@ -1773,7 +1775,7 @@ function WorkspaceScreenContent({
|
||||
] as const,
|
||||
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
||||
priority: 100,
|
||||
isActive: () => isScreenFocused,
|
||||
isActive: () => true,
|
||||
handle: handleWorkspaceTabAction,
|
||||
});
|
||||
|
||||
@@ -1794,21 +1796,29 @@ function WorkspaceScreenContent({
|
||||
] as const,
|
||||
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
||||
priority: 100,
|
||||
isActive: () => isScreenFocused,
|
||||
isActive: () => true,
|
||||
handle: handleWorkspacePaneAction,
|
||||
});
|
||||
|
||||
const activeTabDescriptor = activeTab?.descriptor ?? null;
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web" || typeof document === "undefined" || activeTabDescriptor) {
|
||||
return;
|
||||
}
|
||||
document.title = "Workspace";
|
||||
}, [activeTabDescriptor]);
|
||||
const buildPaneContentModel = useCallback(
|
||||
(input: {
|
||||
tab: WorkspaceTabDescriptor;
|
||||
paneId?: string | null;
|
||||
isPaneFocused: boolean;
|
||||
focusPaneBeforeOpen?: boolean;
|
||||
}) =>
|
||||
buildWorkspacePaneContentModel({
|
||||
tab: input.tab,
|
||||
normalizedServerId,
|
||||
normalizedWorkspaceId,
|
||||
isPaneFocused: input.isPaneFocused,
|
||||
onOpenTab: (target) => {
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
@@ -1855,6 +1865,7 @@ function WorkspaceScreenContent({
|
||||
? buildPaneContentModel({
|
||||
tab: activeTabDescriptor,
|
||||
paneId: focusedPaneTabState.pane?.id ?? null,
|
||||
isPaneFocused: true,
|
||||
focusPaneBeforeOpen: false,
|
||||
})
|
||||
: null,
|
||||
@@ -1885,6 +1896,20 @@ function WorkspaceScreenContent({
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: mainBackgroundColor }]}>
|
||||
{Platform.OS === "web" && activeTabDescriptor ? (
|
||||
<WorkspaceTabPresentationResolver
|
||||
tab={activeTabDescriptor}
|
||||
serverId={normalizedServerId}
|
||||
workspaceId={normalizedWorkspaceId}
|
||||
>
|
||||
{(presentation) => (
|
||||
<WorkspaceDocumentTitleEffect
|
||||
label={presentation.label}
|
||||
titleState={presentation.titleState}
|
||||
/>
|
||||
)}
|
||||
</WorkspaceTabPresentationResolver>
|
||||
) : null}
|
||||
<View style={styles.threePaneRow}>
|
||||
<View style={styles.centerColumn}>
|
||||
<ScreenHeader
|
||||
@@ -2115,13 +2140,14 @@ function WorkspaceScreenContent({
|
||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||
onSelectNewTabOption={handleSelectNewTabOption}
|
||||
newTabAgentOptionId={NEW_TAB_AGENT_OPTION_ID}
|
||||
buildPaneContentModel={({ paneId, tab }) =>
|
||||
buildPaneContentModel({
|
||||
tab,
|
||||
paneId,
|
||||
focusPaneBeforeOpen: true,
|
||||
})
|
||||
}
|
||||
buildPaneContentModel={({ paneId, tab, isPaneFocused }) =>
|
||||
buildPaneContentModel({
|
||||
tab,
|
||||
paneId,
|
||||
isPaneFocused,
|
||||
focusPaneBeforeOpen: true,
|
||||
})
|
||||
}
|
||||
onFocusPane={(paneId) => {
|
||||
focusWorkspacePane(persistenceKey, paneId);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user