Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -51,9 +51,7 @@ describe("checkout-git-actions-store", () => {
|
||||
const second = store.commit({ serverId, cwd });
|
||||
|
||||
expect(client.checkoutCommit).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
store.getStatus({ serverId, cwd, actionId: "commit" })
|
||||
).toBe("pending");
|
||||
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("pending");
|
||||
|
||||
deferred.resolve({});
|
||||
await Promise.all([first, second]);
|
||||
@@ -64,4 +62,3 @@ describe("checkout-git-actions-store", () => {
|
||||
expect(store.getStatus({ serverId, cwd, actionId: "commit" })).toBe("idle");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function resolveClient(serverId: string) {
|
||||
function setStatus(
|
||||
key: CheckoutKey,
|
||||
actionId: CheckoutGitAsyncActionId,
|
||||
status: CheckoutGitActionStatus
|
||||
status: CheckoutGitActionStatus,
|
||||
) {
|
||||
useCheckoutGitActionsStore.setState((state) => {
|
||||
const current = state.statusByCheckout[key]?.[actionId] ?? "idle";
|
||||
@@ -61,10 +61,7 @@ function invalidateCheckoutGitQueries(serverId: string, cwd: string) {
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) &&
|
||||
key[0] === "checkoutDiff" &&
|
||||
key[1] === serverId &&
|
||||
key[2] === cwd
|
||||
Array.isArray(key) && key[0] === "checkoutDiff" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -72,10 +69,7 @@ function invalidateCheckoutGitQueries(serverId: string, cwd: string) {
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
return (
|
||||
Array.isArray(key) &&
|
||||
key[0] === "checkoutPrStatus" &&
|
||||
key[1] === serverId &&
|
||||
key[2] === cwd
|
||||
Array.isArray(key) && key[0] === "checkoutPrStatus" && key[1] === serverId && key[2] === cwd
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -92,10 +86,7 @@ function invalidateWorktreeList() {
|
||||
});
|
||||
}
|
||||
|
||||
function removeWorktreeFromCachedLists(input: {
|
||||
serverId: string;
|
||||
worktreePath: string;
|
||||
}): void {
|
||||
function removeWorktreeFromCachedLists(input: { serverId: string; worktreePath: string }): void {
|
||||
const serverId = input.serverId.trim();
|
||||
const worktreePath = input.worktreePath.trim();
|
||||
if (!serverId || !worktreePath) {
|
||||
@@ -117,7 +108,7 @@ function removeWorktreeFromCachedLists(input: {
|
||||
query.queryKey[0] === "paseoWorktreeList" &&
|
||||
query.queryKey[1] === serverId,
|
||||
},
|
||||
removeFromList
|
||||
removeFromList,
|
||||
);
|
||||
|
||||
queryClient.setQueriesData(
|
||||
@@ -127,7 +118,7 @@ function removeWorktreeFromCachedLists(input: {
|
||||
query.queryKey[0] === "sidebarPaseoWorktreeList" &&
|
||||
query.queryKey[1] === serverId,
|
||||
},
|
||||
removeFromList
|
||||
removeFromList,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,9 @@ type PendingCreateAttempt = {
|
||||
|
||||
type CreateFlowState = {
|
||||
pendingByDraftId: Record<string, PendingCreateAttempt>;
|
||||
setPending: (
|
||||
pending: Omit<PendingCreateAttempt, "lifecycle">
|
||||
) => void;
|
||||
setPending: (pending: Omit<PendingCreateAttempt, "lifecycle">) => void;
|
||||
updateAgentId: (input: { draftId: string; agentId: string }) => void;
|
||||
markLifecycle: (input: {
|
||||
draftId: string;
|
||||
lifecycle: CreateFlowLifecycleState;
|
||||
}) => void;
|
||||
markLifecycle: (input: { draftId: string; lifecycle: CreateFlowLifecycleState }) => void;
|
||||
rekeyDraft: (input: { fromDraftId: string; toDraftId: string }) => void;
|
||||
clear: (input: { draftId: string }) => void;
|
||||
clearAll: () => void;
|
||||
|
||||
@@ -97,7 +97,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
const downloadUrl = buildDownloadUrl(
|
||||
downloadTarget.baseUrl,
|
||||
tokenResponse.token,
|
||||
Platform.OS === "web" ? downloadTarget.authCredentials : null
|
||||
Platform.OS === "web" ? downloadTarget.authCredentials : null,
|
||||
);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
@@ -135,7 +135,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
speed,
|
||||
eta,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const result = await downloadResumable.downloadAsync();
|
||||
@@ -203,9 +203,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
const updated = new Map(state.downloads);
|
||||
updated.delete(id);
|
||||
const newActiveId =
|
||||
state.activeDownloadId === id
|
||||
? findMostRecentDownloadId(updated)
|
||||
: state.activeDownloadId;
|
||||
state.activeDownloadId === id ? findMostRecentDownloadId(updated) : state.activeDownloadId;
|
||||
return { downloads: updated, activeDownloadId: newActiveId };
|
||||
});
|
||||
},
|
||||
@@ -245,8 +243,7 @@ type DownloadTarget = {
|
||||
};
|
||||
|
||||
function resolveDaemonDownloadTarget(daemon?: HostProfile): DownloadTarget {
|
||||
const endpoint =
|
||||
daemon?.connections.find((conn) => conn.type === "directTcp")?.endpoint ?? null;
|
||||
const endpoint = daemon?.connections.find((conn) => conn.type === "directTcp")?.endpoint ?? null;
|
||||
if (!endpoint) {
|
||||
return { baseUrl: null, authHeader: null, authCredentials: null };
|
||||
}
|
||||
@@ -287,7 +284,7 @@ function resolveDaemonDownloadTarget(daemon?: HostProfile): DownloadTarget {
|
||||
function buildDownloadUrl(
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
authCredentials: { username: string; password: string } | null
|
||||
authCredentials: { username: string; password: string } | null,
|
||||
): string {
|
||||
const url = new URL("/api/files/download", baseUrl);
|
||||
url.searchParams.set("token", token);
|
||||
|
||||
@@ -46,10 +46,7 @@ interface DraftStoreActions {
|
||||
getDraftInput: (draftKey: string) => DraftInput | undefined;
|
||||
hydrateDraftInput: (draftKey: string) => Promise<DraftInput | undefined>;
|
||||
saveDraftInput: (input: { draftKey: string; draft: DraftInput }) => void;
|
||||
markDraftLifecycle: (input: {
|
||||
draftKey: string;
|
||||
lifecycle: DraftLifecycleState;
|
||||
}) => void;
|
||||
markDraftLifecycle: (input: { draftKey: string; lifecycle: DraftLifecycleState }) => void;
|
||||
clearDraftInput: (input: {
|
||||
draftKey: string;
|
||||
lifecycle?: Exclude<DraftLifecycleState, "active">;
|
||||
@@ -57,10 +54,7 @@ interface DraftStoreActions {
|
||||
getCreateModalDraft: () => DraftInput | null;
|
||||
saveCreateModalDraft: (draft: DraftInput | null) => void;
|
||||
beginDraftGeneration: (draftKey: string) => number;
|
||||
isDraftGenerationCurrent: (input: {
|
||||
draftKey: string;
|
||||
generation: number;
|
||||
}) => boolean;
|
||||
isDraftGenerationCurrent: (input: { draftKey: string; generation: number }) => boolean;
|
||||
collectActiveAttachmentIds: () => string[];
|
||||
}
|
||||
|
||||
@@ -158,9 +152,7 @@ function toDraftInputIfReady(record: DraftRecord | null | undefined): DraftInput
|
||||
}
|
||||
return {
|
||||
text: record.input.text,
|
||||
images: (record.input.images as AttachmentMetadata[]).map(
|
||||
normalizeAttachmentMetadata
|
||||
),
|
||||
images: (record.input.images as AttachmentMetadata[]).map(normalizeAttachmentMetadata),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,10 +189,7 @@ function pruneFinalizedDraftRecords(input: {
|
||||
let changed = false;
|
||||
const next: Record<string, DraftRecord> = {};
|
||||
for (const [draftKey, record] of Object.entries(input.drafts)) {
|
||||
if (
|
||||
record.lifecycle !== "active" &&
|
||||
input.nowMs - record.updatedAt >= FINALIZED_DRAFT_TTL_MS
|
||||
) {
|
||||
if (record.lifecycle !== "active" && input.nowMs - record.updatedAt >= FINALIZED_DRAFT_TTL_MS) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
@@ -331,7 +320,9 @@ async function migrateAllLegacyDrafts(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateLegacyImages(images: readonly PersistedDraftImage[]): Promise<AttachmentMetadata[]> {
|
||||
async function migrateLegacyImages(
|
||||
images: readonly PersistedDraftImage[],
|
||||
): Promise<AttachmentMetadata[]> {
|
||||
if (images.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -364,7 +355,7 @@ async function migrateLegacyImages(images: readonly PersistedDraftImage[]): Prom
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return migrated.filter((entry): entry is AttachmentMetadata => entry !== null);
|
||||
@@ -389,9 +380,9 @@ function migratePersistedState(state: unknown): DraftStoreState {
|
||||
input: {
|
||||
text: typeof recordInput.text === "string" ? recordInput.text : "",
|
||||
images: Array.isArray(recordInput.images)
|
||||
? (recordInput.images
|
||||
? recordInput.images
|
||||
.map((entry) => normalizePersistedImage(entry))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null)
|
||||
: [],
|
||||
},
|
||||
lifecycle:
|
||||
@@ -410,9 +401,9 @@ function migratePersistedState(state: unknown): DraftStoreState {
|
||||
input: {
|
||||
text: typeof legacy.text === "string" ? legacy.text : "",
|
||||
images: Array.isArray(legacy.images)
|
||||
? (legacy.images
|
||||
? legacy.images
|
||||
.map((entry) => normalizePersistedImage(entry))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null)
|
||||
: [],
|
||||
},
|
||||
lifecycle: "active",
|
||||
@@ -430,9 +421,9 @@ function migratePersistedState(state: unknown): DraftStoreState {
|
||||
input: {
|
||||
text: typeof recordInput.text === "string" ? recordInput.text : "",
|
||||
images: Array.isArray(recordInput.images)
|
||||
? (recordInput.images
|
||||
? recordInput.images
|
||||
.map((entry) => normalizePersistedImage(entry))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null)
|
||||
: [],
|
||||
},
|
||||
lifecycle:
|
||||
@@ -448,9 +439,9 @@ function migratePersistedState(state: unknown): DraftStoreState {
|
||||
input: {
|
||||
text: typeof legacy.text === "string" ? legacy.text : "",
|
||||
images: Array.isArray(legacy.images)
|
||||
? (legacy.images
|
||||
? legacy.images
|
||||
.map((entry) => normalizePersistedImage(entry))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null))
|
||||
.filter((entry): entry is PersistedDraftImage => entry !== null)
|
||||
: [],
|
||||
},
|
||||
lifecycle: "active",
|
||||
@@ -631,8 +622,8 @@ export const useDraftStore = create<DraftStore>()(
|
||||
scheduleAttachmentGc();
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const __draftStoreTestUtils = {
|
||||
|
||||
@@ -22,24 +22,22 @@ interface KeyboardShortcutsState {
|
||||
resetModifiers: () => void;
|
||||
}
|
||||
|
||||
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
|
||||
(set) => ({
|
||||
commandCenterOpen: false,
|
||||
projectPickerOpen: false,
|
||||
shortcutsDialogOpen: false,
|
||||
altDown: false,
|
||||
cmdOrCtrlDown: false,
|
||||
sidebarShortcutWorkspaceTargets: [],
|
||||
visibleWorkspaceTargets: [],
|
||||
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>((set) => ({
|
||||
commandCenterOpen: false,
|
||||
projectPickerOpen: false,
|
||||
shortcutsDialogOpen: false,
|
||||
altDown: false,
|
||||
cmdOrCtrlDown: false,
|
||||
sidebarShortcutWorkspaceTargets: [],
|
||||
visibleWorkspaceTargets: [],
|
||||
|
||||
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }),
|
||||
setProjectPickerOpen: (open) => set({ projectPickerOpen: open }),
|
||||
setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }),
|
||||
setAltDown: (down) => set({ altDown: down }),
|
||||
setCmdOrCtrlDown: (down) => set({ cmdOrCtrlDown: down }),
|
||||
setSidebarShortcutWorkspaceTargets: (targets) =>
|
||||
set({ sidebarShortcutWorkspaceTargets: targets }),
|
||||
setVisibleWorkspaceTargets: (targets) => set({ visibleWorkspaceTargets: targets }),
|
||||
resetModifiers: () => set({ altDown: false, cmdOrCtrlDown: false }),
|
||||
})
|
||||
);
|
||||
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }),
|
||||
setProjectPickerOpen: (open) => set({ projectPickerOpen: open }),
|
||||
setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }),
|
||||
setAltDown: (down) => set({ altDown: down }),
|
||||
setCmdOrCtrlDown: (down) => set({ cmdOrCtrlDown: down }),
|
||||
setSidebarShortcutWorkspaceTargets: (targets) =>
|
||||
set({ sidebarShortcutWorkspaceTargets: targets }),
|
||||
setVisibleWorkspaceTargets: (targets) => set({ visibleWorkspaceTargets: targets }),
|
||||
resetModifiers: () => set({ altDown: false, cmdOrCtrlDown: false }),
|
||||
}));
|
||||
|
||||
@@ -36,10 +36,7 @@ function getSnapshot(): ActiveWorkspaceSelection | null {
|
||||
}
|
||||
|
||||
function emitIfChanged(next: ActiveWorkspaceSelection | null) {
|
||||
if (
|
||||
snapshot?.serverId === next?.serverId &&
|
||||
snapshot?.workspaceId === next?.workspaceId
|
||||
) {
|
||||
if (snapshot?.serverId === next?.serverId && snapshot?.workspaceId === next?.workspaceId) {
|
||||
return;
|
||||
}
|
||||
snapshot = next;
|
||||
@@ -48,7 +45,9 @@ function emitIfChanged(next: ActiveWorkspaceSelection | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function extractActiveWorkspaceFromRoute(route: NavigationRouteLike | undefined): ActiveWorkspaceSelection | null {
|
||||
function extractActiveWorkspaceFromRoute(
|
||||
route: NavigationRouteLike | undefined,
|
||||
): ActiveWorkspaceSelection | null {
|
||||
if (!route) {
|
||||
return null;
|
||||
}
|
||||
@@ -84,11 +83,11 @@ function extractActiveWorkspaceFromRoute(route: NavigationRouteLike | undefined)
|
||||
return { serverId, workspaceId };
|
||||
}
|
||||
|
||||
export function syncNavigationActiveWorkspace(
|
||||
navigationRef: NavigationObserverRef
|
||||
) {
|
||||
export function syncNavigationActiveWorkspace(navigationRef: NavigationObserverRef) {
|
||||
emitIfChanged(
|
||||
extractActiveWorkspaceFromRoute(navigationRef.current?.getCurrentRoute() as NavigationRouteLike | undefined)
|
||||
extractActiveWorkspaceFromRoute(
|
||||
navigationRef.current?.getCurrentRoute() as NavigationRouteLike | undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
cwd,
|
||||
isGit: true,
|
||||
explorerTabByCheckout: {},
|
||||
})
|
||||
}),
|
||||
).toBe("changes");
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
cwd,
|
||||
isGit: false,
|
||||
explorerTabByCheckout: {},
|
||||
})
|
||||
}),
|
||||
).toBe("files");
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
explorerTabByCheckout: {
|
||||
[key]: "files",
|
||||
},
|
||||
})
|
||||
}),
|
||||
).toBe("files");
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
explorerTabByCheckout: {
|
||||
[key]: "terminals" as any,
|
||||
},
|
||||
})
|
||||
}),
|
||||
).toBe("changes");
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
explorerTabByCheckout: {
|
||||
[key]: "changes",
|
||||
},
|
||||
})
|
||||
}),
|
||||
).toBe("files");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,8 +150,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
}),
|
||||
closeFileExplorer: () =>
|
||||
set((state) => ({
|
||||
mobileView:
|
||||
state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
|
||||
mobileView: state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
|
||||
desktop: {
|
||||
...state.desktop,
|
||||
fileExplorerOpen: false,
|
||||
@@ -164,8 +163,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
// On desktop, closing depends on which panel triggered it
|
||||
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
||||
desktop: {
|
||||
agentListOpen:
|
||||
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
||||
agentListOpen: state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
||||
fileExplorerOpen:
|
||||
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
||||
},
|
||||
@@ -174,7 +172,9 @@ export const usePanelStore = create<PanelState>()(
|
||||
toggleAgentList: () =>
|
||||
set((state) => ({
|
||||
// Mobile: toggle between agent and agent-list
|
||||
mobileView: (state.mobileView === "agent-list" ? "agent" : "agent-list") as MobilePanelView,
|
||||
mobileView: (state.mobileView === "agent-list"
|
||||
? "agent"
|
||||
: "agent-list") as MobilePanelView,
|
||||
desktop: {
|
||||
...state.desktop,
|
||||
agentListOpen: !state.desktop.agentListOpen,
|
||||
@@ -186,9 +186,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
// Mobile: toggle between agent and file-explorer
|
||||
const willOpenMobile = state.mobileView !== "file-explorer";
|
||||
const willOpenDesktop = !state.desktop.fileExplorerOpen;
|
||||
const nextMobileView: MobilePanelView = willOpenMobile
|
||||
? "file-explorer"
|
||||
: "agent";
|
||||
const nextMobileView: MobilePanelView = willOpenMobile ? "file-explorer" : "agent";
|
||||
const nextDesktop = {
|
||||
...state.desktop,
|
||||
fileExplorerOpen: willOpenDesktop,
|
||||
@@ -275,7 +273,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
state.explorerFilesSplitRatio = DEFAULT_EXPLORER_FILES_SPLIT_RATIO;
|
||||
} else {
|
||||
state.explorerFilesSplitRatio = clampExplorerFilesSplitRatio(
|
||||
state.explorerFilesSplitRatio
|
||||
state.explorerFilesSplitRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -294,7 +292,11 @@ export const usePanelStore = create<PanelState>()(
|
||||
state.explorerTab = "changes";
|
||||
}
|
||||
|
||||
if (version < 4 || typeof state.explorerTabByCheckout !== "object" || !state.explorerTabByCheckout) {
|
||||
if (
|
||||
version < 4 ||
|
||||
typeof state.explorerTabByCheckout !== "object" ||
|
||||
!state.explorerTabByCheckout
|
||||
) {
|
||||
state.explorerTabByCheckout = {};
|
||||
} else {
|
||||
const entries = Object.entries(state.explorerTabByCheckout as Record<string, unknown>);
|
||||
@@ -321,8 +323,8 @@ export const usePanelStore = create<PanelState>()(
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
explorerFilesSplitRatio: state.explorerFilesSplitRatio,
|
||||
}),
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,8 +36,8 @@ export const useSectionOrderStore = create<SectionOrderState>()(
|
||||
partialize: (state) => ({
|
||||
projectOrder: state.projectOrder,
|
||||
}),
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ export const useSectionOrderStore = create<SectionOrderState>()(
|
||||
*/
|
||||
export function sortProjectsByStoredOrder<T extends { projectKey: string }>(
|
||||
groups: T[],
|
||||
storedOrder: string[]
|
||||
storedOrder: string[],
|
||||
): T[] {
|
||||
if (storedOrder.length === 0) {
|
||||
return groups;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,29 +6,26 @@ interface SidebarCollapsedSectionsState {
|
||||
setProjectCollapsed: (projectKey: string, collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export const useSidebarCollapsedSectionsStore = create<SidebarCollapsedSectionsState>(
|
||||
(set) => ({
|
||||
collapsedProjectKeys: new Set(),
|
||||
toggleProjectCollapsed: (projectKey) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.collapsedProjectKeys);
|
||||
if (next.has(projectKey)) {
|
||||
next.delete(projectKey);
|
||||
} else {
|
||||
next.add(projectKey);
|
||||
}
|
||||
return { collapsedProjectKeys: next };
|
||||
}),
|
||||
setProjectCollapsed: (projectKey, collapsed) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.collapsedProjectKeys);
|
||||
if (collapsed) {
|
||||
next.add(projectKey);
|
||||
} else {
|
||||
next.delete(projectKey);
|
||||
}
|
||||
return { collapsedProjectKeys: next };
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export const useSidebarCollapsedSectionsStore = create<SidebarCollapsedSectionsState>((set) => ({
|
||||
collapsedProjectKeys: new Set(),
|
||||
toggleProjectCollapsed: (projectKey) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.collapsedProjectKeys);
|
||||
if (next.has(projectKey)) {
|
||||
next.delete(projectKey);
|
||||
} else {
|
||||
next.add(projectKey);
|
||||
}
|
||||
return { collapsedProjectKeys: next };
|
||||
}),
|
||||
setProjectCollapsed: (projectKey, collapsed) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.collapsedProjectKeys);
|
||||
if (collapsed) {
|
||||
next.add(projectKey);
|
||||
} else {
|
||||
next.delete(projectKey);
|
||||
}
|
||||
return { collapsedProjectKeys: next };
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
interface SidebarOrderStoreState {
|
||||
projectOrderByServerId: Record<string, string[]>
|
||||
workspaceOrderByServerAndProject: Record<string, string[]>
|
||||
getProjectOrder: (serverId: string) => string[]
|
||||
setProjectOrder: (serverId: string, keys: string[]) => void
|
||||
getWorkspaceOrder: (serverId: string, projectKey: string) => string[]
|
||||
setWorkspaceOrder: (serverId: string, projectKey: string, keys: string[]) => void
|
||||
projectOrderByServerId: Record<string, string[]>;
|
||||
workspaceOrderByServerAndProject: Record<string, string[]>;
|
||||
getProjectOrder: (serverId: string) => string[];
|
||||
setProjectOrder: (serverId: string, keys: string[]) => void;
|
||||
getWorkspaceOrder: (serverId: string, projectKey: string) => string[];
|
||||
setWorkspaceOrder: (serverId: string, projectKey: string, keys: string[]) => void;
|
||||
}
|
||||
|
||||
function normalizeKeys(keys: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
const normalized: string[] = []
|
||||
const seen = new Set<string>();
|
||||
const normalized: string[] = [];
|
||||
|
||||
for (const rawKey of keys) {
|
||||
const key = rawKey.trim()
|
||||
const key = rawKey.trim();
|
||||
if (!key || seen.has(key)) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
seen.add(key)
|
||||
normalized.push(key)
|
||||
seen.add(key);
|
||||
normalized.push(key);
|
||||
}
|
||||
|
||||
return normalized
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildWorkspaceScopeKey(serverId: string, projectKey: string): string {
|
||||
return `${serverId.trim()}::${projectKey.trim()}`
|
||||
return `${serverId.trim()}::${projectKey.trim()}`;
|
||||
}
|
||||
|
||||
export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
|
||||
@@ -37,57 +37,57 @@ export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
|
||||
projectOrderByServerId: {},
|
||||
workspaceOrderByServerAndProject: {},
|
||||
getProjectOrder: (serverId) => {
|
||||
const key = serverId.trim()
|
||||
const key = serverId.trim();
|
||||
if (!key) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
return get().projectOrderByServerId[key] ?? []
|
||||
return get().projectOrderByServerId[key] ?? [];
|
||||
},
|
||||
setProjectOrder: (serverId, keys) => {
|
||||
const key = serverId.trim()
|
||||
const key = serverId.trim();
|
||||
if (!key) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeKeys(keys)
|
||||
const normalized = normalizeKeys(keys);
|
||||
set((state) => ({
|
||||
projectOrderByServerId: {
|
||||
...state.projectOrderByServerId,
|
||||
[key]: normalized,
|
||||
},
|
||||
}))
|
||||
}));
|
||||
},
|
||||
getWorkspaceOrder: (serverId, projectKey) => {
|
||||
const serverKey = serverId.trim()
|
||||
const projectScope = projectKey.trim()
|
||||
const serverKey = serverId.trim();
|
||||
const projectScope = projectKey.trim();
|
||||
if (!serverKey || !projectScope) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope)
|
||||
return get().workspaceOrderByServerAndProject[scopeKey] ?? []
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope);
|
||||
return get().workspaceOrderByServerAndProject[scopeKey] ?? [];
|
||||
},
|
||||
setWorkspaceOrder: (serverId, projectKey, keys) => {
|
||||
const serverKey = serverId.trim()
|
||||
const projectScope = projectKey.trim()
|
||||
const serverKey = serverId.trim();
|
||||
const projectScope = projectKey.trim();
|
||||
if (!serverKey || !projectScope) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope)
|
||||
const normalized = normalizeKeys(keys)
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope);
|
||||
const normalized = normalizeKeys(keys);
|
||||
set((state) => ({
|
||||
workspaceOrderByServerAndProject: {
|
||||
...state.workspaceOrderByServerAndProject,
|
||||
[scopeKey]: normalized,
|
||||
},
|
||||
}))
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'sidebar-project-workspace-order',
|
||||
name: "sidebar-project-workspace-order",
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
partialize: (state) => ({
|
||||
projectOrderByServerId: state.projectOrderByServerId,
|
||||
workspaceOrderByServerAndProject: state.workspaceOrderByServerAndProject,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -217,12 +217,16 @@ function generateNodeId(prefix: "pane" | "group"): string {
|
||||
return `${prefix}_${randomValue}`;
|
||||
}
|
||||
|
||||
function createPaneNode(input: { id: string; tabs?: WorkspaceTab[]; focusedTabId?: string | null }): SplitNodeInternal {
|
||||
function createPaneNode(input: {
|
||||
id: string;
|
||||
tabs?: WorkspaceTab[];
|
||||
focusedTabId?: string | null;
|
||||
}): SplitNodeInternal {
|
||||
const normalizedTabs = normalizeWorkspaceTabs(input.tabs ?? []);
|
||||
const tabIds = normalizedTabs.map((tab) => tab.tabId);
|
||||
const focusedTabId = tabIds.includes(input.focusedTabId ?? "")
|
||||
? (input.focusedTabId ?? null)
|
||||
: tabIds[tabIds.length - 1] ?? null;
|
||||
: (tabIds[tabIds.length - 1] ?? null);
|
||||
|
||||
return {
|
||||
kind: "pane",
|
||||
@@ -370,11 +374,18 @@ function asInternalNode(node: SplitNode): SplitNodeInternal {
|
||||
return node as SplitNodeInternal;
|
||||
}
|
||||
|
||||
function asInternalLayout(layout: WorkspaceLayout): { root: SplitNodeInternal; focusedPaneId: string } {
|
||||
function asInternalLayout(layout: WorkspaceLayout): {
|
||||
root: SplitNodeInternal;
|
||||
focusedPaneId: string;
|
||||
} {
|
||||
return layout as { root: SplitNodeInternal; focusedPaneId: string };
|
||||
}
|
||||
|
||||
function findPanePathById(node: SplitNodeInternal, paneId: string, path: number[] = []): number[] | null {
|
||||
function findPanePathById(
|
||||
node: SplitNodeInternal,
|
||||
paneId: string,
|
||||
path: number[] = [],
|
||||
): number[] | null {
|
||||
if (node.kind === "pane") {
|
||||
return node.pane.id === paneId ? path : null;
|
||||
}
|
||||
@@ -387,12 +398,19 @@ function findPanePathById(node: SplitNodeInternal, paneId: string, path: number[
|
||||
return null;
|
||||
}
|
||||
|
||||
function findPanePathContainingTab(node: SplitNodeInternal, tabId: string, path: number[] = []): number[] | null {
|
||||
function findPanePathContainingTab(
|
||||
node: SplitNodeInternal,
|
||||
tabId: string,
|
||||
path: number[] = [],
|
||||
): number[] | null {
|
||||
if (node.kind === "pane") {
|
||||
return node.pane.tabs.some((tab) => tab.tabId === tabId) ? path : null;
|
||||
}
|
||||
for (let index = 0; index < node.group.children.length; index += 1) {
|
||||
const childPath = findPanePathContainingTab(node.group.children[index], tabId, [...path, index]);
|
||||
const childPath = findPanePathContainingTab(node.group.children[index], tabId, [
|
||||
...path,
|
||||
index,
|
||||
]);
|
||||
if (childPath) {
|
||||
return childPath;
|
||||
}
|
||||
@@ -400,7 +418,11 @@ function findPanePathContainingTab(node: SplitNodeInternal, tabId: string, path:
|
||||
return null;
|
||||
}
|
||||
|
||||
function findGroupPathById(node: SplitNodeInternal, groupId: string, path: number[] = []): number[] | null {
|
||||
function findGroupPathById(
|
||||
node: SplitNodeInternal,
|
||||
groupId: string,
|
||||
path: number[] = [],
|
||||
): number[] | null {
|
||||
if (node.kind === "pane") {
|
||||
return null;
|
||||
}
|
||||
@@ -428,7 +450,7 @@ function getNodeAtPath(node: SplitNodeInternal, path: number[]): SplitNodeIntern
|
||||
function replaceNodeAtPath(
|
||||
node: SplitNodeInternal,
|
||||
path: number[],
|
||||
updater: (node: SplitNodeInternal) => SplitNodeInternal
|
||||
updater: (node: SplitNodeInternal) => SplitNodeInternal,
|
||||
): SplitNodeInternal {
|
||||
if (path.length === 0) {
|
||||
return updater(node);
|
||||
@@ -437,7 +459,7 @@ function replaceNodeAtPath(
|
||||
invariant(node.kind === "group", "Expected group while replacing split tree node");
|
||||
const [index, ...rest] = path;
|
||||
const nextChildren = node.group.children.map((child, childIndex) =>
|
||||
childIndex === index ? replaceNodeAtPath(child, rest, updater) : child
|
||||
childIndex === index ? replaceNodeAtPath(child, rest, updater) : child,
|
||||
);
|
||||
|
||||
return createGroupNode({
|
||||
@@ -450,7 +472,7 @@ function replaceNodeAtPath(
|
||||
|
||||
function insertChildIntoGroup(
|
||||
groupNode: SplitNodeInternal,
|
||||
input: InsertChildIntoGroupInput
|
||||
input: InsertChildIntoGroupInput,
|
||||
): SplitNodeInternal {
|
||||
invariant(groupNode.kind === "group", "Expected group for split insertion");
|
||||
const nextChildren = groupNode.group.children.slice();
|
||||
@@ -509,7 +531,7 @@ function normalizePaneAfterTabChange(pane: SplitPaneInternal): SplitPaneInternal
|
||||
const tabIds = tabs.map((tab) => tab.tabId);
|
||||
const focusedTabId = tabIds.includes(pane.focusedTabId ?? "")
|
||||
? pane.focusedTabId
|
||||
: tabIds[tabIds.length - 1] ?? null;
|
||||
: (tabIds[tabIds.length - 1] ?? null);
|
||||
|
||||
return {
|
||||
id: pane.id,
|
||||
@@ -561,7 +583,9 @@ function normalizeNode(node: unknown): SplitNodeInternal | null {
|
||||
}
|
||||
|
||||
const children = Array.isArray(rawGroup.children)
|
||||
? rawGroup.children.map((child) => normalizeNode(child)).filter((child): child is SplitNodeInternal => child !== null)
|
||||
? rawGroup.children
|
||||
.map((child) => normalizeNode(child))
|
||||
.filter((child): child is SplitNodeInternal => child !== null)
|
||||
: [];
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
@@ -639,7 +663,7 @@ function removePaneByPath(root: SplitNodeInternal, path: number[]): SplitNodeInt
|
||||
|
||||
function detachTabFromTree(
|
||||
root: SplitNodeInternal,
|
||||
input: DetachTabFromTreeInput
|
||||
input: DetachTabFromTreeInput,
|
||||
): DetachTabFromTreeResult {
|
||||
const panePath = findPanePathContainingTab(root, input.tabId);
|
||||
if (!panePath) {
|
||||
@@ -670,7 +694,10 @@ function detachTabFromTree(
|
||||
};
|
||||
}
|
||||
|
||||
function insertTabIntoPane(root: SplitNodeInternal, input: InsertTabIntoPaneInput): SplitNodeInternal {
|
||||
function insertTabIntoPane(
|
||||
root: SplitNodeInternal,
|
||||
input: InsertTabIntoPaneInput,
|
||||
): SplitNodeInternal {
|
||||
const panePath = findPanePathById(root, input.paneId);
|
||||
invariant(panePath, `Pane not found: ${input.paneId}`);
|
||||
return replaceNodeAtPath(root, panePath, (node) => {
|
||||
@@ -716,14 +743,17 @@ function updateTabInTree(root: SplitNodeInternal, input: UpdateTabInTreeInput):
|
||||
pane: normalizePaneAfterTabChange({
|
||||
...node.pane,
|
||||
tabs: node.pane.tabs.map((tab) =>
|
||||
tab.tabId === input.tabId ? { ...tab, target: input.target } : tab
|
||||
tab.tabId === input.tabId ? { ...tab, target: input.target } : tab,
|
||||
),
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateGroupSizesInTree(root: SplitNodeInternal, input: UpdateGroupSizesInTreeInput): SplitNodeInternal {
|
||||
function updateGroupSizesInTree(
|
||||
root: SplitNodeInternal,
|
||||
input: UpdateGroupSizesInTreeInput,
|
||||
): SplitNodeInternal {
|
||||
const groupPath = findGroupPathById(root, input.groupId);
|
||||
if (!groupPath) {
|
||||
return root;
|
||||
@@ -742,7 +772,10 @@ function updateGroupSizesInTree(root: SplitNodeInternal, input: UpdateGroupSizes
|
||||
});
|
||||
}
|
||||
|
||||
function updatePaneInTree(root: SplitNodeInternal, input: UpdatePaneInTreeInput): SplitNodeInternal {
|
||||
function updatePaneInTree(
|
||||
root: SplitNodeInternal,
|
||||
input: UpdatePaneInTreeInput,
|
||||
): SplitNodeInternal {
|
||||
const panePath = findPanePathById(root, input.paneId);
|
||||
if (!panePath) {
|
||||
return root;
|
||||
@@ -757,7 +790,8 @@ function updatePaneInTree(root: SplitNodeInternal, input: UpdatePaneInTreeInput)
|
||||
}
|
||||
|
||||
function insertSplitInternal(input: InsertSplitInternalInput): InsertSplitInternalResult {
|
||||
const direction = input.position === "left" || input.position === "right" ? "horizontal" : "vertical";
|
||||
const direction =
|
||||
input.position === "left" || input.position === "right" ? "horizontal" : "vertical";
|
||||
const insertAfter = input.position === "right" || input.position === "bottom";
|
||||
|
||||
const targetPathBeforeDetach = findPanePathById(input.root, input.targetPaneId);
|
||||
@@ -783,10 +817,7 @@ function insertSplitInternal(input: InsertSplitInternalInput): InsertSplitIntern
|
||||
|
||||
const parentPath = targetPath.slice(0, -1);
|
||||
const targetIndex = targetPath[targetPath.length - 1] ?? 0;
|
||||
const parentNode =
|
||||
parentPath.length > 0
|
||||
? getNodeAtPath(detached.root, parentPath)
|
||||
: null;
|
||||
const parentNode = parentPath.length > 0 ? getNodeAtPath(detached.root, parentPath) : null;
|
||||
|
||||
if (parentNode?.kind === "group" && parentNode.group.direction === direction) {
|
||||
const targetSize = parentNode.group.sizes[targetIndex] ?? 0;
|
||||
@@ -801,7 +832,7 @@ function insertSplitInternal(input: InsertSplitInternalInput): InsertSplitIntern
|
||||
index: insertIndex,
|
||||
node: newPaneNode,
|
||||
sizes: nextSizes,
|
||||
})
|
||||
}),
|
||||
),
|
||||
newPaneId,
|
||||
};
|
||||
@@ -902,7 +933,7 @@ export function insertSplit(
|
||||
root: SplitNode,
|
||||
targetPaneId: string,
|
||||
tabId: string,
|
||||
position: "left" | "right" | "top" | "bottom"
|
||||
position: "left" | "right" | "top" | "bottom",
|
||||
): SplitNode {
|
||||
return insertSplitInternal({
|
||||
root: asInternalNode(root),
|
||||
@@ -929,15 +960,16 @@ export function removeTabFromTree(root: SplitNode, tabId: string): SplitNode {
|
||||
export function openTabInLayout(input: OpenTabInLayoutInput): OpenTabInLayoutResult {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
const existingTab = collectAllTabs(layout.root).find((tab) =>
|
||||
workspaceTabTargetsEqual(tab.target, input.target)
|
||||
workspaceTabTargetsEqual(tab.target, input.target),
|
||||
);
|
||||
if (existingTab) {
|
||||
return {
|
||||
tabId: existingTab.tabId,
|
||||
layout: focusTabInLayout({
|
||||
layout,
|
||||
tabId: existingTab.tabId,
|
||||
}) ?? input.layout,
|
||||
layout:
|
||||
focusTabInLayout({
|
||||
layout,
|
||||
tabId: existingTab.tabId,
|
||||
}) ?? input.layout,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1005,7 +1037,9 @@ export function focusTabInLayout(input: FocusTabInLayoutInput): WorkspaceLayout
|
||||
};
|
||||
}
|
||||
|
||||
export function retargetTabInLayout(input: RetargetTabInLayoutInput): RetargetTabInLayoutResult | null {
|
||||
export function retargetTabInLayout(
|
||||
input: RetargetTabInLayoutInput,
|
||||
): RetargetTabInLayoutResult | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
const pane = findPaneContainingTab(layout.root, input.tabId);
|
||||
if (!pane) {
|
||||
@@ -1033,7 +1067,7 @@ export function retargetTabInLayout(input: RetargetTabInLayoutInput): RetargetTa
|
||||
}
|
||||
|
||||
export function reorderFocusedPaneTabsInLayout(
|
||||
input: ReorderFocusedPaneTabsInLayoutInput
|
||||
input: ReorderFocusedPaneTabsInLayoutInput,
|
||||
): WorkspaceLayout | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
if (!findPaneById(layout.root, layout.focusedPaneId)) {
|
||||
@@ -1078,13 +1112,16 @@ export function splitPaneInLayout(input: SplitPaneInLayoutInput): SplitPaneInLay
|
||||
};
|
||||
}
|
||||
|
||||
export function splitPaneEmptyInLayout(input: SplitPaneEmptyInLayoutInput): SplitPaneInLayoutResult | null {
|
||||
export function splitPaneEmptyInLayout(
|
||||
input: SplitPaneEmptyInLayoutInput,
|
||||
): SplitPaneInLayoutResult | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
if (!findPaneById(layout.root, input.targetPaneId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const direction = input.position === "left" || input.position === "right" ? "horizontal" : "vertical";
|
||||
const direction =
|
||||
input.position === "left" || input.position === "right" ? "horizontal" : "vertical";
|
||||
const insertAfter = input.position === "right" || input.position === "bottom";
|
||||
|
||||
const targetPath = findPanePathById(layout.root, input.targetPaneId);
|
||||
@@ -1107,7 +1144,7 @@ export function splitPaneEmptyInLayout(input: SplitPaneEmptyInLayoutInput): Spli
|
||||
nextSizes.splice(insertIndex, 0, targetSize / 2);
|
||||
nextSizes[targetIndex + (insertAfter ? 0 : 1)] = targetSize / 2;
|
||||
nextRoot = replaceNodeAtPath(layout.root, parentPath, () =>
|
||||
insertChildIntoGroup(parentNode, { index: insertIndex, node: newPaneNode, sizes: nextSizes })
|
||||
insertChildIntoGroup(parentNode, { index: insertIndex, node: newPaneNode, sizes: nextSizes }),
|
||||
);
|
||||
} else {
|
||||
const newGroup = createGroupNode({
|
||||
@@ -1181,7 +1218,9 @@ export function resizeSplitInLayout(input: ResizeSplitInLayoutInput): WorkspaceL
|
||||
};
|
||||
}
|
||||
|
||||
export function reorderPaneTabsInLayout(input: ReorderPaneTabsInLayoutInput): WorkspaceLayout | null {
|
||||
export function reorderPaneTabsInLayout(
|
||||
input: ReorderPaneTabsInLayoutInput,
|
||||
): WorkspaceLayout | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
if (!findPaneById(layout.root, input.paneId)) {
|
||||
return null;
|
||||
|
||||
@@ -190,7 +190,9 @@ describe("workspace-layout-store tree transforms", () => {
|
||||
|
||||
expect(nextGroup.group.sizes).toEqual([0.2, 0.8]);
|
||||
expect(collectAllPanes(nextRoot).map((pane) => pane.id)).toEqual(["left", "bottom-right"]);
|
||||
expect(nextGroup.group.children[1]).toEqual(createPane({ id: "bottom-right", tabIds: ["tab-c"] }));
|
||||
expect(nextGroup.group.children[1]).toEqual(
|
||||
createPane({ id: "bottom-right", tabIds: ["tab-c"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("removeTabFromTree collapses empty panes but keeps the final root pane", () => {
|
||||
@@ -228,7 +230,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("opens tabs into the focused pane and focuses duplicate opens instead of creating them", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -244,7 +246,10 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(splitPaneId).toBe("pane_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
|
||||
store.focusPane(workspaceKey, "main");
|
||||
const duplicateTabId = store.openTab(workspaceKey, { kind: "file", path: "/repo/worktree/b.ts" });
|
||||
const duplicateTabId = store.openTab(workspaceKey, {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/b.ts",
|
||||
});
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(firstTabId).toBe("file_/repo/worktree/a.ts");
|
||||
@@ -259,7 +264,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("focusTab moves workspace focus to the pane containing the tab", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
|
||||
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -285,7 +290,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("retargetTab updates the existing tab target without moving it to a different pane", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"12121212-1212-1212-1212-121212121212"
|
||||
"12121212-1212-1212-1212-121212121212",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -333,16 +338,28 @@ describe("workspace-layout-store actions", () => {
|
||||
tabIds: [thirdTabId!, firstTabId!, secondTabId!],
|
||||
focusedTabId: thirdTabId,
|
||||
tabs: [
|
||||
{ tabId: thirdTabId, target: { kind: "file", path: "/repo/worktree/c.ts" }, createdAt: expect.any(Number) },
|
||||
{ tabId: firstTabId, target: { kind: "file", path: "/repo/worktree/a.ts" }, createdAt: expect.any(Number) },
|
||||
{ tabId: secondTabId, target: { kind: "file", path: "/repo/worktree/b.ts" }, createdAt: expect.any(Number) },
|
||||
{
|
||||
tabId: thirdTabId,
|
||||
target: { kind: "file", path: "/repo/worktree/c.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
{
|
||||
tabId: firstTabId,
|
||||
target: { kind: "file", path: "/repo/worktree/a.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
{
|
||||
tabId: secondTabId,
|
||||
target: { kind: "file", path: "/repo/worktree/b.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("reorderTabsInPane reorders tabs in the requested pane without changing focused pane", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"34343434-3434-3434-3434-343434343434"
|
||||
"34343434-3434-3434-3434-343434343434",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -369,15 +386,23 @@ describe("workspace-layout-store actions", () => {
|
||||
tabIds: [fourthTabId!, thirdTabId!],
|
||||
focusedTabId: fourthTabId,
|
||||
tabs: [
|
||||
{ tabId: fourthTabId, target: { kind: "file", path: "/repo/worktree/d.ts" }, createdAt: expect.any(Number) },
|
||||
{ tabId: thirdTabId, target: { kind: "file", path: "/repo/worktree/c.ts" }, createdAt: expect.any(Number) },
|
||||
{
|
||||
tabId: fourthTabId,
|
||||
target: { kind: "file", path: "/repo/worktree/d.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
{
|
||||
tabId: thirdTabId,
|
||||
target: { kind: "file", path: "/repo/worktree/c.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("focusPane switches workspace focus to a different pane", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"56565656-5656-5656-5656-565656565656"
|
||||
"56565656-5656-5656-5656-565656565656",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -403,7 +428,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("closeTab collapses an emptied pane and keeps the nearest sibling focused", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"cccccccc-cccc-cccc-cccc-cccccccccccc"
|
||||
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -475,7 +500,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("moveTabToPane collapses the source pane when its last tab moves out", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"dddddddd-dddd-dddd-dddd-dddddddddddd"
|
||||
"dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -542,7 +567,7 @@ describe("workspace-layout-store actions", () => {
|
||||
tabIds: child.pane.tabIds,
|
||||
focusedTabId: child.pane.focusedTabId,
|
||||
};
|
||||
})
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: "main",
|
||||
@@ -560,7 +585,7 @@ describe("workspace-layout-store actions", () => {
|
||||
|
||||
it("openTab focuses the existing tab instead of creating a duplicate entry", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue(
|
||||
"abababab-abab-abab-abab-abababababab"
|
||||
"abababab-abab-abab-abab-abababababab",
|
||||
);
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = useWorkspaceLayoutStore.getState();
|
||||
@@ -574,7 +599,10 @@ describe("workspace-layout-store actions", () => {
|
||||
});
|
||||
|
||||
store.focusPane(workspaceKey, "main");
|
||||
const duplicateTabId = store.openTab(workspaceKey, { kind: "file", path: "/repo/worktree/b.ts" });
|
||||
const duplicateTabId = store.openTab(workspaceKey, {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/b.ts",
|
||||
});
|
||||
const layout = useWorkspaceLayoutStore.getState().layoutByWorkspace[workspaceKey]!;
|
||||
|
||||
expect(splitPaneId).toBe("pane_abababab-abab-abab-abab-abababababab");
|
||||
|
||||
@@ -66,14 +66,14 @@ interface WorkspaceLayoutStore {
|
||||
tabId: string;
|
||||
targetPaneId: string;
|
||||
position: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
},
|
||||
) => string | null;
|
||||
splitPaneEmpty: (
|
||||
workspaceKey: string,
|
||||
input: {
|
||||
targetPaneId: string;
|
||||
position: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
},
|
||||
) => string | null;
|
||||
moveTabToPane: (workspaceKey: string, tabId: string, toPaneId: string) => void;
|
||||
focusPane: (workspaceKey: string, paneId: string) => void;
|
||||
@@ -93,7 +93,10 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function getWorkspaceLayout(state: Record<string, WorkspaceLayout>, workspaceKey: string): WorkspaceLayout {
|
||||
function getWorkspaceLayout(
|
||||
state: Record<string, WorkspaceLayout>,
|
||||
workspaceKey: string,
|
||||
): WorkspaceLayout {
|
||||
return normalizeLayout(state[workspaceKey] ?? createDefaultLayout());
|
||||
}
|
||||
|
||||
@@ -227,7 +230,9 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
if (!normalizedWorkspaceKey) {
|
||||
return [];
|
||||
}
|
||||
return collectAllTabs(getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey).root);
|
||||
return collectAllTabs(
|
||||
getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey).root,
|
||||
);
|
||||
},
|
||||
splitPane: (workspaceKey, input) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
@@ -464,6 +469,6 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
splitSizesByWorkspace: state.splitSizesByWorkspace,
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,7 +15,10 @@ vi.mock("@react-native-async-storage/async-storage", () => {
|
||||
};
|
||||
});
|
||||
|
||||
import { buildWorkspaceTabPersistenceKey, useWorkspaceTabsStore } from "@/stores/workspace-tabs-store";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceTabsStore,
|
||||
} from "@/stores/workspace-tabs-store";
|
||||
|
||||
const SERVER_ID = "server-1";
|
||||
const WORKSPACE_ID = "/repo/worktree";
|
||||
@@ -97,7 +100,7 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
});
|
||||
expect(focusedTabId).toBe("terminal_term-1");
|
||||
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
|
||||
"terminal_term-1"
|
||||
"terminal_term-1",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,7 +132,7 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
const tabs = state.uiTabsByWorkspace[workspaceKey] ?? [];
|
||||
const order = state.tabOrderByWorkspace[workspaceKey] ?? [];
|
||||
const matchingTabs = tabs.filter(
|
||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === "created-agent"
|
||||
(tab) => tab.target.kind === "agent" && tab.target.agentId === "created-agent",
|
||||
);
|
||||
|
||||
expect(ensured).toBe(draftTabId);
|
||||
@@ -162,7 +165,7 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
});
|
||||
|
||||
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
|
||||
focusedFileTabId
|
||||
focusedFileTabId,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -185,7 +188,7 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
expect(fileTabId).toBe("file_/repo/worktree/src/index.ts");
|
||||
expect(terminalTabId).toBe("terminal_term-1");
|
||||
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
|
||||
terminalTabId
|
||||
terminalTabId,
|
||||
);
|
||||
|
||||
const reopenedFileTabId = useWorkspaceTabsStore.getState().openOrFocusTab({
|
||||
@@ -195,8 +198,6 @@ describe("workspace-tabs-store retargetTab", () => {
|
||||
});
|
||||
|
||||
expect(reopenedFileTabId).toBe(fileTabId);
|
||||
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(
|
||||
fileTabId
|
||||
);
|
||||
expect(useWorkspaceTabsStore.getState().focusedTabIdByWorkspace[workspaceKey]).toBe(fileTabId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,9 @@ export function buildWorkspaceTabPersistenceKey(input: {
|
||||
return `${serverId}:${normalizeWorkspaceId(workspaceId)}`;
|
||||
}
|
||||
|
||||
function normalizeTabTarget(value: WorkspaceTabTarget | null | undefined): WorkspaceTabTarget | null {
|
||||
function normalizeTabTarget(
|
||||
value: WorkspaceTabTarget | null | undefined,
|
||||
): WorkspaceTabTarget | null {
|
||||
if (!value || typeof value !== "object" || typeof value.kind !== "string") {
|
||||
return null;
|
||||
}
|
||||
@@ -121,7 +123,11 @@ type WorkspaceTabsState = {
|
||||
uiTabsByWorkspace: Record<string, WorkspaceTab[]>;
|
||||
tabOrderByWorkspace: Record<string, string[]>;
|
||||
focusedTabIdByWorkspace: Record<string, string>;
|
||||
openDraftTab: (input: { serverId: string; workspaceId: string; draftId: string }) => string | null;
|
||||
openDraftTab: (input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
draftId: string;
|
||||
}) => string | null;
|
||||
ensureTab: (input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
@@ -184,14 +190,17 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
const existingIndex = currentTabs.findIndex((tab) => tab.tabId === effectiveTabId);
|
||||
const nextTabs = (() => {
|
||||
if (existingIndex < 0) {
|
||||
return [...currentTabs, { tabId: effectiveTabId, target: normalizedTarget, createdAt: now }];
|
||||
return [
|
||||
...currentTabs,
|
||||
{ tabId: effectiveTabId, target: normalizedTarget, createdAt: now },
|
||||
];
|
||||
}
|
||||
const existing = currentTabs[existingIndex];
|
||||
if (existing && tabTargetsEqual(existing.target, normalizedTarget)) {
|
||||
return currentTabs;
|
||||
}
|
||||
return currentTabs.map((tab, index) =>
|
||||
index === existingIndex ? { ...tab, target: normalizedTarget } : tab
|
||||
index === existingIndex ? { ...tab, target: normalizedTarget } : tab,
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -271,7 +280,9 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
|
||||
const currentFocused = state.focusedTabIdByWorkspace[key] ?? null;
|
||||
const nextFocused =
|
||||
currentFocused !== normalizedTabId ? currentFocused : nextOrder[nextOrder.length - 1] ?? null;
|
||||
currentFocused !== normalizedTabId
|
||||
? currentFocused
|
||||
: (nextOrder[nextOrder.length - 1] ?? null);
|
||||
const nextFocusedByWorkspace = (() => {
|
||||
if (!nextFocused) {
|
||||
const { [key]: _removed, ...rest } = state.focusedTabIdByWorkspace;
|
||||
@@ -319,7 +330,7 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
}
|
||||
|
||||
const nextTabs = currentTabs.map((tab, tabIndex) =>
|
||||
tabIndex === index ? { ...tab, target: normalizedTarget } : tab
|
||||
tabIndex === index ? { ...tab, target: normalizedTarget } : tab,
|
||||
);
|
||||
retargetedTabId = normalizedTabId;
|
||||
return {
|
||||
@@ -435,12 +446,23 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
const rawState = (legacy as any)?.state ?? legacy ?? {};
|
||||
|
||||
const rawUiTabsByWorkspace =
|
||||
rawState.uiTabsByWorkspace ?? rawState.openTabsByWorkspace ?? legacy?.uiTabsByWorkspace ?? legacy?.openTabsByWorkspace ?? {};
|
||||
rawState.uiTabsByWorkspace ??
|
||||
rawState.openTabsByWorkspace ??
|
||||
legacy?.uiTabsByWorkspace ??
|
||||
legacy?.openTabsByWorkspace ??
|
||||
{};
|
||||
const rawFocused =
|
||||
rawState.focusedTabIdByWorkspace ?? legacy?.focusedTabIdByWorkspace ?? rawState.lastFocusedTabByWorkspace ?? {};
|
||||
rawState.focusedTabIdByWorkspace ??
|
||||
legacy?.focusedTabIdByWorkspace ??
|
||||
rawState.lastFocusedTabByWorkspace ??
|
||||
{};
|
||||
const rawOrder =
|
||||
rawState.tabOrderByWorkspace ?? legacy?.tabOrderByWorkspace ?? rawState.tabOrderByWorkspace ?? {};
|
||||
const legacyOrder = rawState.tabOrderByWorkspace ?? rawState.tabOrderLegacyByWorkspace ?? {};
|
||||
rawState.tabOrderByWorkspace ??
|
||||
legacy?.tabOrderByWorkspace ??
|
||||
rawState.tabOrderByWorkspace ??
|
||||
{};
|
||||
const legacyOrder =
|
||||
rawState.tabOrderByWorkspace ?? rawState.tabOrderLegacyByWorkspace ?? {};
|
||||
|
||||
const uiTabsByWorkspace: Record<string, WorkspaceTab[]> = {};
|
||||
const tabOrderByWorkspace: Record<string, string[]> = {};
|
||||
@@ -544,7 +566,11 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
focusedTabIdByWorkspace[key] = `agent_${value.agentId.trim()}`;
|
||||
continue;
|
||||
}
|
||||
if (value.kind === "terminal" && typeof value.terminalId === "string" && value.terminalId.trim()) {
|
||||
if (
|
||||
value.kind === "terminal" &&
|
||||
typeof value.terminalId === "string" &&
|
||||
value.terminalId.trim()
|
||||
) {
|
||||
focusedTabIdByWorkspace[key] = `terminal_${value.terminalId.trim()}`;
|
||||
continue;
|
||||
}
|
||||
@@ -559,6 +585,6 @@ export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
focusedTabIdByWorkspace,
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user