Merge branch 'main' into dev
Resolve 38 conflicts preserving both sides' work: - Server: DB-based registries (dev) + WorkspaceGitService/DaemonConfigStore (main) - App: script proxy/voice MCP (dev) + pin/hide tabs/bootstrap boundary (main) - Shared: WorkspaceScriptPayload (dev) + git/GitHub runtime schemas (main) - Deleted workspace-registry-bootstrap.ts (dev's DB approach supersedes)
This commit is contained in:
@@ -256,7 +256,7 @@ function resolveFormState(
|
||||
if (!userModified.thinkingOptionId) {
|
||||
const effectiveModelId = result.model.trim();
|
||||
const preferredThinking = effectiveModelId
|
||||
? providerPrefs?.thinkingByModel?.[effectiveModelId]?.trim() ?? ""
|
||||
? (providerPrefs?.thinkingByModel?.[effectiveModelId]?.trim() ?? "")
|
||||
: "";
|
||||
|
||||
if (initialThinkingOptionId.length > 0) {
|
||||
@@ -329,11 +329,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
onlineServerIds = [],
|
||||
} = options;
|
||||
|
||||
const {
|
||||
preferences,
|
||||
isLoading: isPreferencesLoading,
|
||||
updatePreferences,
|
||||
} = useFormPreferences();
|
||||
const { preferences, isLoading: isPreferencesLoading, updatePreferences } = useFormPreferences();
|
||||
|
||||
const daemons = useHosts();
|
||||
|
||||
@@ -503,13 +499,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
]);
|
||||
|
||||
// User setters - mark fields as modified and persist to preferences
|
||||
const setSelectedServerIdFromUser = useCallback(
|
||||
(value: string | null) => {
|
||||
setFormState((prev) => ({ ...prev, serverId: value }));
|
||||
setUserModified((prev) => ({ ...prev, serverId: true }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setSelectedServerIdFromUser = useCallback((value: string | null) => {
|
||||
setFormState((prev) => ({ ...prev, serverId: value }));
|
||||
setUserModified((prev) => ({ ...prev, serverId: true }));
|
||||
}, []);
|
||||
|
||||
const setProviderFromUser = useCallback(
|
||||
(provider: AgentProvider) => {
|
||||
@@ -520,8 +513,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
const providerDef = selectableProviderDefinitionMap.get(provider);
|
||||
const providerPrefs = preferences?.providerPreferences?.[provider];
|
||||
|
||||
const isValidModel = (m: string) =>
|
||||
providerModels?.some((am) => am.id === m) ?? false;
|
||||
const isValidModel = (m: string) => providerModels?.some((am) => am.id === m) ?? false;
|
||||
const preferredModel = normalizeSelectedModelId(providerPrefs?.model);
|
||||
const defaultModelId = resolveDefaultModelId(providerModels);
|
||||
const nextModelId =
|
||||
@@ -533,10 +525,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
const nextModeId =
|
||||
providerPrefs?.mode && validModeIds.includes(providerPrefs.mode)
|
||||
? providerPrefs.mode
|
||||
: providerDef?.defaultModeId ?? "";
|
||||
: (providerDef?.defaultModeId ?? "");
|
||||
|
||||
const preferredThinking = nextModelId
|
||||
? providerPrefs?.thinkingByModel?.[nextModelId]?.trim() ?? ""
|
||||
? (providerPrefs?.thinkingByModel?.[nextModelId]?.trim() ?? "")
|
||||
: "";
|
||||
const nextThinkingOptionId = resolveThinkingOptionId({
|
||||
availableModels: providerModels,
|
||||
@@ -591,13 +583,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
[allProviderModels, selectableProviderDefinitionMap, updatePreferences],
|
||||
);
|
||||
|
||||
const setModeFromUser = useCallback(
|
||||
(modeId: string) => {
|
||||
setFormState((prev) => ({ ...prev, modeId }));
|
||||
setUserModified((prev) => ({ ...prev, modeId: true }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setModeFromUser = useCallback((modeId: string) => {
|
||||
setFormState((prev) => ({ ...prev, modeId }));
|
||||
setUserModified((prev) => ({ ...prev, modeId: true }));
|
||||
}, []);
|
||||
|
||||
const setModelFromUser = useCallback(
|
||||
(modelId: string) => {
|
||||
@@ -620,25 +609,19 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
[availableModels, userModified.thinkingOptionId],
|
||||
);
|
||||
|
||||
const setThinkingOptionFromUser = useCallback(
|
||||
(thinkingOptionId: string) => {
|
||||
setFormState((prev) => ({ ...prev, thinkingOptionId }));
|
||||
setUserModified((prev) => ({ ...prev, thinkingOptionId: true }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setThinkingOptionFromUser = useCallback((thinkingOptionId: string) => {
|
||||
setFormState((prev) => ({ ...prev, thinkingOptionId }));
|
||||
setUserModified((prev) => ({ ...prev, thinkingOptionId: true }));
|
||||
}, []);
|
||||
|
||||
const setWorkingDir = useCallback((value: string) => {
|
||||
setFormState((prev) => ({ ...prev, workingDir: value }));
|
||||
}, []);
|
||||
|
||||
const setWorkingDirFromUser = useCallback(
|
||||
(value: string) => {
|
||||
setFormState((prev) => ({ ...prev, workingDir: value }));
|
||||
setUserModified((prev) => ({ ...prev, workingDir: true }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const setWorkingDirFromUser = useCallback((value: string) => {
|
||||
setFormState((prev) => ({ ...prev, workingDir: value }));
|
||||
setUserModified((prev) => ({ ...prev, workingDir: true }));
|
||||
}, []);
|
||||
|
||||
const setSelectedServerId = useCallback((value: string | null) => {
|
||||
setFormState((prev) => ({ ...prev, serverId: value }));
|
||||
|
||||
@@ -104,18 +104,8 @@ describe("useArchiveAgent", () => {
|
||||
|
||||
it("applies archived agent close results to session state and cached lists", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
useSessionStore
|
||||
.getState()
|
||||
.initializeSession("server-a", {} as DaemonClient);
|
||||
useSessionStore.getState().setAgents(
|
||||
"server-a",
|
||||
new Map([
|
||||
[
|
||||
"agent-1",
|
||||
makeAgent(),
|
||||
],
|
||||
]),
|
||||
);
|
||||
useSessionStore.getState().initializeSession("server-a", {} as DaemonClient);
|
||||
useSessionStore.getState().setAgents("server-a", new Map([["agent-1", makeAgent()]]));
|
||||
queryClient.setQueryData(["sidebarAgentsList", "server-a"], {
|
||||
entries: [{ agent: { id: "agent-1" } }, { agent: { id: "agent-2" } }],
|
||||
});
|
||||
@@ -130,7 +120,10 @@ describe("useArchiveAgent", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
useSessionStore.getState().sessions["server-a"]?.agents.get("agent-1")?.archivedAt?.toISOString(),
|
||||
useSessionStore
|
||||
.getState()
|
||||
.sessions["server-a"]?.agents.get("agent-1")
|
||||
?.archivedAt?.toISOString(),
|
||||
).toBe("2026-04-01T04:00:00.000Z");
|
||||
expect(queryClient.getQueryData(["sidebarAgentsList", "server-a"])).toEqual({
|
||||
entries: [{ agent: { id: "agent-2" } }],
|
||||
|
||||
@@ -216,9 +216,7 @@ interface ApplyArchivedAgentCloseResultsInput {
|
||||
invalidateQueries?: boolean;
|
||||
}
|
||||
|
||||
export function applyArchivedAgentCloseResults(
|
||||
input: ApplyArchivedAgentCloseResultsInput,
|
||||
): void {
|
||||
export function applyArchivedAgentCloseResults(input: ApplyArchivedAgentCloseResultsInput): void {
|
||||
if (input.results.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ export function useBranchSwitcher({
|
||||
if (!client) return;
|
||||
const shouldStash = await confirmDialog({
|
||||
title: "Uncommitted changes",
|
||||
message:
|
||||
"You have uncommitted changes. Stash them before switching branches?",
|
||||
message: "You have uncommitted changes. Stash them before switching branches?",
|
||||
confirmLabel: "Stash & Switch",
|
||||
cancelLabel: "Cancel",
|
||||
});
|
||||
@@ -135,7 +134,8 @@ export function useBranchSwitcher({
|
||||
if (targetStash) {
|
||||
const shouldRestore = await confirmDialog({
|
||||
title: "Restore stashed changes?",
|
||||
message: "This branch has stashed changes from a previous session. Would you like to restore them?",
|
||||
message:
|
||||
"This branch has stashed changes from a previous session. Would you like to restore them?",
|
||||
confirmLabel: "Restore",
|
||||
cancelLabel: "Later",
|
||||
});
|
||||
@@ -157,7 +157,14 @@ export function useBranchSwitcher({
|
||||
}
|
||||
})();
|
||||
},
|
||||
[client, currentBranchName, invalidateStashAndCheckout, normalizedWorkspaceId, stashAndSwitch, toast],
|
||||
[
|
||||
client,
|
||||
currentBranchName,
|
||||
invalidateStashAndCheckout,
|
||||
normalizedWorkspaceId,
|
||||
stashAndSwitch,
|
||||
toast,
|
||||
],
|
||||
);
|
||||
|
||||
return { branchOptions, isOpen, setIsOpen, handleBranchSelect, invalidateStashAndCheckout };
|
||||
|
||||
@@ -54,11 +54,12 @@ function selectWorkspacePrHint(payload: CheckoutPrStatusPayload): PrHint | null
|
||||
return {
|
||||
url: status.url,
|
||||
number,
|
||||
state: status.isMerged || status.state === "merged"
|
||||
? "merged"
|
||||
: status.state === "open"
|
||||
? "open"
|
||||
: "closed",
|
||||
state:
|
||||
status.isMerged || status.state === "merged"
|
||||
? "merged"
|
||||
: status.state === "open"
|
||||
? "open"
|
||||
: "closed",
|
||||
checks: status.checks,
|
||||
checksStatus: status.checksStatus as PrHint["checksStatus"],
|
||||
reviewDecision: status.reviewDecision as PrHint["reviewDecision"],
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "@server/shared/messages";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
|
||||
export function daemonConfigQueryKey(serverId: string | null) {
|
||||
return ["daemon-config", serverId] as const;
|
||||
}
|
||||
|
||||
interface UseDaemonConfigResult {
|
||||
config: MutableDaemonConfig | null;
|
||||
isLoading: boolean;
|
||||
patchConfig: (patch: MutableDaemonConfigPatch) => Promise<MutableDaemonConfig | undefined>;
|
||||
}
|
||||
|
||||
export function useDaemonConfig(serverId: string | null): UseDaemonConfigResult {
|
||||
const queryClient = useQueryClient();
|
||||
const client = useHostRuntimeClient(serverId ?? "");
|
||||
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
|
||||
const queryKey = useMemo(() => daemonConfigQueryKey(serverId), [serverId]);
|
||||
|
||||
const configQuery = useQuery({
|
||||
queryKey,
|
||||
enabled: Boolean(serverId && client && isConnected),
|
||||
staleTime: Infinity,
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
throw new Error("Host is not connected");
|
||||
}
|
||||
const result = await client.getDaemonConfig();
|
||||
return result.config;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!client || !isConnected || !serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return client.on("status", (message) => {
|
||||
if (message.type !== "status") {
|
||||
return;
|
||||
}
|
||||
if (message.payload.status !== "daemon_config_changed") {
|
||||
return;
|
||||
}
|
||||
queryClient.setQueryData(queryKey, message.payload.config as MutableDaemonConfig);
|
||||
});
|
||||
}, [client, isConnected, queryClient, queryKey, serverId]);
|
||||
|
||||
const patchConfig = useCallback(
|
||||
async (patch: MutableDaemonConfigPatch) => {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
const result = await client.patchDaemonConfig(patch);
|
||||
queryClient.setQueryData(queryKey, result.config);
|
||||
return result.config;
|
||||
},
|
||||
[client, queryClient, queryKey],
|
||||
);
|
||||
|
||||
return {
|
||||
config: configQuery.data ?? null,
|
||||
isLoading: configQuery.isLoading,
|
||||
patchConfig,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type {
|
||||
AgentProvider,
|
||||
AgentSessionConfig,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
import type { AgentProvider, AgentSessionConfig } from "@server/server/agent/agent-sdk-types";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { mergeProviderPreferences, useFormPreferences } from "./use-form-preferences";
|
||||
import {
|
||||
@@ -100,16 +97,16 @@ export function useDraftAgentFeatures(input: {
|
||||
}, [availableFeatures, localFeatureValues]);
|
||||
|
||||
const effectiveFeatureValues = Object.keys(featureValues).length > 0 ? featureValues : undefined;
|
||||
const setFeatureValue = useCallback((featureId: string, value: unknown) => {
|
||||
setLocalFeatureValues((current) => {
|
||||
if (Object.is(current[featureId], value)) {
|
||||
return current;
|
||||
}
|
||||
const setFeatureValue = useCallback(
|
||||
(featureId: string, value: unknown) => {
|
||||
setLocalFeatureValues((current) => {
|
||||
if (Object.is(current[featureId], value)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return { ...current, [featureId]: value };
|
||||
});
|
||||
void updatePreferences(
|
||||
(current) =>
|
||||
return { ...current, [featureId]: value };
|
||||
});
|
||||
void updatePreferences((current) =>
|
||||
mergeProviderPreferences({
|
||||
preferences: current,
|
||||
provider,
|
||||
@@ -119,10 +116,12 @@ export function useDraftAgentFeatures(input: {
|
||||
},
|
||||
},
|
||||
}),
|
||||
).catch((error) => {
|
||||
console.warn("[useDraftAgentFeatures] persist feature preference failed", error);
|
||||
});
|
||||
}, [provider, updatePreferences]);
|
||||
).catch((error) => {
|
||||
console.warn("[useDraftAgentFeatures] persist feature preference failed", error);
|
||||
});
|
||||
},
|
||||
[provider, updatePreferences],
|
||||
);
|
||||
|
||||
return {
|
||||
features,
|
||||
|
||||
@@ -31,12 +31,14 @@ const providerPreferencesSchema = z.object({
|
||||
const formPreferencesSchema = z.object({
|
||||
provider: z.string().optional(),
|
||||
providerPreferences: z.record(providerPreferencesSchema).optional(),
|
||||
favoriteModels: z.array(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
modelId: z.string(),
|
||||
}),
|
||||
).optional(),
|
||||
favoriteModels: z
|
||||
.array(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
modelId: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ProviderPreferences = z.infer<typeof providerPreferencesSchema>;
|
||||
@@ -148,8 +150,7 @@ export function useFormPreferences(): UseFormPreferencesReturn {
|
||||
const prev =
|
||||
queryClient.getQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY) ??
|
||||
DEFAULT_FORM_PREFERENCES;
|
||||
const next =
|
||||
typeof updates === "function" ? updates(prev) : { ...prev, ...updates };
|
||||
const next = typeof updates === "function" ? updates(prev) : { ...prev, ...updates };
|
||||
queryClient.setQueryData<FormPreferences>(FORM_PREFERENCES_QUERY_KEY, next);
|
||||
await AsyncStorage.setItem(FORM_PREFERENCES_STORAGE_KEY, JSON.stringify(next));
|
||||
},
|
||||
|
||||
@@ -27,8 +27,7 @@ export function useKeyboardShortcutOverrides(): UseKeyboardShortcutOverridesRetu
|
||||
|
||||
const setOverride = useCallback(
|
||||
async (bindingId: string, comboString: string) => {
|
||||
const prev =
|
||||
queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||
const prev = queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||
const next = { ...prev, [bindingId]: comboString };
|
||||
queryClient.setQueryData<Record<string, string>>(QUERY_KEY, next);
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||
@@ -38,8 +37,7 @@ export function useKeyboardShortcutOverrides(): UseKeyboardShortcutOverridesRetu
|
||||
|
||||
const removeOverride = useCallback(
|
||||
async (bindingId: string) => {
|
||||
const prev =
|
||||
queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||
const prev = queryClient.getQueryData<Record<string, string>>(QUERY_KEY) ?? EMPTY_OVERRIDES;
|
||||
const { [bindingId]: _, ...next } = prev;
|
||||
queryClient.setQueryData<Record<string, string>>(QUERY_KEY, next);
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||
|
||||
@@ -57,6 +57,7 @@ describe("use-settings", () => {
|
||||
expect(result).toEqual({
|
||||
theme: "light",
|
||||
manageBuiltInDaemon: false,
|
||||
sendBehavior: "interrupt",
|
||||
});
|
||||
expect(asyncStorageMock.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
|
||||
export function useShowShortcutBadges(): boolean {
|
||||
return useKeyboardShortcutsStore((state) => state.showShortcutBadges);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function item(key: string): OrderedItem {
|
||||
}
|
||||
|
||||
function workspace(
|
||||
input: Pick<WorkspaceDescriptor, "id" | "projectId" | "name" | "status" | "activityAt"> &
|
||||
input: Pick<WorkspaceDescriptor, "id" | "projectId" | "name" | "status"> &
|
||||
Partial<
|
||||
Pick<
|
||||
WorkspaceDescriptor,
|
||||
@@ -39,7 +39,6 @@ function workspace(
|
||||
workspaceKind: input.workspaceKind ?? "checkout",
|
||||
name: input.name,
|
||||
status: input.status,
|
||||
activityAt: input.activityAt,
|
||||
diffStat: null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
@@ -130,7 +129,6 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
projectId: "project-1",
|
||||
name: "feat/hard-cut",
|
||||
status: "failed",
|
||||
activityAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -156,7 +154,6 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
projectId: "project-1",
|
||||
name: "main",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
scripts: [runningScript, stoppedScript],
|
||||
}),
|
||||
],
|
||||
@@ -168,21 +165,19 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
expect(projects[0]?.workspaces[0]?.hasRunningScripts).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves stored project order even when activity changes", () => {
|
||||
it("preserves stored project order even when input order differs", () => {
|
||||
const initialWorkspaces: WorkspaceDescriptor[] = [
|
||||
workspace({
|
||||
id: "/repo/b",
|
||||
projectId: "project-b",
|
||||
name: "feat/b",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/a",
|
||||
projectId: "project-a",
|
||||
name: "feat/a",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -199,19 +194,17 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
const updatedProjects = buildSidebarProjectsFromWorkspaces({
|
||||
serverId: "srv",
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "/repo/b",
|
||||
projectId: "project-b",
|
||||
name: "feat/b",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/a",
|
||||
projectId: "project-a",
|
||||
name: "feat/a",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-03T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/b",
|
||||
projectId: "project-b",
|
||||
name: "feat/b",
|
||||
status: "running",
|
||||
}),
|
||||
],
|
||||
projectOrder: seededOrder,
|
||||
@@ -219,8 +212,8 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
});
|
||||
|
||||
expect(updatedProjects.map((project) => project.projectKey)).toEqual([
|
||||
"project-b",
|
||||
"project-a",
|
||||
"project-b",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -233,21 +226,18 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
projectId: "project-c",
|
||||
name: "feat/c",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-04T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/b",
|
||||
projectId: "project-b",
|
||||
name: "feat/b",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/a",
|
||||
projectId: "project-a",
|
||||
name: "feat/a",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
}),
|
||||
],
|
||||
projectOrder: ["project-b", "project-a", "project-c"],
|
||||
@@ -270,14 +260,12 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
projectId: "project-1",
|
||||
name: "main",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/feature",
|
||||
projectId: "project-1",
|
||||
name: "feature",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
}),
|
||||
],
|
||||
projectOrder: ["project-1"],
|
||||
@@ -297,14 +285,12 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
projectId: "project-1",
|
||||
name: "main",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
}),
|
||||
workspace({
|
||||
id: "/repo/feature",
|
||||
projectId: "project-1",
|
||||
name: "feature",
|
||||
status: "running",
|
||||
activityAt: new Date("2026-01-03T00:00:00.000Z"),
|
||||
}),
|
||||
],
|
||||
projectOrder: ["project-1"],
|
||||
@@ -314,8 +300,8 @@ describe("buildSidebarProjectsFromWorkspaces", () => {
|
||||
});
|
||||
|
||||
expect(projects[0]?.workspaces.map((workspace) => workspace.workspaceId)).toEqual([
|
||||
"/repo/main",
|
||||
"/repo/feature",
|
||||
"/repo/main",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
mergeWorkspaceSnapshotWithExisting,
|
||||
normalizeWorkspaceDescriptor,
|
||||
useSessionStore,
|
||||
type WorkspaceDescriptor,
|
||||
} from "@/stores/session-store";
|
||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||
|
||||
const EMPTY_ORDER: string[] = [];
|
||||
@@ -25,7 +25,6 @@ export interface SidebarWorkspaceEntry {
|
||||
projectKind: WorkspaceDescriptor["projectKind"];
|
||||
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
||||
name: string;
|
||||
activityAt: Date | null;
|
||||
statusBucket: SidebarStateBucket;
|
||||
diffStat: { additions: number; deletions: number } | null;
|
||||
scripts: WorkspaceDescriptor["scripts"];
|
||||
@@ -40,7 +39,6 @@ export interface SidebarProjectEntry {
|
||||
statusBucket: SidebarStateBucket;
|
||||
activeCount: number;
|
||||
totalWorkspaces: number;
|
||||
latestActivityAt: Date | null;
|
||||
workspaces: SidebarWorkspaceEntry[];
|
||||
}
|
||||
|
||||
@@ -73,15 +71,6 @@ function compareWorkspaceBaseline(
|
||||
left: SidebarWorkspaceEntry,
|
||||
right: SidebarWorkspaceEntry,
|
||||
): number {
|
||||
if (left.activityAt && right.activityAt) {
|
||||
const dateDelta = right.activityAt.getTime() - left.activityAt.getTime();
|
||||
if (dateDelta !== 0) {
|
||||
return dateDelta;
|
||||
}
|
||||
} else if (left.activityAt || right.activityAt) {
|
||||
return left.activityAt ? -1 : 1;
|
||||
}
|
||||
|
||||
const nameDelta = left.name.localeCompare(right.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
@@ -97,15 +86,6 @@ function compareWorkspaceBaseline(
|
||||
}
|
||||
|
||||
function compareProjectBaseline(left: SidebarProjectEntry, right: SidebarProjectEntry): number {
|
||||
if (left.latestActivityAt && right.latestActivityAt) {
|
||||
const dateDelta = right.latestActivityAt.getTime() - left.latestActivityAt.getTime();
|
||||
if (dateDelta !== 0) {
|
||||
return dateDelta;
|
||||
}
|
||||
} else if (left.latestActivityAt || right.latestActivityAt) {
|
||||
return left.latestActivityAt ? -1 : 1;
|
||||
}
|
||||
|
||||
return left.projectName.localeCompare(right.projectName, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "base",
|
||||
@@ -132,7 +112,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
|
||||
statusBucket: "done",
|
||||
activeCount: 0,
|
||||
totalWorkspaces: 0,
|
||||
latestActivityAt: null,
|
||||
workspaces: [],
|
||||
} satisfies SidebarProjectEntry);
|
||||
|
||||
@@ -145,7 +124,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
|
||||
projectKind: workspace.projectKind,
|
||||
workspaceKind: workspace.workspaceKind,
|
||||
name: workspace.name,
|
||||
activityAt: workspace.activityAt,
|
||||
statusBucket: workspace.status,
|
||||
diffStat: workspace.diffStat,
|
||||
scripts: workspace.scripts,
|
||||
@@ -158,12 +136,6 @@ export function buildSidebarProjectsFromWorkspaces(input: {
|
||||
project.activeCount += 1;
|
||||
}
|
||||
project.statusBucket = aggregateBucket(project.statusBucket, workspace.status);
|
||||
if (
|
||||
!project.latestActivityAt ||
|
||||
(workspace.activityAt && workspace.activityAt.getTime() > project.latestActivityAt.getTime())
|
||||
) {
|
||||
project.latestActivityAt = workspace.activityAt;
|
||||
}
|
||||
|
||||
byProject.set(workspace.projectId, project);
|
||||
}
|
||||
@@ -262,23 +234,9 @@ function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string
|
||||
return `${serverId.trim()}::${projectKey.trim()}`;
|
||||
}
|
||||
|
||||
function toWorkspaceDescriptor(payload: {
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
workspaceDirectory: string;
|
||||
projectKind: WorkspaceDescriptor["projectKind"];
|
||||
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
||||
name: string;
|
||||
status: WorkspaceDescriptor["status"];
|
||||
activityAt: string | null;
|
||||
scripts?: WorkspaceDescriptorPayload["scripts"];
|
||||
}): WorkspaceDescriptor {
|
||||
return normalizeWorkspaceDescriptor({
|
||||
...payload,
|
||||
scripts: payload.scripts ?? [],
|
||||
});
|
||||
function toWorkspaceDescriptor(payload: WorkspaceDescriptorPayload): WorkspaceDescriptor {
|
||||
return normalizeWorkspaceDescriptor(payload);
|
||||
|
||||
}
|
||||
|
||||
export function useSidebarWorkspacesList(options?: {
|
||||
|
||||
Reference in New Issue
Block a user