feat: projects → workspaces → tabs
This commit is contained in:
@@ -18,7 +18,7 @@ interface DownloadProgress {
|
||||
export interface Download {
|
||||
id: string;
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
scopeId: string;
|
||||
fileName: string;
|
||||
status: "downloading" | "complete" | "error";
|
||||
message?: string;
|
||||
@@ -32,14 +32,11 @@ interface DownloadState {
|
||||
|
||||
startDownload: (params: {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
scopeId: string;
|
||||
fileName: string;
|
||||
path: string;
|
||||
daemonProfile: HostProfile | undefined;
|
||||
requestFileDownloadToken: (
|
||||
agentId: string,
|
||||
path: string
|
||||
) => Promise<{
|
||||
requestFileDownloadToken: (path: string) => Promise<{
|
||||
token: string | null;
|
||||
fileName: string | null;
|
||||
mimeType: string | null;
|
||||
@@ -64,7 +61,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
|
||||
startDownload: async ({
|
||||
serverId,
|
||||
agentId,
|
||||
scopeId,
|
||||
fileName,
|
||||
path,
|
||||
daemonProfile,
|
||||
@@ -74,7 +71,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
const download: Download = {
|
||||
id,
|
||||
serverId,
|
||||
agentId,
|
||||
scopeId,
|
||||
fileName,
|
||||
status: "downloading",
|
||||
startedAt: Date.now(),
|
||||
@@ -86,7 +83,7 @@ export const useDownloadStore = create<DownloadState>()((set, get) => ({
|
||||
}));
|
||||
|
||||
try {
|
||||
const tokenResponse = await requestFileDownloadToken(agentId, path);
|
||||
const tokenResponse = await requestFileDownloadToken(path);
|
||||
if (tokenResponse.error || !tokenResponse.token) {
|
||||
throw new Error(tokenResponse.error ?? "Failed to request download token.");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type ExplorerTab = "changes" | "files" | "terminals";
|
||||
export type ExplorerTab = "changes" | "files";
|
||||
|
||||
export function isExplorerTab(value: unknown): value is ExplorerTab {
|
||||
return value === "changes" || value === "files" || value === "terminals";
|
||||
return value === "changes" || value === "files";
|
||||
}
|
||||
|
||||
export function buildExplorerCheckoutKey(serverId: string, cwd: string): string | null {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("panel-store explorer tab resolution", () => {
|
||||
).toBe("files");
|
||||
});
|
||||
|
||||
it("restores a stored terminals tab for git checkouts", () => {
|
||||
it("falls back to default when stored tab is invalid", () => {
|
||||
const key = buildExplorerCheckoutKey(serverId, cwd)!;
|
||||
expect(
|
||||
resolveExplorerTabForCheckout({
|
||||
@@ -52,10 +52,10 @@ describe("panel-store explorer tab resolution", () => {
|
||||
cwd,
|
||||
isGit: true,
|
||||
explorerTabByCheckout: {
|
||||
[key]: "terminals",
|
||||
[key]: "terminals" as any,
|
||||
},
|
||||
})
|
||||
).toBe("terminals");
|
||||
).toBe("changes");
|
||||
});
|
||||
|
||||
it("coerces stored changes to files for non-git checkouts", () => {
|
||||
|
||||
@@ -336,7 +336,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
}),
|
||||
{
|
||||
name: "panel-state",
|
||||
version: 4,
|
||||
version: 5,
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
migrate: (persistedState, version) => {
|
||||
const state = persistedState as Partial<PanelState> & Record<string, unknown>;
|
||||
@@ -369,6 +369,10 @@ export const usePanelStore = create<PanelState>()(
|
||||
}
|
||||
}
|
||||
|
||||
if (!isExplorerTab(state.explorerTab)) {
|
||||
state.explorerTab = "changes";
|
||||
}
|
||||
|
||||
if (version < 4 || typeof state.explorerTabByCheckout !== "object" || !state.explorerTabByCheckout) {
|
||||
state.explorerTabByCheckout = {};
|
||||
} else {
|
||||
|
||||
@@ -1,99 +1,93 @@
|
||||
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 {
|
||||
orderByServerId: Record<string, string[]>;
|
||||
getOrder: (serverId: string) => string[];
|
||||
setOrder: (serverId: string, keys: string[]) => void;
|
||||
insertAtTop: (serverId: string, key: string) => void;
|
||||
remove: (serverId: string, key: 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()}`
|
||||
}
|
||||
|
||||
export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
orderByServerId: {},
|
||||
getOrder: (serverId) => {
|
||||
const key = serverId.trim();
|
||||
projectOrderByServerId: {},
|
||||
workspaceOrderByServerAndProject: {},
|
||||
getProjectOrder: (serverId) => {
|
||||
const key = serverId.trim()
|
||||
if (!key) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return get().orderByServerId[key] ?? [];
|
||||
return get().projectOrderByServerId[key] ?? []
|
||||
},
|
||||
setOrder: (serverId, keys) => {
|
||||
const key = serverId.trim();
|
||||
setProjectOrder: (serverId, keys) => {
|
||||
const key = serverId.trim()
|
||||
if (!key) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const normalized = normalizeKeys(keys);
|
||||
const normalized = normalizeKeys(keys)
|
||||
set((state) => ({
|
||||
orderByServerId: {
|
||||
...state.orderByServerId,
|
||||
projectOrderByServerId: {
|
||||
...state.projectOrderByServerId,
|
||||
[key]: normalized,
|
||||
},
|
||||
}));
|
||||
}))
|
||||
},
|
||||
insertAtTop: (serverId, rawKey) => {
|
||||
const serverKey = serverId.trim();
|
||||
const entryKey = rawKey.trim();
|
||||
if (!serverKey || !entryKey) {
|
||||
return;
|
||||
getWorkspaceOrder: (serverId, projectKey) => {
|
||||
const serverKey = serverId.trim()
|
||||
const projectScope = projectKey.trim()
|
||||
if (!serverKey || !projectScope) {
|
||||
return []
|
||||
}
|
||||
set((state) => {
|
||||
const current = state.orderByServerId[serverKey] ?? [];
|
||||
const next = [entryKey, ...current.filter((key) => key !== entryKey)];
|
||||
return {
|
||||
orderByServerId: {
|
||||
...state.orderByServerId,
|
||||
[serverKey]: next,
|
||||
},
|
||||
};
|
||||
});
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope)
|
||||
return get().workspaceOrderByServerAndProject[scopeKey] ?? []
|
||||
},
|
||||
remove: (serverId, rawKey) => {
|
||||
const serverKey = serverId.trim();
|
||||
const entryKey = rawKey.trim();
|
||||
if (!serverKey || !entryKey) {
|
||||
return;
|
||||
setWorkspaceOrder: (serverId, projectKey, keys) => {
|
||||
const serverKey = serverId.trim()
|
||||
const projectScope = projectKey.trim()
|
||||
if (!serverKey || !projectScope) {
|
||||
return
|
||||
}
|
||||
set((state) => {
|
||||
const current = state.orderByServerId[serverKey] ?? [];
|
||||
const next = current.filter((key) => key !== entryKey);
|
||||
if (next.length === current.length) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
orderByServerId: {
|
||||
...state.orderByServerId,
|
||||
[serverKey]: next,
|
||||
},
|
||||
};
|
||||
});
|
||||
const scopeKey = buildWorkspaceScopeKey(serverKey, projectScope)
|
||||
const normalized = normalizeKeys(keys)
|
||||
set((state) => ({
|
||||
workspaceOrderByServerAndProject: {
|
||||
...state.workspaceOrderByServerAndProject,
|
||||
[scopeKey]: normalized,
|
||||
},
|
||||
}))
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "sidebar-agent-order",
|
||||
name: 'sidebar-project-workspace-order',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
partialize: (state) => ({
|
||||
orderByServerId: state.orderByServerId,
|
||||
projectOrderByServerId: state.projectOrderByServerId,
|
||||
workspaceOrderByServerAndProject: state.workspaceOrderByServerAndProject,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
export type WorkspaceTabTarget =
|
||||
| { kind: "agent"; agentId: string }
|
||||
| { kind: "terminal"; terminalId: string };
|
||||
|
||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeWorkspaceTab(
|
||||
value: WorkspaceTabTarget | null | undefined
|
||||
): WorkspaceTabTarget | null {
|
||||
if (!value || typeof value !== "object" || typeof value.kind !== "string") {
|
||||
return null;
|
||||
}
|
||||
if (value.kind === "agent") {
|
||||
const agentId = trimNonEmpty(value.agentId);
|
||||
if (!agentId) {
|
||||
return null;
|
||||
}
|
||||
return { kind: "agent", agentId };
|
||||
}
|
||||
if (value.kind === "terminal") {
|
||||
const terminalId = trimNonEmpty(value.terminalId);
|
||||
if (!terminalId) {
|
||||
return null;
|
||||
}
|
||||
return { kind: "terminal", terminalId };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildWorkspaceTabPersistenceKey(input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
}): string | null {
|
||||
const serverId = trimNonEmpty(input.serverId);
|
||||
const workspaceId = trimNonEmpty(input.workspaceId);
|
||||
if (!serverId || !workspaceId) {
|
||||
return null;
|
||||
}
|
||||
return `${serverId}:${workspaceId}`;
|
||||
}
|
||||
|
||||
type WorkspaceTabsState = {
|
||||
lastFocusedTabByWorkspace: Record<string, WorkspaceTabTarget>;
|
||||
setLastFocusedTab: (input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
tab: WorkspaceTabTarget;
|
||||
}) => void;
|
||||
getLastFocusedTab: (input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
}) => WorkspaceTabTarget | null;
|
||||
};
|
||||
|
||||
export const useWorkspaceTabsStore = create<WorkspaceTabsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
lastFocusedTabByWorkspace: {},
|
||||
setLastFocusedTab: ({ serverId, workspaceId, tab }) => {
|
||||
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
|
||||
const normalizedTab = normalizeWorkspaceTab(tab);
|
||||
if (!key || !normalizedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const current = state.lastFocusedTabByWorkspace[key];
|
||||
if (
|
||||
current &&
|
||||
current.kind === normalizedTab.kind &&
|
||||
((current.kind === "agent" && normalizedTab.kind === "agent"
|
||||
? current.agentId === normalizedTab.agentId
|
||||
: current.kind === "terminal" && normalizedTab.kind === "terminal"
|
||||
? current.terminalId === normalizedTab.terminalId
|
||||
: false))
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
lastFocusedTabByWorkspace: {
|
||||
...state.lastFocusedTabByWorkspace,
|
||||
[key]: normalizedTab,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
getLastFocusedTab: ({ serverId, workspaceId }) => {
|
||||
const key = buildWorkspaceTabPersistenceKey({ serverId, workspaceId });
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
const value = get().lastFocusedTabByWorkspace[key];
|
||||
return normalizeWorkspaceTab(value);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "workspace-tabs-state",
|
||||
version: 1,
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
migrate: (persistedState) => {
|
||||
const state = persistedState as
|
||||
| {
|
||||
lastFocusedTabByWorkspace?: Record<string, WorkspaceTabTarget>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const raw = state?.lastFocusedTabByWorkspace ?? {};
|
||||
const next: Record<string, WorkspaceTabTarget> = {};
|
||||
|
||||
for (const key in raw) {
|
||||
const value = raw[key];
|
||||
const normalized = normalizeWorkspaceTab(value);
|
||||
if (normalized) {
|
||||
next[key] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
lastFocusedTabByWorkspace: next,
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user