diff --git a/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md b/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md new file mode 100644 index 00000000000..bf40a83d511 --- /dev/null +++ b/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md @@ -0,0 +1,111 @@ +# Optimistic Electric Updates + +Desktop uses TanStack DB collections backed by Electric shapes for task and workspace data. The default write model is **optimistic online**, not offline-first. + +## Decision + +Use TanStack DB collection mutations for routine server-backed writes that already have stable local identity: + +1. The UI calls `collection.insert`, `collection.update`, or `collection.delete`. +2. TanStack DB applies optimistic state immediately. +3. The collection handler persists through our API. +4. The API returns the PostgreSQL `txid` from the same database transaction as the write. +5. Electric streams that transaction back to the client. +6. TanStack DB drops the optimistic overlay or rolls it back if persistence fails. + +This matches the documented TanStack DB mutation lifecycle and Electric collection txid strategy: + +- TanStack DB mutations: https://tanstack.com/db/latest/docs/guides/mutations +- Electric collection txid matching: https://tanstack.com/db/latest/docs/collections/electric-collection + +## Scope + +Optimistic online is the right default for task edits, status changes, assignment changes, priority changes, title/description edits, and soft deletes. These are simple, single-record writes where immediate feedback matters and rollback is acceptable if the server rejects the write. + +New task creation can remain server-confirmed while it relies on server-generated slugs, default status seeding, and navigation to the canonical record. Move creation to optimistic insert only after the client can provide all stable identity and routing fields up front. + +Do not treat this as offline-first. If the API call cannot run, the transaction should fail, TanStack DB should roll back the optimistic state, and the UI should show a failure toast. We are not adding a durable outbox, replay queue, conflict resolver, or persisted collection state in this pass. + +## Desktop Collection Matrix + +Desktop currently has three write categories. + +### Server-backed Electric writes + +These collections have Electric mutation handlers in `CollectionsProvider/collections.ts`: + +| Collection | Handlers | Current write surface | Behavior | +| --- | --- | --- | --- | +| `tasks` | insert, update, delete | `useOptimisticCollectionActions().tasks` for update/delete; create dialog still uses `task.createFromUi` directly | Optimistic for task edits/deletes; collection handlers return `{ txid }`. | +| `v2Projects` | update | `useOptimisticCollectionActions().v2Projects` for rename/repository updates | Optimistic for project row edits; create/delete remain API-confirmed. | +| `v2Workspaces` | update | `useOptimisticCollectionActions().v2Workspaces` for rename-style updates | Optimistic for workspace row edits; create/delete remain host-service sagas. | +| `chatSessions` | delete | `useOptimisticCollectionActions().chatSessions` for chat session deletion | Optimistic delete; create remains server-confirmed because the chat runtime coordinates session creation. | +| `agentCommands` | update | `useCommandWatcher` | Background optimistic update; caller awaits `tx.isPersisted.promise` and retries on failure. | + +### Read-only Electric collections + +These are Electric-backed in the renderer but have no collection mutation handlers and no direct renderer `collection.insert/update/delete` calls: + +- `organizations` +- `taskStatuses` +- `projects` +- `v2Hosts` +- `v2Clients` +- `v2UsersHosts` +- `workspaces` +- `members` +- `users` +- `invitations` +- `integrationConnections` +- `subscriptions` +- `apiKeys` +- `sessionHosts` +- `githubRepositories` +- `githubPullRequests` +- `automations` +- `automationRuns` + +Workspace create/delete flows do not use `collections.v2Workspaces.insert/delete`. They go through host-service or tRPC APIs and then Electric streams the confirmed row back: + +- workspace create/checkout/adopt writes a local `pendingWorkspaces` row, then the pending page calls host-service +- workspace delete calls host-service `workspaceCleanup.destroy`; the sidebar hides the row through `DeletingWorkspacesProvider` while the saga runs + +Workspace rename does use `collections.v2Workspaces.update` via `useOptimisticCollectionActions().v2Workspaces`, backed by `v2Workspace.update` returning `{ txid }` from the same Postgres transaction. + +### LocalStorage collections + +These are client-local TanStack DB collections. They are synchronous local persistence, not Electric/Postgres optimistic writes: + +- `v2SidebarProjects` — sidebar project order/collapse/default app +- `v2WorkspaceLocalState` — sidebar placement, pane layout, viewed files, changes tab +- `v2SidebarSections` — user-created sidebar sections and ordering +- `v2TerminalPresets` — local terminal presets +- `pendingWorkspaces` — durable local bus for workspace creation progress and launch handoff +- `v2UserPreferences` — local v2 preferences such as link behavior and delete-branch default + +LocalStorage mutations can still throw for schema/storage errors, but they do not have remote persistence confirmation or Electric rollback semantics. + +## Implementation Contract + +Collection handlers must return `{ txid }` for server-backed Electric writes. The txid must come from `pg_current_xact_id()` inside the same transaction that performs the mutation. A txid captured before or after the write can leave `tx.isPersisted.promise` waiting for a transaction that Electric will never stream. + +Feature code should not scatter direct server-backed collection mutations. Use `useOptimisticCollectionActions` and the relevant grouped action surface, such as `.tasks`, `.v2Workspaces`, `.v2Projects`, or `.chatSessions`, so every call site gets the same behavior: + +- apply the optimistic collection mutation immediately +- attach a rejection handler to `tx.isPersisted.promise` +- show a user-visible error when persistence fails +- let TanStack DB own rollback + +Use `{ optimistic: false }` only for exceptional flows where the UI must wait for server confirmation before revealing the result, such as a workflow that depends on a server-generated identifier or a multi-step server-side effect. + +## Offline-First Boundary + +Offline-first needs more than optimistic state. It needs durable local persistence, queued transactions, replay ordering, idempotency, and conflict handling. If we decide to support offline task writes later, design it as a separate feature with: + +- a durable transaction queue +- client-generated stable IDs for created records +- idempotent API mutations +- explicit conflict policy per write type +- UI for pending and failed replays + +Until then, Electric is our read/sync confirmation path and the API remains the write authority. diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts b/apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts index 30740ace5eb..b08487ef356 100644 --- a/apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts +++ b/apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts @@ -19,6 +19,7 @@ const STORAGE_KEY_PREFIX = "terminal-buffer:"; const DIMS_KEY_PREFIX = "terminal-dims:"; const DEFAULT_COLS = 120; const DEFAULT_ROWS = 32; +const RESIZE_DEBOUNCE_MS = 75; // xterm's _keyDown calls stopPropagation after processing, so any chord we // want the host (react-hotkeys-hook, Electron menu accelerators) or the shell @@ -85,6 +86,7 @@ export interface TerminalRuntime { wrapper: HTMLDivElement; container: HTMLDivElement | null; resizeObserver: ResizeObserver | null; + _disposeResizeObserver: (() => void) | null; lastCols: number; lastRows: number; _disposeAddons: (() => void) | null; @@ -211,11 +213,70 @@ function getParkingContainer(): HTMLDivElement { return el; } -function measureAndResize(runtime: TerminalRuntime) { - if (!hostIsVisible(runtime.container)) return; +function measureAndResize(runtime: TerminalRuntime): boolean { + if (!hostIsVisible(runtime.container)) return false; + const { terminal } = runtime; + const buffer = terminal.buffer.active; + const wasPinnedToBottom = buffer.viewportY >= buffer.baseY; + const savedViewportY = buffer.viewportY; + const prevCols = terminal.cols; + const prevRows = terminal.rows; + runtime.fitAddon.fit(); - runtime.lastCols = runtime.terminal.cols; - runtime.lastRows = runtime.terminal.rows; + runtime.lastCols = terminal.cols; + runtime.lastRows = terminal.rows; + + if (wasPinnedToBottom) { + terminal.scrollToBottom(); + } else { + const targetY = Math.min(savedViewportY, terminal.buffer.active.baseY); + if (terminal.buffer.active.viewportY !== targetY) { + terminal.scrollToLine(targetY); + } + } + + terminal.refresh(0, Math.max(0, terminal.rows - 1)); + + return terminal.cols !== prevCols || terminal.rows !== prevRows; +} + +function createResizeScheduler( + runtime: TerminalRuntime, + onResize?: () => void, +): { + observe: ResizeObserverCallback; + dispose: () => void; +} { + let timeoutId: ReturnType | null = null; + + const dispose = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + const run = () => { + timeoutId = null; + const changed = measureAndResize(runtime); + if (changed) onResize?.(); + }; + + const observe: ResizeObserverCallback = (entries) => { + if ( + entries.some( + (entry) => + entry.contentRect.width <= 0 || entry.contentRect.height <= 0, + ) + ) { + dispose(); + return; + } + dispose(); + timeoutId = setTimeout(run, RESIZE_DEBOUNCE_MS); + }; + + return { observe, dispose }; } export function createRuntime( @@ -259,6 +320,7 @@ export function createRuntime( wrapper, container: null, resizeObserver: null, + _disposeResizeObserver: null, lastCols: cols, lastRows: rows, _disposeAddons: addonsResult.dispose, @@ -287,9 +349,10 @@ export function attachToContainer( containerWidth: container.clientWidth, containerHeight: container.clientHeight, }); - measureAndResize(runtime); + if (measureAndResize(runtime)) onResize?.(); // Renderer may have skipped frames while the wrapper was detached. + // (refresh is now handled inside measureAndResize) terminalRendererDebug.info( "runtime-refresh", { @@ -301,15 +364,15 @@ export function attachToContainer( fingerprint: ["terminal.renderer", "runtime-refresh"], }, ); - runtime.terminal.refresh(0, runtime.terminal.rows - 1); + runtime._disposeResizeObserver?.(); + runtime._disposeResizeObserver = null; runtime.resizeObserver?.disconnect(); - const observer = new ResizeObserver(() => { - measureAndResize(runtime); - onResize?.(); - }); + const scheduler = createResizeScheduler(runtime, onResize); + const observer = new ResizeObserver(scheduler.observe); observer.observe(container); runtime.resizeObserver = observer; + runtime._disposeResizeObserver = scheduler.dispose; runtime.terminal.focus(); } @@ -329,6 +392,8 @@ export function detachFromContainer(runtime: TerminalRuntime) { fingerprint: ["terminal.renderer", "runtime-detach-from-container"], }, ); + runtime._disposeResizeObserver?.(); + runtime._disposeResizeObserver = null; runtime.resizeObserver?.disconnect(); runtime.resizeObserver = null; // Park instead of .remove() so xterm survives the React unmount — @@ -341,7 +406,7 @@ export function updateRuntimeAppearance( runtime: TerminalRuntime, appearance: TerminalAppearance, ) { - const { terminal, fitAddon } = runtime; + const { terminal } = runtime; terminal.options.theme = appearance.theme; const fontChanged = @@ -352,9 +417,7 @@ export function updateRuntimeAppearance( terminal.options.fontFamily = appearance.fontFamily; terminal.options.fontSize = appearance.fontSize; if (hostIsVisible(runtime.container)) { - fitAddon.fit(); - runtime.lastCols = terminal.cols; - runtime.lastRows = terminal.rows; + measureAndResize(runtime); } } } @@ -370,6 +433,8 @@ export function disposeRuntime( } runtime._disposeAddons?.(); runtime._disposeAddons = null; + runtime._disposeResizeObserver?.(); + runtime._disposeResizeObserver = null; runtime.resizeObserver?.disconnect(); runtime.resizeObserver = null; runtime.wrapper.remove(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts index 0fafd1a535c..bedc2d31c6d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts @@ -2,8 +2,8 @@ import { alert } from "@superset/ui/atoms/Alert"; import { toast } from "@superset/ui/sonner"; import { useNavigate } from "@tanstack/react-router"; import { useState } from "react"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; import type { DashboardSidebarProject } from "../../../../types"; @@ -16,6 +16,7 @@ export function useDashboardSidebarProjectSectionActions({ }: UseDashboardSidebarProjectSectionActionsOptions) { const openModal = useOpenNewWorkspaceModal(); const navigate = useNavigate(); + const { v2Projects: projectActions } = useOptimisticCollectionActions(); const { createSection, deleteSection, @@ -37,20 +38,11 @@ export function useDashboardSidebarProjectSectionActions({ setRenameValue(project.name); }; - const submitRename = async () => { + const submitRename = () => { setIsRenaming(false); const trimmed = renameValue.trim(); if (!trimmed || trimmed === project.name) return; - try { - await apiTrpcClient.v2Project.update.mutate({ - id: project.id, - name: trimmed, - }); - } catch (error) { - toast.error( - `Failed to rename: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + projectActions.renameProject(project.id, trimmed); }; const handleOpenInFinder = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx index 5f21eac6ef6..912b877884b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx @@ -35,8 +35,8 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< ref={ref} className={cn( "relative flex items-center justify-center size-8 rounded-md", - "hover:bg-muted/50 transition-colors cursor-pointer", - isActive && "bg-muted", + "transition-colors cursor-pointer", + isActive ? "bg-muted hover:bg-muted" : "hover:bg-muted/50", className, )} {...props} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 77496cadfc0..9f06b89d5f2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -120,7 +120,10 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< onDoubleClick={onDoubleClick} className={cn( "relative flex w-full items-center pl-3 pr-2 text-left text-sm", - onClick && "cursor-pointer hover:bg-muted/50", + onClick && + (isActive + ? "cursor-pointer hover:bg-muted" + : "cursor-pointer hover:bg-muted/50"), "group", "py-1.5", isActive && "bg-muted", diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 3eb9f4f059f..f761c648ed0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -2,13 +2,13 @@ import { toast } from "@superset/ui/sonner"; import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { electronTrpcClient } from "renderer/lib/trpc-client"; import { getDeleteFocusTargetWorkspaceId } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId"; import { getFlattenedV2WorkspaceIds } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getFlattenedV2WorkspaceIds"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; @@ -30,6 +30,7 @@ export function useDashboardSidebarWorkspaceItemActions({ const collections = useCollections(); const { activeHostUrl } = useLocalHostService(); const { copyToClipboard } = useCopyToClipboard(); + const { v2Workspaces: workspaceActions } = useOptimisticCollectionActions(); const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = useDashboardSidebarState(); @@ -61,20 +62,11 @@ export function useDashboardSidebarWorkspaceItemActions({ setRenameValue(workspaceName); }; - const submitRename = async () => { + const submitRename = () => { setIsRenaming(false); const trimmed = renameValue.trim(); if (!trimmed || trimmed === workspaceName) return; - try { - await apiTrpcClient.v2Workspace.update.mutate({ - id: workspaceId, - name: trimmed, - }); - } catch (error) { - toast.error( - `Failed to rename: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } + workspaceActions.renameWorkspace(workspaceId, trimmed); }; /** diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/AssigneeProperty/AssigneeProperty.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/AssigneeProperty/AssigneeProperty.tsx index e9c5ac0d6b5..90706ba0dc5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/AssigneeProperty/AssigneeProperty.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/AssigneeProperty/AssigneeProperty.tsx @@ -8,6 +8,7 @@ import { import { useLiveQuery } from "@tanstack/react-db"; import { useMemo, useState } from "react"; import { HiOutlineUserCircle } from "react-icons/hi2"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { TaskWithStatus } from "../../../../../components/TasksView/hooks/useTasksTable"; @@ -17,6 +18,7 @@ interface AssigneePropertyProps { export function AssigneeProperty({ task }: AssigneePropertyProps) { const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const { data: allUsers } = useLiveQuery( @@ -32,14 +34,10 @@ export function AssigneeProperty({ task }: AssigneePropertyProps) { return; } - setOpen(false); - - collections.tasks.update(task.id, (draft) => { - draft.assigneeId = userId; - draft.assigneeExternalId = null; - draft.assigneeDisplayName = null; - draft.assigneeAvatarUrl = null; - }); + const transaction = taskActions.updateAssignee(task.id, userId); + if (transaction) { + setOpen(false); + } }; return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/PriorityProperty/PriorityProperty.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/PriorityProperty/PriorityProperty.tsx index 645eb240a54..3f3de904ff4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/PriorityProperty/PriorityProperty.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/PriorityProperty/PriorityProperty.tsx @@ -6,7 +6,7 @@ import { DropdownMenuTrigger, } from "@superset/ui/dropdown-menu"; import { useState } from "react"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { PriorityIcon } from "../../../../../components/TasksView/components/shared/PriorityIcon"; import type { TaskWithStatus } from "../../../../../components/TasksView/hooks/useTasksTable"; import { ALL_PRIORITIES } from "../../../../../components/TasksView/utils/sorting"; @@ -24,7 +24,7 @@ interface PriorityPropertyProps { } export function PriorityProperty({ task }: PriorityPropertyProps) { - const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const currentPriority = task.priority; @@ -36,13 +36,9 @@ export function PriorityProperty({ task }: PriorityPropertyProps) { return; } - try { - collections.tasks.update(task.id, (draft) => { - draft.priority = newPriority; - }); + const transaction = taskActions.updatePriority(task.id, newPriority); + if (transaction) { setOpen(false); - } catch (error) { - console.error("[PriorityProperty] Failed to update priority:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx index 448d83c2d76..abf102a0e36 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx @@ -7,6 +7,7 @@ import { } from "@superset/ui/dropdown-menu"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo, useState } from "react"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { StatusIcon, @@ -22,6 +23,7 @@ interface StatusPropertyProps { export function StatusProperty({ task }: StatusPropertyProps) { const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const { data: allStatuses } = useLiveQuery( @@ -42,13 +44,9 @@ export function StatusProperty({ task }: StatusPropertyProps) { return; } - try { - collections.tasks.update(task.id, (draft) => { - draft.statusId = newStatus.id; - }); + const transaction = taskActions.updateStatus(task.id, newStatus.id); + if (transaction) { setOpen(false); - } catch (error) { - console.error("[StatusProperty] Failed to update status:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskActionMenu/TaskActionMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskActionMenu/TaskActionMenu.tsx index ca37d3f5b7c..1a56a7c2ccc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskActionMenu/TaskActionMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskActionMenu/TaskActionMenu.tsx @@ -13,7 +13,7 @@ import { HiOutlineTrash, } from "react-icons/hi2"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import type { TaskWithStatus } from "../../../components/TasksView/hooks/useTasksTable"; interface TaskActionMenuProps { @@ -22,7 +22,7 @@ interface TaskActionMenuProps { } export function TaskActionMenu({ task, onDelete }: TaskActionMenuProps) { - const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const { copyToClipboard } = useCopyToClipboard(); @@ -37,13 +37,11 @@ export function TaskActionMenu({ task, onDelete }: TaskActionMenuProps) { setOpen(false); }; - const handleDelete = async () => { - try { - await collections.tasks.delete(task.id); + const handleDelete = () => { + const transaction = taskActions.deleteTask(task.id); + if (transaction) { setOpen(false); onDelete?.(); - } catch (error) { - console.error("[TaskActionMenu] Failed to delete task:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/page.tsx index a51f58201ef..c28b9c33fd4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/page.tsx @@ -12,6 +12,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useMemo } from "react"; import { MarkdownEditor } from "renderer/components/MarkdownEditor"; import { apiTrpcClient } from "renderer/lib/api-trpc-client"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { Route as TasksLayoutRoute } from "../layout"; import { ActivitySection } from "./components/ActivitySection"; @@ -37,6 +38,7 @@ function TaskDetailPage() { const { tab, assignee, search } = TasksLayoutRoute.useSearch(); const navigate = useNavigate(); const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const isUuidTaskId = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( taskId, @@ -108,16 +110,12 @@ function TaskDetailPage() { const handleSaveTitle = (title: string) => { if (!task) return; - collections.tasks.update(task.id, (draft) => { - draft.title = title; - }); + taskActions.updateTitle(task.id, title); }; const handleSaveDescription = (markdown: string) => { if (!task) return; - collections.tasks.update(task.id, (draft) => { - draft.description = markdown; - }); + taskActions.updateDescription(task.id, markdown); }; const handleDelete = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksBoardView/TasksBoardView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksBoardView/TasksBoardView.tsx index 4965ac63ba3..ec08f8c9dbc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksBoardView/TasksBoardView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksBoardView/TasksBoardView.tsx @@ -12,7 +12,7 @@ import { import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import type { SelectTaskStatus } from "@superset/db/schema"; import { useCallback, useMemo, useState } from "react"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import type { TaskWithStatus } from "../../hooks/useTasksData"; import { compareStatusesForDropdown } from "../../utils/sorting"; import { KanbanCard } from "./components/KanbanCard"; @@ -29,7 +29,7 @@ export function TasksBoardView({ allStatuses, onTaskClick, }: TasksBoardViewProps) { - const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [activeTask, setActiveTask] = useState(null); const sensors = useSensors( @@ -95,11 +95,9 @@ export function TasksBoardView({ const task = data.find((t) => t.id === taskId); if (!task || task.statusId === targetStatusId) return; - collections.tasks.update(taskId, (draft) => { - draft.statusId = targetStatusId; - }); + taskActions.updateStatus(taskId, targetStatusId); }, - [data, collections], + [data, taskActions], ); const handleDragCancel = useCallback(() => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/TasksTableView.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/TasksTableView.test.ts index 0c5fd6b7ff5..209c0cf7614 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/TasksTableView.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/TasksTableView.test.ts @@ -11,12 +11,13 @@ function readComponent(relativePath: string): string { } describe("Tasks table delete wiring", () => { - test("TaskContextMenu deletes tasks through the collections API", () => { + test("TaskContextMenu deletes tasks through optimistic task actions", () => { const source = readComponent( "components/TaskContextMenu/TaskContextMenu.tsx", ); - expect(source).toContain("collections.tasks.delete(task.id)"); + expect(source).toContain("useOptimisticCollectionActions"); + expect(source).toContain("taskActions.deleteTask(task.id)"); expect(source).toContain("onSelect={handleDelete}"); }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/components/TaskContextMenu/TaskContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/components/TaskContextMenu/TaskContextMenu.tsx index 6f3f02a3b32..2e86c76cbee 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/components/TaskContextMenu/TaskContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTableView/components/TaskContextMenu/TaskContextMenu.tsx @@ -17,6 +17,7 @@ import { HiOutlineUserCircle, } from "react-icons/hi2"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { TaskWithStatus } from "../../../../hooks/useTasksTable"; import { compareStatusesForDropdown } from "../../../../utils/sorting"; @@ -38,6 +39,7 @@ export function TaskContextMenu({ onDelete, }: TaskContextMenuProps) { const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const { data: allStatuses } = useLiveQuery( (q) => q.from({ taskStatuses: collections.taskStatuses }), @@ -57,36 +59,15 @@ export function TaskContextMenu({ const users = useMemo(() => allUsers || [], [allUsers]); const handleStatusChange = (status: SelectTaskStatus) => { - try { - collections.tasks.update(task.id, (draft) => { - draft.statusId = status.id; - }); - } catch (error) { - console.error("[TaskContextMenu] Failed to update status:", error); - } + taskActions.updateStatus(task.id, status.id); }; const handleAssigneeChange = (userId: string | null) => { - try { - collections.tasks.update(task.id, (draft) => { - draft.assigneeId = userId; - draft.assigneeExternalId = null; - draft.assigneeDisplayName = null; - draft.assigneeAvatarUrl = null; - }); - } catch (error) { - console.error("[TaskContextMenu] Failed to update assignee:", error); - } + taskActions.updateAssignee(task.id, userId); }; const handlePriorityChange = (priority: typeof task.priority) => { - try { - collections.tasks.update(task.id, (draft) => { - draft.priority = priority; - }); - } catch (error) { - console.error("[TaskContextMenu] Failed to update priority:", error); - } + taskActions.updatePriority(task.id, priority); }; const { copyToClipboard } = useCopyToClipboard(); @@ -100,11 +81,9 @@ export function TaskContextMenu({ }; const handleDelete = () => { - try { - collections.tasks.delete(task.id); + const transaction = taskActions.deleteTask(task.id); + if (transaction) { onDelete?.(); - } catch (error) { - console.error("[TaskContextMenu] Failed to delete task:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx index 368d922e484..a2926a4d961 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx @@ -9,6 +9,7 @@ import { useLiveQuery } from "@tanstack/react-db"; import type { CellContext } from "@tanstack/react-table"; import { useMemo, useState } from "react"; import { HiOutlineUserCircle } from "react-icons/hi2"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { TaskWithStatus } from "../../useTasksTable"; @@ -18,6 +19,7 @@ interface AssigneeCellProps { export function AssigneeCell({ info }: AssigneeCellProps) { const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const task = info.row.original; @@ -36,14 +38,10 @@ export function AssigneeCell({ info }: AssigneeCellProps) { return; } - setOpen(false); - - collections.tasks.update(task.id, (draft) => { - draft.assigneeId = userId; - draft.assigneeExternalId = null; - draft.assigneeDisplayName = null; - draft.assigneeAvatarUrl = null; - }); + const transaction = taskActions.updateAssignee(task.id, userId); + if (transaction) { + setOpen(false); + } }; return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/PriorityCell/PriorityCell.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/PriorityCell/PriorityCell.tsx index b605da46445..d2f0703ad41 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/PriorityCell/PriorityCell.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/PriorityCell/PriorityCell.tsx @@ -7,7 +7,7 @@ import { } from "@superset/ui/dropdown-menu"; import type { CellContext } from "@tanstack/react-table"; import { useState } from "react"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { PriorityIcon } from "../../../../components/shared/PriorityIcon"; import { ALL_PRIORITIES } from "../../../../utils/sorting"; import type { TaskWithStatus } from "../../useTasksTable"; @@ -25,7 +25,7 @@ const PRIORITY_LABELS: Record = { }; export function PriorityCell({ info }: PriorityCellProps) { - const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const task = info.row.original; @@ -38,13 +38,9 @@ export function PriorityCell({ info }: PriorityCellProps) { return; } - try { - collections.tasks.update(task.id, (draft) => { - draft.priority = newPriority; - }); + const transaction = taskActions.updatePriority(task.id, newPriority); + if (transaction) { setOpen(false); - } catch (error) { - console.error("[PriorityCell] Failed to update priority:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/StatusCell/StatusCell.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/StatusCell/StatusCell.tsx index 7d9981b7a0a..44b886868cc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/StatusCell/StatusCell.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/StatusCell/StatusCell.tsx @@ -7,6 +7,7 @@ import { } from "@superset/ui/dropdown-menu"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo, useState } from "react"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { StatusIcon, @@ -22,6 +23,7 @@ interface StatusCellProps { export function StatusCell({ taskWithStatus }: StatusCellProps) { const collections = useCollections(); + const { tasks: taskActions } = useOptimisticCollectionActions(); const [open, setOpen] = useState(false); const { data: allStatuses } = useLiveQuery( @@ -42,13 +44,12 @@ export function StatusCell({ taskWithStatus }: StatusCellProps) { return; } - try { - collections.tasks.update(taskWithStatus.id, (draft) => { - draft.statusId = newStatus.id; - }); + const transaction = taskActions.updateStatus( + taskWithStatus.id, + newStatus.id, + ); + if (transaction) { setOpen(false); - } catch (error) { - console.error("[StatusCell] Failed to update status:", error); } }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx index 3f676523390..4f6a763070c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx @@ -1,8 +1,3 @@ -import { - AGENT_PRESET_COMMANDS, - AGENT_PRESET_DESCRIPTIONS, - AGENT_TYPES, -} from "@superset/shared/agent-command"; import { Button } from "@superset/ui/button"; import { DropdownMenu, @@ -13,9 +8,9 @@ import { } from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useNavigate } from "@tanstack/react-router"; +import { Eye, EyeOff, Settings } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { HiMiniCog6Tooth, HiMiniCommandLine } from "react-icons/hi2"; -import { LuCirclePlus, LuPin } from "react-icons/lu"; +import { HiMiniCommandLine } from "react-icons/hi2"; import { getPresetIcon, useIsDarkTheme, @@ -34,7 +29,7 @@ interface V2PresetsBarProps { // Co-located to keep v2 self-contained. Mirrors the v1 array in // renderer/hotkeys/registry.ts; order matches the registry OPEN_PRESET_{n} -// definitions so PRESET_HOTKEY_IDS[i] targets the i-th pinned preset. +// definitions so PRESET_HOTKEY_IDS[i] targets the i-th visible preset. const PRESET_HOTKEY_IDS: HotkeyId[] = [ "OPEN_PRESET_1", "OPEN_PRESET_2", @@ -47,24 +42,9 @@ const PRESET_HOTKEY_IDS: HotkeyId[] = [ "OPEN_PRESET_9", ]; -interface PresetTemplate { - name: string; - description: string; - cwd: string; - commands: string[]; -} - -const QUICK_ADD_PRESET_TEMPLATES: PresetTemplate[] = AGENT_TYPES.map( - (agent) => ({ - name: agent, - description: AGENT_PRESET_DESCRIPTIONS[agent], - cwd: "", - commands: AGENT_PRESET_COMMANDS[agent], - }), -); - -function isPresetPinnedToBar(pinnedToBar: boolean | undefined): boolean { - // Backward-compatibility rule mirroring v1: undefined defaults to pinned. +function isPresetVisibleInBar(pinnedToBar: boolean | undefined): boolean { + // The persisted field is legacy "pinned" wording; the v2 UI treats it as + // show/hide visibility. Undefined defaults to visible for compatibility. return pinnedToBar !== false; } @@ -73,11 +53,11 @@ function areStringArraysEqual(left: string[], right: string[]): boolean { return left.every((value, index) => value === right[index]); } -function getPinnedPresetOrder( +function getVisiblePresetOrder( presets: ReadonlyArray<{ id: string; pinnedToBar?: boolean }>, ): string[] { return presets.flatMap((preset) => - isPresetPinnedToBar(preset.pinnedToBar) ? [preset.id] : [], + isPresetVisibleInBar(preset.pinnedToBar) ? [preset.id] : [], ); } @@ -90,97 +70,57 @@ export function V2PresetsBar({ const collections = useCollections(); useMigrateV1PresetsToV2(); - const [localPinnedPresetIds, setLocalPinnedPresetIds] = useState( - () => getPinnedPresetOrder(matchedPresets), + const [localVisiblePresetIds, setLocalVisiblePresetIds] = useState( + () => getVisiblePresetOrder(matchedPresets), ); useEffect(() => { - const serverPinnedPresetIds = getPinnedPresetOrder(matchedPresets); - setLocalPinnedPresetIds((current) => - areStringArraysEqual(current, serverPinnedPresetIds) + const serverVisiblePresetIds = getVisiblePresetOrder(matchedPresets); + setLocalVisiblePresetIds((current) => + areStringArraysEqual(current, serverVisiblePresetIds) ? current - : serverPinnedPresetIds, + : serverVisiblePresetIds, ); }, [matchedPresets]); - const presetsByName = useMemo(() => { - const map = new Map(); - for (const preset of matchedPresets) { - const existing = map.get(preset.name); - if (existing) { - existing.push(preset); - continue; - } - map.set(preset.name, [preset]); - } - return map; - }, [matchedPresets]); - - const pinnedPresets = useMemo(() => { + const visiblePresets = useMemo(() => { const presetById = new Map( matchedPresets.map((preset, index) => [preset.id, { preset, index }]), ); - const orderedPinnedPresets: Array<{ + const orderedVisiblePresets: Array<{ preset: V2TerminalPresetRow; index: number; }> = []; const seenIds = new Set(); - for (const presetId of localPinnedPresetIds) { + for (const presetId of localVisiblePresetIds) { const item = presetById.get(presetId); if (!item) continue; - if (!isPresetPinnedToBar(item.preset.pinnedToBar)) continue; - orderedPinnedPresets.push(item); + if (!isPresetVisibleInBar(item.preset.pinnedToBar)) continue; + orderedVisiblePresets.push(item); seenIds.add(presetId); } for (const [index, preset] of matchedPresets.entries()) { - if (!isPresetPinnedToBar(preset.pinnedToBar)) continue; + if (!isPresetVisibleInBar(preset.pinnedToBar)) continue; if (seenIds.has(preset.id)) continue; - orderedPinnedPresets.push({ preset, index }); + orderedVisiblePresets.push({ preset, index }); } - return orderedPinnedPresets; - }, [matchedPresets, localPinnedPresetIds]); + return orderedVisiblePresets; + }, [matchedPresets, localVisiblePresetIds]); - const presetIndexById = useMemo( - () => new Map(matchedPresets.map((preset, index) => [preset.id, index])), - [matchedPresets], + const visiblePresetIndexById = useMemo( + () => + new Map( + visiblePresets.map(({ preset }, visibleIndex) => [ + preset.id, + visibleIndex, + ]), + ), + [visiblePresets], ); - const managedPresets = useMemo(() => { - const templateNames = new Set( - QUICK_ADD_PRESET_TEMPLATES.map((t) => t.name), - ); - const primaryTemplatePresetIds = new Set( - QUICK_ADD_PRESET_TEMPLATES.flatMap((template) => { - const match = presetsByName.get(template.name)?.[0]; - return match ? [match.id] : []; - }), - ); - const fromTemplates = QUICK_ADD_PRESET_TEMPLATES.map((template) => ({ - key: `template:${template.name}`, - name: template.name, - preset: presetsByName.get(template.name)?.[0], - template, - iconName: template.name, - })); - const customExisting = matchedPresets - .filter( - (preset) => - !templateNames.has(preset.name) || - !primaryTemplatePresetIds.has(preset.id), - ) - .map((preset) => ({ - key: `preset:${preset.id}`, - name: preset.name || "default", - preset: preset as V2TerminalPresetRow | undefined, - template: null as PresetTemplate | null, - iconName: preset.name, - })); - return [...fromTemplates, ...customExisting]; - }, [matchedPresets, presetsByName]); - const handleEditPreset = useCallback( (presetId: string) => { navigate({ @@ -191,9 +131,9 @@ export function V2PresetsBar({ [navigate], ); - const handleLocalPinnedReorder = useCallback( + const handleLocalVisibleReorder = useCallback( (fromIndex: number, toIndex: number) => { - setLocalPinnedPresetIds((current) => { + setLocalVisiblePresetIds((current) => { if ( fromIndex < 0 || fromIndex >= current.length || @@ -211,63 +151,45 @@ export function V2PresetsBar({ [], ); - const handlePersistPinnedReorder = useCallback( - (presetId: string, targetPinnedIndex: number) => { - const reorderedPinnedPresetIds = [...localPinnedPresetIds]; - const currentPinnedIndex = reorderedPinnedPresetIds.indexOf(presetId); - if (currentPinnedIndex === -1) return; - const [moved] = reorderedPinnedPresetIds.splice(currentPinnedIndex, 1); - reorderedPinnedPresetIds.splice(targetPinnedIndex, 0, moved); + const handlePersistVisibleReorder = useCallback( + (presetId: string, targetVisibleIndex: number) => { + const reorderedVisiblePresetIds = [...localVisiblePresetIds]; + const currentVisibleIndex = reorderedVisiblePresetIds.indexOf(presetId); + if (currentVisibleIndex === -1) return; + const [moved] = reorderedVisiblePresetIds.splice(currentVisibleIndex, 1); + reorderedVisiblePresetIds.splice(targetVisibleIndex, 0, moved); - const pinnedSet = new Set(reorderedPinnedPresetIds); - const unpinned = matchedPresets - .filter((preset) => !pinnedSet.has(preset.id)) + const visibleSet = new Set(reorderedVisiblePresetIds); + const hidden = matchedPresets + .filter((preset) => !visibleSet.has(preset.id)) .map((preset) => preset.id); - const finalOrder = [...reorderedPinnedPresetIds, ...unpinned]; + const finalOrder = [...reorderedVisiblePresetIds, ...hidden]; + const currentTabOrderById = new Map( + matchedPresets.map((preset) => [preset.id, preset.tabOrder]), + ); for (const [index, id] of finalOrder.entries()) { + if (currentTabOrderById.get(id) === index) continue; collections.v2TerminalPresets.update(id, (draft) => { draft.tabOrder = index; }); } }, - [collections.v2TerminalPresets, localPinnedPresetIds, matchedPresets], + [collections.v2TerminalPresets, localVisiblePresetIds, matchedPresets], ); - const handleTogglePinned = useCallback( - (presetId: string, nextPinned: boolean) => { + const handleTogglePresetVisibility = useCallback( + (presetId: string, nextVisible: boolean) => { collections.v2TerminalPresets.update(presetId, (draft) => { - draft.pinnedToBar = nextPinned; + draft.pinnedToBar = nextVisible; }); }, [collections.v2TerminalPresets], ); - const handleCreateFromTemplate = useCallback( - (template: PresetTemplate) => { - const maxTabOrder = matchedPresets.reduce( - (max, preset) => Math.max(max, preset.tabOrder), - -1, - ); - collections.v2TerminalPresets.insert({ - id: crypto.randomUUID(), - name: template.name, - description: template.description, - cwd: template.cwd, - commands: template.commands, - projectIds: null, - pinnedToBar: true, - executionMode: "new-tab", - tabOrder: maxTabOrder + 1, - createdAt: new Date(), - }); - }, - [collections.v2TerminalPresets, matchedPresets], - ); - return (
@@ -275,7 +197,7 @@ export function V2PresetsBar({ @@ -283,32 +205,22 @@ export function V2PresetsBar({ Manage Presets - - {managedPresets.map((item) => { - const icon = getPresetIcon(item.iconName, isDark); - const isPinned = item.preset - ? isPresetPinnedToBar(item.preset.pinnedToBar) - : false; - const hasPreset = !!item.preset; - const presetIndex = item.preset - ? presetIndexById.get(item.preset.id) - : undefined; + + {matchedPresets.map((preset) => { + const icon = getPresetIcon(preset.name, isDark); + const isVisible = isPresetVisibleInBar(preset.pinnedToBar); + const visibleIndex = visiblePresetIndexById.get(preset.id); const hotkeyId = - typeof presetIndex === "number" - ? PRESET_HOTKEY_IDS[presetIndex] + typeof visibleIndex === "number" + ? PRESET_HOTKEY_IDS[visibleIndex] : undefined; return ( { event.preventDefault(); - if (hasPreset && item.preset) { - handleTogglePinned(item.preset.id, !isPinned); - return; - } - if (!item.template) return; - handleCreateFromTemplate(item.template); + handleTogglePresetVisibility(preset.id, !isVisible); }} > {icon ? ( @@ -316,19 +228,17 @@ export function V2PresetsBar({ ) : ( )} - {item.name || "default"} + + {preset.name || "default"} +
- {hotkeyId ? : null} - {hasPreset ? ( - + {isVisible && hotkeyId ? ( + + ) : null} + {isVisible ? ( + ) : ( - + )}
@@ -339,25 +249,24 @@ export function V2PresetsBar({ className="gap-2" onClick={() => navigate({ to: "/settings/terminal" })} > - + Manage Presets
-
- {pinnedPresets.map(({ preset, index }, pinnedIndex) => { - const hotkeyId = PRESET_HOTKEY_IDS[index]; + {visiblePresets.map(({ preset }, visibleIndex) => { + const hotkeyId = PRESET_HOTKEY_IDS[visibleIndex]; return ( handleEditPreset(presetToEdit.id)} - onLocalReorder={handleLocalPinnedReorder} - onPersistReorder={handlePersistPinnedReorder} + onLocalReorder={handleLocalVisibleReorder} + onPersistReorder={handlePersistVisibleReorder} /> ); })} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/components/V2PresetBarItem/V2PresetBarItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/components/V2PresetBarItem/V2PresetBarItem.tsx index 231c37a49a9..ec7374bad4c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/components/V2PresetBarItem/V2PresetBarItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/components/V2PresetBarItem/V2PresetBarItem.tsx @@ -19,18 +19,18 @@ const V2_PRESET_BAR_ITEM_TYPE = "V2_PRESET_BAR_ITEM"; interface V2PresetBarItemProps { preset: V2TerminalPresetRow; - pinnedIndex: number; + visibleIndex: number; hotkeyId?: HotkeyId; isDark: boolean; onExecutePreset: (preset: V2TerminalPresetRow) => void; onEdit: (preset: V2TerminalPresetRow) => void; onLocalReorder: (fromIndex: number, toIndex: number) => void; - onPersistReorder: (presetId: string, targetPinnedIndex: number) => void; + onPersistReorder: (presetId: string, targetVisibleIndex: number) => void; } export function V2PresetBarItem({ preset, - pinnedIndex, + visibleIndex, hotkeyId, isDark, onExecutePreset, @@ -47,22 +47,22 @@ export function V2PresetBarItem({ type: V2_PRESET_BAR_ITEM_TYPE, item: { id: preset.id, - index: pinnedIndex, - originalIndex: pinnedIndex, + index: visibleIndex, + originalIndex: visibleIndex, }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }), - [preset.id, pinnedIndex], + [preset.id, visibleIndex], ); const [, drop] = useDrop({ accept: V2_PRESET_BAR_ITEM_TYPE, hover: (item: { id: string; index: number; originalIndex: number }) => { - if (item.index !== pinnedIndex) { - onLocalReorder(item.index, pinnedIndex); - item.index = pinnedIndex; + if (item.index !== visibleIndex) { + onLocalReorder(item.index, visibleIndex); + item.index = visibleIndex; } }, drop: (item: { id: string; index: number; originalIndex: number }) => { @@ -89,15 +89,19 @@ export function V2PresetBarItem({ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx index f576d844d8f..c213754ef88 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/BrowserPane.tsx @@ -164,7 +164,7 @@ export function BrowserPaneToolbar({ ctx }: BrowserPaneToolbarProps) { const PaneHeaderActions = ctx.components.PaneHeaderActions; return ( -
+
-
+
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx index 1c6cff146d0..555146add71 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/BrowserPane/components/BrowserToolbar/BrowserToolbar.tsx @@ -107,8 +107,8 @@ export function BrowserToolbar({ ); return ( -
-
+
+
-
-
+
+
{isEditing ? (
{isBlank ? ( - + Enter URL or search... ) : ( <> - + {url} {pageTitle && ( - + / {pageTitle} )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/ChatPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/ChatPane.tsx index b1978b922d5..bff5eaab35f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/ChatPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/ChatPane.tsx @@ -32,7 +32,7 @@ export function ChatPane({ return (
-
+
@@ -78,13 +78,13 @@ export function DiffFileHeader({ }} disabled={!onOpenFile && !onOpenInExternalEditor} aria-label="Open in file viewer" - className="flex min-w-0 flex-1 items-center gap-2 rounded border border-border px-2 py-1 text-left transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-60" + className="flex h-6 min-w-20 flex-[1_1_10rem] items-center gap-1.5 rounded border border-border px-1.5 py-0.5 text-left transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-60" > - - + + {path} - + {additions > 0 && ( +{additions} @@ -99,12 +99,21 @@ export function DiffFileHeader({ - - Open in file viewer. {CLICK_HINT_TOOLTIP} + +
+
Open in file viewer. {CLICK_HINT_TOOLTIP}
+
+ {path} +
+
-
+
@@ -113,7 +122,7 @@ export function DiffFileHeader({ onClick={onOpenInExternalEditor} disabled={!onOpenInExternalEditor} aria-label="Open in editor" - className="rounded p-1 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" + className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" > @@ -126,16 +135,16 @@ export function DiffFileHeader({ -
+
onToggleViewed()} - className="size-3.5 border-muted-foreground/50" + className="size-3 border-muted-foreground/50" /> @@ -150,7 +159,7 @@ export function DiffFileHeader({ aria-label={ expandUnchanged ? "Hide unchanged regions" : "Show all lines" } - className="rounded p-1 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" + className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" > {expandUnchanged ? ( @@ -171,7 +180,7 @@ export function DiffFileHeader({ onClick={onCopyContents} disabled={!onCopyContents} aria-label="Copy file contents" - className="rounded p-1 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" + className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-40" > @@ -188,7 +197,7 @@ export function DiffFileHeader({ onClick={onDiscard} disabled={!onDiscard} aria-label="Discard changes" - className="rounded p-1 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-destructive disabled:pointer-events-none disabled:opacity-40" + className="rounded p-0.5 text-muted-foreground/60 transition-colors hover:bg-accent hover:text-destructive disabled:pointer-events-none disabled:opacity-40" > diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx index 29e83b40519..bcbcde96ed6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx @@ -1,6 +1,7 @@ import type { RendererContext } from "@superset/panes"; import { alert } from "@superset/ui/atoms/Alert"; import { useCallback, useEffect } from "react"; +import { getBaseName } from "renderer/lib/pathBasename"; import { deriveMemoDisplayName, getTrustedMemoRootPath, @@ -88,7 +89,7 @@ function FilePaneContent({ context, workspaceId }: FilePaneProps) { const hasConflict = document.conflict !== null; useEffect(() => { if (!hasConflict) return; - const name = filePath.split(/[/\\]/).pop(); + const name = getBaseName(filePath); alert({ title: `Do you want to save the changes you made to ${name}?`, description: "Your changes will be lost if you don't save them.", diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/FilePaneHeaderExtras/FilePaneHeaderExtras.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/FilePaneHeaderExtras/FilePaneHeaderExtras.tsx index e91050e32b7..d2f98bc10e2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/FilePaneHeaderExtras/FilePaneHeaderExtras.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/FilePaneHeaderExtras/FilePaneHeaderExtras.tsx @@ -72,7 +72,7 @@ function FilePaneHeaderExtrasInner({ }, [filePath, openInExternalEditor]); return ( -
+
{shouldShowToggle && activeView && ( - {views.map((view) => ( - - ))} +
+ {views.map((view) => { + const label = resolveViewLabel(view, filePath); + + return ( + + ); + })}
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/registry/views/ImageView/ImageView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/registry/views/ImageView/ImageView.tsx index dae1d9a845e..807b40c56e5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/registry/views/ImageView/ImageView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/registry/views/ImageView/ImageView.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { getBaseName } from "renderer/lib/pathBasename"; import { getImageMimeType } from "shared/file-types"; import type { ViewProps } from "../../types"; @@ -34,7 +35,7 @@ export function ImageView({ document, filePath }: ViewProps) { > {filePath.split("/").pop() diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx index 6e0d97c0c20..722fce9982a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx @@ -214,13 +214,14 @@ export function TerminalSessionDropdown({
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/PresetsSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/PresetsSection.tsx index da4154dc593..9ef733c6df7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/PresetsSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/PresetsSection.tsx @@ -312,11 +312,11 @@ export function PresetsSection({ [setPresetAutoApply], ); - const handleTogglePin = useCallback( - (presetId: string, pinned: boolean) => { + const handleToggleVisibility = useCallback( + (presetId: string, visible: boolean) => { updatePreset.mutate({ id: presetId, - patch: { pinnedToBar: pinned }, + patch: { pinnedToBar: visible }, }); }, [updatePreset], @@ -485,7 +485,7 @@ export function PresetsSection({ onEdit={setEditingPreset} onLocalReorder={handleLocalReorder} onPersistReorder={handlePersistReorder} - onTogglePin={handleTogglePin} + onToggleVisibility={handleToggleVisibility} />

Click a preset row to edit details. diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/components/PresetsTable/PresetsTable.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/components/PresetsTable/PresetsTable.tsx index 65d6a4eb803..ec10c06941b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/components/PresetsTable/PresetsTable.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetsSection/components/PresetsTable/PresetsTable.tsx @@ -11,7 +11,7 @@ interface PresetsTableProps { onEdit: (presetId: string) => void; onLocalReorder: (fromIndex: number, toIndex: number) => void; onPersistReorder: (presetId: string, targetIndex: number) => void; - onTogglePin: (presetId: string, pinned: boolean) => void; + onToggleVisibility: (presetId: string, visible: boolean) => void; } export function PresetsTable({ @@ -22,7 +22,7 @@ export function PresetsTable({ onEdit, onLocalReorder, onPersistReorder, - onTogglePin, + onToggleVisibility, }: PresetsTableProps) { return (

@@ -33,7 +33,7 @@ export function PresetsTable({
Applies to
Mode
Auto-run
-
Pinned
+
Visibility
)) ) : ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/V2PresetsSection/V2PresetsSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/V2PresetsSection/V2PresetsSection.tsx index 9ea6a854c7b..df5235af68b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/V2PresetsSection/V2PresetsSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/V2PresetsSection/V2PresetsSection.tsx @@ -393,9 +393,9 @@ export function V2PresetsSection({ [updateV2Preset], ); - const handleTogglePin = useCallback( - (presetId: string, pinned: boolean) => { - updateV2Preset(presetId, { pinnedToBar: pinned }); + const handleToggleVisibility = useCallback( + (presetId: string, visible: boolean) => { + updateV2Preset(presetId, { pinnedToBar: visible }); }, [updateV2Preset], ); @@ -557,7 +557,7 @@ export function V2PresetsSection({ onEdit={setEditingPreset} onLocalReorder={handleLocalReorder} onPersistReorder={handlePersistReorder} - onTogglePin={handleTogglePin} + onToggleVisibility={handleToggleVisibility} />

Click a preset row to edit details. diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/RepositorySection/RepositorySection.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/RepositorySection/RepositorySection.tsx index c22b3b463bb..eaaa5376eb4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/RepositorySection/RepositorySection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/components/RepositorySection/RepositorySection.tsx @@ -1,8 +1,7 @@ import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; -import { toast } from "@superset/ui/sonner"; import { useEffect, useRef, useState } from "react"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; +import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/hooks/useOptimisticCollectionActions"; interface RepositorySectionProps { projectId: string; @@ -13,9 +12,9 @@ export function RepositorySection({ projectId, currentRepoCloneUrl, }: RepositorySectionProps) { + const { v2Projects: projectActions } = useOptimisticCollectionActions(); const [isEditing, setIsEditing] = useState(false); const [value, setValue] = useState(currentRepoCloneUrl ?? ""); - const [isSaving, setIsSaving] = useState(false); const inputRef = useRef(null); useEffect(() => { @@ -32,25 +31,18 @@ export function RepositorySection({ setIsEditing(false); }; - const save = async () => { - if (isSaving) return; + const save = () => { const trimmed = value.trim(); if (trimmed === (currentRepoCloneUrl ?? "")) { setIsEditing(false); return; } - setIsSaving(true); - try { - await apiTrpcClient.v2Project.update.mutate({ - id: projectId, - repoCloneUrl: trimmed === "" ? null : trimmed, - }); - toast.success("Repository updated"); + const transaction = projectActions.updateRepository( + projectId, + trimmed === "" ? null : trimmed, + ); + if (transaction) { setIsEditing(false); - } catch (err) { - toast.error(err instanceof Error ? err.message : "Failed to update"); - } finally { - setIsSaving(false); } }; @@ -67,7 +59,7 @@ export function RepositorySection({ onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); - void save(); + save(); } else if (e.key === "Escape") { e.preventDefault(); cancelEdit(); @@ -79,12 +71,11 @@ export function RepositorySection({ variant="outline" size="sm" onClick={cancelEdit} - disabled={isSaving} > Cancel - ) : ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx index b79db133d2c..fb910d4fd49 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx @@ -77,8 +77,8 @@ export function CollapsedWorkspaceItem({ onMouseEnter={onMouseEnter} className={cn( "relative flex items-center justify-center size-8 rounded-md", - "hover:bg-muted/50 transition-colors", - isActive && "bg-muted", + "transition-colors", + isActive ? "bg-muted hover:bg-muted" : "hover:bg-muted/50", )} > { store: StoreApi>; @@ -55,6 +56,7 @@ function SplitView({ return ( { if (sizes[0] != null) { @@ -70,7 +72,10 @@ function SplitView({ groupRef.current?.setLayout([50, 50]); }} > - + ({ /> - + ({ } return ( -

+
({ {/* biome-ignore lint/a11y/noStaticElementInteractions: clicking anywhere in a pane focuses it (standard IDE behavior) */}
{ diff --git a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/components/DefaultHeaderContent/DefaultHeaderContent.tsx b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/components/DefaultHeaderContent/DefaultHeaderContent.tsx index 14261228da5..412f0a0e255 100644 --- a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/components/DefaultHeaderContent/DefaultHeaderContent.tsx +++ b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/components/DefaultHeaderContent/DefaultHeaderContent.tsx @@ -19,16 +19,17 @@ export function DefaultHeaderContent({ actionsContent, }: DefaultHeaderContentProps) { return ( -
-
+
+
{titleContent ?? ( <> {icon && {icon}} {title} diff --git a/packages/panes/src/react/components/Workspace/components/Tab/constants.ts b/packages/panes/src/react/components/Workspace/components/Tab/constants.ts new file mode 100644 index 00000000000..5d224339c05 --- /dev/null +++ b/packages/panes/src/react/components/Workspace/components/Tab/constants.ts @@ -0,0 +1 @@ +export const PANE_MIN_SIZE_CLASS_NAME = "min-h-[160px] min-w-[260px]"; diff --git a/packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx b/packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx index 4f43fde1f4c..18742a131ba 100644 --- a/packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx +++ b/packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx @@ -110,7 +110,10 @@ export function TabItem({
({ {isEditing ? (
({ > {title} -
+