import { useAtomValue } from "@effect/atom-react"; import { isAtomCommandInterrupted, mapAtomCommandResult, settlePromise, squashAtomCommandFailure, type AtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; import { scopeProjectRef } from "@t3tools/client-runtime/environment"; import { AsyncResult } from "effect/unstable/reactivity"; import { deriveProjectGroupingOverrideKey, selectProjectGroupingSettings, } from "../../logicalProject"; import type { ContextMenuItem, ModelSelection, ProviderDriverKind, SidebarProjectGroupingMode, T3ProjectFileScript, ThreadEnvMode, } from "@t3tools/contracts"; import { resolveEnvModeLabel } from "../BranchToolbar.logic"; import { createModelSelection } from "@t3tools/shared/model"; import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; import { useCanGoBack, useNavigate } from "@tanstack/react-router"; import * as Cause from "effect/Cause"; import { ChevronDownIcon, CopyIcon, PlusIcon, SettingsIcon, Trash2Icon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent, } from "react"; import { useComposerDraftStore } from "../../composerDraftStore"; import { isElectron } from "../../env"; import { useClientSettings, useUpdateClientSettings, usePrimarySettings, } from "../../hooks/useSettings"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { useT3ProjectFileState } from "../../hooks/useT3ProjectFileScripts"; import { shortcutLabelForCommand } from "../../keybindings"; import { keybindingValueForCommand } from "../../lib/projectScriptKeybindings"; import { releaseProjectDraftUploads } from "../../lib/composerDraftUploads"; import { readLocalApi } from "../../localApi"; import { buildProjectScript, commandForProjectScript, nextProjectScriptId, } from "../../projectScripts"; import { decodeProjectScriptKeybindingRule } from "../../lib/projectScriptKeybindings"; import { applyProviderInstanceSettings, deriveProviderInstanceEntries, resolveDefaultProviderModelSelection, sortProviderInstanceEntries, } from "../../providerInstances"; import { getCustomModelOptionsByInstance } from "../../modelSelection"; import { buildSidebarProjectSnapshots, type SidebarProjectGroupMember, type SidebarProjectSnapshot, } from "../../sidebarProjectGrouping"; import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments"; import { useProjects, useThreadShells } from "../../state/entities"; import { projectEnvironment } from "../../state/projects"; import { primaryServerProvidersAtom, serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { ProjectFavicon } from "../ProjectFavicon"; import { EMPTY_PROJECT_SCRIPT_INPUT, editorRequestForScript, ProjectScriptEditorDialog, ScriptIcon, type NewProjectScriptInput, type ProjectScriptEditorRequest, } from "../projectScriptEditor"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Menu, MenuGroup, MenuGroupLabel, MenuItem, MenuPopup, MenuSeparator, MenuTrigger, } from "../ui/menu"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; import { SidebarInset } from "../ui/sidebar"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem, WorkspaceBreadcrumbSeparator, } from "../WorkspaceBreadcrumb"; import { WorkspacePageHeader } from "../WorkspacePageHeader"; import { SettingResetButton, SettingsPageContainer, SettingsRow, SettingsSection, } from "./settingsLayout"; import { canPickExternalProjectFavicon, ProjectFaviconPickerDialog, } from "./ProjectFaviconPickerDialog"; import { projectGroupTitleNeedsUpdate } from "./ProjectSettingsPanel.logic"; export const PROJECT_GROUPING_MODE_LABELS: Record = { repository: "Group by repository", repository_path: "Group by repository path", separate: "Keep separate", }; /** Logical project groups for the settings page, sorted by display name. */ export function useSettingsProjectGroups(): SidebarProjectSnapshot[] { const projects = useProjects(); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const primaryEnvironmentId = usePrimaryEnvironmentId(); const { environments } = useEnvironments(); const environmentLabelById = useMemo( () => new Map( environments.map((environment) => [environment.environmentId, environment.label] as const), ), [environments], ); return useMemo( () => buildSidebarProjectSnapshots({ projects, settings: projectGroupingSettings, primaryEnvironmentId, resolveEnvironmentLabel: (environmentId) => environmentLabelById.get(environmentId) ?? null, }).sort((a, b) => a.displayName.localeCompare(b.displayName)), [environmentLabelById, primaryEnvironmentId, projectGroupingSettings, projects], ); } function memberKey(member: { environmentId: string; id: string }): string { return `${member.environmentId}:${member.id}`; } export function ProjectSettingsPage({ projectKey }: { projectKey: string }) { const navigate = useNavigate(); const canGoBack = useCanGoBack(); const navigateBackWithinApp = useCallback(() => { if (canGoBack) { window.history.back(); return; } void navigate({ to: "/" }); }, [canGoBack, navigate]); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; if (event.key !== "Escape") return; event.preventDefault(); const activeElement = document.activeElement; if (activeElement instanceof HTMLElement) { activeElement.blur(); } navigateBackWithinApp(); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [navigateBackWithinApp]); return (
); } function ProjectSettingsBreadcrumb({ projectKey }: { projectKey: string }) { const groups = useSettingsProjectGroups(); const navigate = useNavigate(); const selected = groups.find((group) => group.projectKey === projectKey) ?? null; const openProjectMenu = (event: ReactMouseEvent) => { const api = readLocalApi(); if (!api) return; const rect = event.currentTarget.getBoundingClientRect(); const items: ContextMenuItem[] = groups.map((group) => ({ id: group.projectKey, label: group.displayName, })); void settlePromise(() => api.contextMenu.show(items, { x: rect.left, y: rect.bottom + 4 }), ).then((clicked) => { if (clicked._tag === "Failure" || clicked.value === null) return; void navigate({ to: "/projects/$projectKey", params: { projectKey: clicked.value }, replace: true, hashScrollIntoView: false, }); }); }; return ( Projects {selected ? ( ) : ( Unavailable project )} ); } export function ProjectSettingsPanel({ projectKey }: { projectKey: string }) { const groups = useSettingsProjectGroups(); const navigate = useNavigate(); const selected = groups.find((group) => group.projectKey === projectKey) ?? null; // Remember the members of the last rendered group so a grouping-rule change // (which changes the group key) can follow the project to its new group. const lastSelectionRef = useRef<{ key: string; memberKeys: string[] } | null>(null); useEffect(() => { if (!selected) return; lastSelectionRef.current = { key: selected.projectKey, memberKeys: selected.memberProjects.map((member) => member.physicalProjectKey), }; }, [selected]); // A grouping-rule change replaces the group key mid-visit; follow the // project to its new key instead of parking on the not-found state. useEffect(() => { if (selected !== null) return; const last = lastSelectionRef.current; if (last?.key !== projectKey) return; const successor = groups.find((group) => group.memberProjects.some((member) => last.memberKeys.includes(member.physicalProjectKey)), ); if (successor) { void navigate({ to: "/projects/$projectKey", params: { projectKey: successor.projectKey }, replace: true, hashScrollIntoView: false, }); } }, [groups, navigate, projectKey, selected]); if (!selected) { return (
{groups.length === 0 ? "Add a project from the sidebar to configure it here." : "This project is no longer available."}
); } return ; } function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) { const navigate = useNavigate(); const primaryEnvironmentId = usePrimaryEnvironmentId(); const settings = usePrimarySettings(); const updateClientSettings = useUpdateClientSettings(); const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const serverProviders = useAtomValue(primaryServerProvidersAtom); const threads = useThreadShells(); const updateProject = useAtomCommand(projectEnvironment.update, { reportFailure: false }); const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false }); const upsertKeybinding = useAtomCommand(serverEnvironment.upsertKeybinding, { reportFailure: false, }); const removeKeybinding = useAtomCommand(serverEnvironment.removeKeybinding, { reportFailure: false, }); const projectNameEditedRef = useRef(false); const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({ onCopy: ({ path }) => { toastManager.add({ type: "success", title: "Path copied", description: path }); }, onError: (error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Failed to copy path", description: error instanceof Error ? error.message : "An error occurred.", }), ); }, }); const representative = group.memberProjects.find( (member) => member.environmentId === group.environmentId && member.id === group.id, ) ?? group.memberProjects[0]!; const faviconPath = representative.faviconPath ?? null; const pickProjectFavicon = typeof window !== "undefined" && group.memberProjects.every( (member) => member.environmentId === primaryEnvironmentId && canPickExternalProjectFavicon(member.workspaceRoot, navigator.platform), ) ? window.desktopBridge?.pickProjectFavicon : undefined; const threadCountByMember = useMemo(() => { const counts = new Map(); for (const thread of threads) { const key = `${thread.environmentId}:${thread.projectId}`; counts.set(key, (counts.get(key) ?? 0) + 1); } return counts; }, [threads]); const reportFailure = useCallback((title: string, result: AtomCommandResult) => { if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; const error = squashAtomCommandFailure(result); toastManager.add( stackedThreadToast({ type: "error", title, description: error instanceof Error ? error.message : "An error occurred.", }), ); }, []); // Group-shared fields live on each physical project record, so a // group-level edit fans out to every member. const updateAllMembers = useCallback( async ( input: Partial<{ title: string; defaultModelSelection: ModelSelection | null; defaultThreadEnvMode: ThreadEnvMode | null; faviconPath: string | null; }>, failureTitle: string, ): Promise> => { for (const member of group.memberProjects) { const result = mapAtomCommandResult( await updateProject({ environmentId: member.environmentId, input: { projectId: member.id, ...input }, }), () => undefined, ); if (result._tag === "Failure") { // A partial fan-out is possible: earlier members already took the // write. Name the environment so the user knows where it stopped. reportFailure( group.memberProjects.length > 1 ? `${failureTitle} on ${member.environmentLabel ?? "the current environment"}` : failureTitle, result, ); return result; } } return AsyncResult.success(undefined); }, [group.memberProjects, reportFailure, updateProject], ); const renameGroup = useCallback( async (nextTitle: string, wasEdited: boolean) => { const title = nextTitle.trim(); if (!title) { toastManager.add({ type: "warning", title: "Project title cannot be empty" }); return; } if ( !projectGroupTitleNeedsUpdate( group.memberProjects.map((member) => member.title), title, wasEdited, ) ) { return; } await updateAllMembers({ title }, "Failed to rename project"); }, [group.memberProjects, updateAllMembers], ); // ----- default model ----- const storedSelection = representative.defaultModelSelection; const resolvedSelection = resolveDefaultProviderModelSelection(serverProviders, storedSelection); const instanceEntries = useMemo( () => sortProviderInstanceEntries( applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings), ), [serverProviders, settings], ); const modelOptionsByInstance = useMemo( () => getCustomModelOptionsByInstance(settings, serverProviders), [serverProviders, settings], ); const activeEntry = instanceEntries.find( (entry) => entry.instanceId === resolvedSelection?.instanceId, ); const setDefaultModel = useCallback( (selection: ModelSelection | null) => void updateAllMembers({ defaultModelSelection: selection }, "Failed to update default model"), [updateAllMembers], ); // ----- new-thread workspace mode ----- const storedEnvMode = representative.defaultThreadEnvMode ?? null; const setDefaultThreadEnvMode = useCallback( (mode: ThreadEnvMode | null) => void updateAllMembers( { defaultThreadEnvMode: mode }, "Failed to update new-thread workspace", ), [updateAllMembers], ); // ----- favicon ----- const [faviconPickerOpen, setFaviconPickerOpen] = useState(false); const [isSavingFavicon, setIsSavingFavicon] = useState(false); const savingFaviconRef = useRef(false); const setFaviconPath = useCallback( async (faviconPath: string | null) => { if (savingFaviconRef.current) return; savingFaviconRef.current = true; setIsSavingFavicon(true); try { await updateAllMembers({ faviconPath }, "Failed to update project icon"); } finally { savingFaviconRef.current = false; setIsSavingFavicon(false); } }, [updateAllMembers], ); // ----- checkout selection and scripts ----- const [selectedCheckoutKey, setSelectedCheckoutKey] = useState(representative.physicalProjectKey); const selectedCheckout = group.memberProjects.find((member) => member.physicalProjectKey === selectedCheckoutKey) ?? representative; const selectedServerConfig = useAtomValue( serverEnvironment.configValueAtom(selectedCheckout.environmentId), ); const keybindings = selectedServerConfig?.keybindings ?? DEFAULT_RESOLVED_KEYBINDINGS; const scripts = selectedCheckout.scripts; const [editorRequest, setEditorRequest] = useState(null); // Script writes replace the whole array, so two overlapping writes computed // from the same snapshot would drop each other's changes. One at a time. const [isSavingScripts, setIsSavingScripts] = useState(false); const savingScriptsRef = useRef(false); const t3File = useT3ProjectFileState( selectedCheckout.environmentId, selectedCheckout.workspaceRoot, ); // What the "Default" option resolves to while no override is set: the // repo's t3.json value when present, otherwise the global setting. const inheritedEnvMode = t3File.file?.defaultThreadEnvMode ?? settings.defaultThreadEnvMode; const inheritedEnvModeSource = t3File.file?.defaultThreadEnvMode != null ? "t3.json" : "global"; const importableScripts = useMemo( () => t3File.scripts.filter( (fileScript) => !scripts.some( (script) => script.command === fileScript.command || script.name.toLowerCase() === fileScript.name.toLowerCase(), ), ), [scripts, t3File.scripts], ); const persistScripts = useCallback( async ( nextScripts: ReadonlyArray>, keybinding: string | null | undefined, keybindingCommand: ReturnType, ): Promise> => { if (savingScriptsRef.current) { return AsyncResult.failure( Cause.fail(new Error("Another script change is still saving. Try again.")), ); } savingScriptsRef.current = true; setIsSavingScripts(true); try { // Captured before the write so a cleared or deleted binding can be // removed from the keybindings config afterwards. const previousKeybinding = keybindingValueForCommand(keybindings, keybindingCommand); const updateResult = mapAtomCommandResult( await updateProject({ environmentId: selectedCheckout.environmentId, input: { projectId: selectedCheckout.id, scripts: nextScripts }, }), () => undefined, ); if (updateResult._tag === "Failure") { reportFailure("Failed to save scripts", updateResult); return updateResult; } const keybindingRule = decodeProjectScriptKeybindingRule({ keybinding, command: keybindingCommand, }); if (!isElectron) return updateResult; const environmentIds = [selectedCheckout.environmentId]; const previousTarget = previousKeybinding ? decodeProjectScriptKeybindingRule({ keybinding: previousKeybinding, command: keybindingCommand, }) : null; if (keybindingRule) { // `replace` swaps the command's previous rule instead of appending a // second one that would keep the old shortcut alive. const input = previousTarget && previousTarget.key !== keybindingRule.key ? { ...keybindingRule, replace: previousTarget } : keybindingRule; for (const environmentId of environmentIds) { const result = mapAtomCommandResult( await upsertKeybinding({ environmentId, input }), () => undefined, ); if (result._tag === "Failure") { reportFailure("Failed to save keybinding", result); return result; } } } else if (previousTarget) { for (const environmentId of environmentIds) { const result = mapAtomCommandResult( await removeKeybinding({ environmentId, input: previousTarget }), () => undefined, ); if (result._tag === "Failure") { reportFailure("Failed to remove keybinding", result); return result; } } } return updateResult; } finally { savingScriptsRef.current = false; setIsSavingScripts(false); } }, [ keybindings, removeKeybinding, reportFailure, selectedCheckout.environmentId, selectedCheckout.id, updateProject, upsertKeybinding, ], ); const submitScript = useCallback( async ( scriptId: string | null, input: NewProjectScriptInput, ): Promise> => { if (scriptId === null) { const nextId = nextProjectScriptId( input.name, scripts.map((script) => script.id), ); const nextScript = buildProjectScript(nextId, input); const nextScripts = input.runOnWorktreeCreate ? [ ...scripts.map((script) => script.runOnWorktreeCreate ? { ...script, runOnWorktreeCreate: false } : script, ), nextScript, ] : [...scripts, nextScript]; return persistScripts(nextScripts, input.keybinding, commandForProjectScript(nextId)); } const updatedScript = buildProjectScript(scriptId, input); const nextScripts = scripts.map((script) => script.id === scriptId ? updatedScript : input.runOnWorktreeCreate ? { ...script, runOnWorktreeCreate: false } : script, ); return persistScripts(nextScripts, input.keybinding, commandForProjectScript(scriptId)); }, [persistScripts, scripts], ); const deleteScript = useCallback( (scriptId: string) => { const nextScripts = scripts.filter((script) => script.id !== scriptId); void persistScripts(nextScripts, null, commandForProjectScript(scriptId)); }, [persistScripts, scripts], ); const importFileScript = useCallback( async (fileScript: T3ProjectFileScript) => { const payload: NewProjectScriptInput = { name: fileScript.name, command: fileScript.command, icon: fileScript.icon ?? "play", runOnWorktreeCreate: fileScript.runOnWorktreeCreate ?? false, keybinding: null, previewUrl: fileScript.previewUrl ?? null, autoOpenPreview: fileScript.previewUrl ? (fileScript.autoOpenPreview ?? false) : false, }; const result = await submitScript(null, payload); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); setEditorRequest({ scriptId: null, initial: payload, error: error instanceof Error ? error.message : "Failed to import action.", }); } }, [submitScript], ); // ----- checkouts ----- const updateGroupingPreference = useCallback( (member: SidebarProjectGroupMember, selection: SidebarProjectGroupingMode | "inherit") => { const overrideKey = deriveProjectGroupingOverrideKey(member); const nextOverrides = { ...projectGroupingSettings.sidebarProjectGroupingOverrides }; if (selection === "inherit") { delete nextOverrides[overrideKey]; } else { nextOverrides[overrideKey] = selection; } updateClientSettings({ sidebarProjectGroupingOverrides: nextOverrides }); }, [projectGroupingSettings.sidebarProjectGroupingOverrides, updateClientSettings], ); const removeMembers = useCallback( async (members: ReadonlyArray) => { const api = readLocalApi(); if (!api) return; const memberKeys = new Set(members.map(memberKey)); const projectThreads = threads.filter((thread) => memberKeys.has(`${thread.environmentId}:${thread.projectId}`), ); const isWholeGroup = members.length === group.memberProjects.length; const singleMember = members.length === 1 ? members[0]! : null; const targetLabel = singleMember?.title ?? group.displayName; const confirmed = await settlePromise(() => api.dialogs.confirm( [ projectThreads.length > 0 ? `Remove project "${targetLabel}" and delete its ${projectThreads.length} thread${projectThreads.length === 1 ? "" : "s"}?` : `Remove project "${targetLabel}"?`, ...(singleMember ? [ `Path: ${singleMember.workspaceRoot}`, ...(singleMember.environmentLabel ? [`Environment: ${singleMember.environmentLabel}`] : []), ] : [`This removes ${members.length} grouped project entries.`]), ...(projectThreads.length > 0 ? ["This permanently clears conversation history for those threads."] : []), isWholeGroup ? "This removes only the project entries, not the files on disk." : "Other entries in this grouped project are unaffected.", "This action cannot be undone.", ].join("\n"), { variant: "destructive" }, ), ); if (confirmed._tag === "Failure" || !confirmed.value) return; const draftStore = useComposerDraftStore.getState(); for (const member of members) { const memberThreads = projectThreads.filter( (thread) => thread.environmentId === member.environmentId && thread.projectId === member.id, ); const result = mapAtomCommandResult( await deleteProject({ environmentId: member.environmentId, input: { projectId: member.id, ...(memberThreads.length > 0 ? { force: true } : {}), }, }), () => undefined, ); if (result._tag === "Failure") { reportFailure(`Failed to remove "${member.title}"`, result); return; } const projectRef = scopeProjectRef(member.environmentId, member.id); releaseProjectDraftUploads(projectRef); const projectDraftThread = draftStore.getDraftThreadByProjectRef(projectRef); if (projectDraftThread) { draftStore.clearDraftThread(projectDraftThread.draftId); } draftStore.clearProjectDraftThreadId(projectRef); } // The project's settings page just deleted itself; there is no projects // listing to fall back to, so leave settings entirely. if (isWholeGroup) { void navigate({ to: "/", replace: true }); } }, [ deleteProject, group.displayName, group.memberProjects.length, navigate, reportFailure, threads, ], ); const selectedCheckoutThreadCount = threadCountByMember.get(memberKey(selectedCheckout)) ?? 0; const selectedCheckoutGrouping = projectGroupingSettings.sidebarProjectGroupingOverrides?.[ deriveProjectGroupingOverrideKey(selectedCheckout) ] ?? "inherit"; const selectedCheckoutLabel = selectedCheckout.environmentLabel ?? "This machine"; return ( <> { projectNameEditedRef.current = true; }} onBlur={(event) => { const wasEdited = projectNameEditedRef.current; projectNameEditedRef.current = false; void renameGroup(event.currentTarget.value, wasEdited); }} onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur(); }} /> } /> void setFaviconPath(null)} /> ) : null } control={
} />
setDefaultModel(null)} /> ) : null } control={ resolvedSelection && activeEntry ? (
{ setDefaultModel(createModelSelection(instanceId, model)); }} /> {}} modelOptions={resolvedSelection.options ?? []} allowPromptInjectedEffort={false} planModeEnabled={settings.planModeEnabled} triggerVariant="outline" triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground" onModelOptionsChange={(nextOptions) => { setDefaultModel( createModelSelection( resolvedSelection.instanceId, resolvedSelection.model, nextOptions, ), ); }} />
) : ( No providers available ) } /> setDefaultThreadEnvMode(null)} /> ) : null } control={ } />
setSelectedCheckoutKey(String(value))} > {selectedCheckoutLabel} {group.memberProjects.map((member) => ( {member.environmentLabel ?? "This machine"} · {member.workspaceRoot} ))} } >
copyPathToClipboard(selectedCheckout.workspaceRoot, { path: selectedCheckout.workspaceRoot, }) } > {selectedCheckout.workspaceRoot} } /> Copy path
{selectedCheckoutThreadCount === 1 ? "1 thread" : `${selectedCheckoutThreadCount} threads`}
{ if ( value === "inherit" || value === "repository" || value === "repository_path" || value === "separate" ) { updateGroupingPreference(selectedCheckout, value); } }} > {selectedCheckoutGrouping === "inherit" ? `Default (${PROJECT_GROUPING_MODE_LABELS[projectGroupingSettings.sidebarProjectGroupingMode]})` : PROJECT_GROUPING_MODE_LABELS[selectedCheckoutGrouping]} Use global default {PROJECT_GROUPING_MODE_LABELS.repository} {PROJECT_GROUPING_MODE_LABELS.repository_path} {PROJECT_GROUPING_MODE_LABELS.separate} } /> {group.memberProjects.length > 1 ? ( void removeMembers([selectedCheckout])} > Remove checkout } /> ) : null}

Actions

Saved and run only in {selectedCheckoutLabel}.

{importableScripts.length > 0 ? ( } > Import scripts Import from t3.json

Add actions declared by this checkout without editing them first.

{importableScripts.map((fileScript) => ( void importFileScript(fileScript)} >
{fileScript.name}
{fileScript.command}
))}
) : null}
{scripts.length === 0 ? (

No actions configured for this checkout.

) : ( scripts.map((script) => { const shortcutLabel = shortcutLabelForCommand( keybindings, commandForProjectScript(script.id), ); return ( {script.name} {script.command} {script.runOnWorktreeCreate ? ( setup ) : null} {script.previewUrl ? ( preview · desktop only ) : null} } control={ <> {shortcutLabel ? ( {shortcutLabel} ) : null} } /> ); }) )} {t3File.status === "invalid" ? ( ) : null}
1 ? "Remove this project everywhere" : "Remove project" } description={ group.memberProjects.length > 1 ? `Deletes all ${group.memberProjects.length} checkout entries and their threads on every machine. Files on disk are not touched.` : "Deletes the project entry and its threads. Files on disk are not touched." } control={ } />
setEditorRequest(null)} /> pickProjectFavicon(representative.workspaceRoot) } : {})} onSelect={(path) => void setFaviconPath(path)} open={faviconPickerOpen} projectName={group.displayName} /> ); }