diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx index 4c089eb9c87..92b8a4bbbc9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { GoGitBranch } from "react-icons/go"; -import { HiCheck, HiExclamationTriangle } from "react-icons/hi2"; +import { HiExclamationTriangle } from "react-icons/hi2"; import { useHostTargetUrl } from "renderer/hooks/host-service/useHostTargetUrl"; import { authClient } from "renderer/lib/auth-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; @@ -15,6 +15,7 @@ import { clearAttachments, loadAttachments, } from "renderer/lib/pending-attachment-store"; +import { V2WorkspaceLoadingView } from "renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView"; import { useAdoptWorktree } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useAdoptWorktree"; import { useCheckoutDashboardWorkspace } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCheckoutDashboardWorkspace"; import { useCreateDashboardWorkspace } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace"; @@ -22,6 +23,7 @@ import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/u import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { PendingWorkspaceRow } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; +import type { WorkspaceInitStep } from "shared/types/workspace-init"; import type { ResolvedPrContent } from "./buildForkAgentLaunch"; import { buildAdoptPayload, @@ -277,7 +279,8 @@ function PendingWorkspacePage() { }, [pending, fireIntent]); // Poll host-service for step-by-step progress (fork + checkout only; - // adopt is fast and doesn't instrument progress). + // adopt is fast and doesn't instrument progress). Drives the keypad-loader + // off real backend state so keys press as steps advance. const intentHasProgress = pending?.intent === "fork" || pending?.intent === "checkout"; const hostUrl = useHostTargetUrl(pending?.hostTarget ?? null); @@ -295,7 +298,7 @@ function PendingWorkspacePage() { enabled: pending?.status === "creating" && !!hostUrl && intentHasProgress, }); - const steps = progress?.steps ?? []; + const loaderStep = mapHostProgressToInitStep(progress?.steps); const STALE_THRESHOLD_MS = 2 * 60 * 1000; const [now, setNow] = useState(Date.now()); @@ -371,12 +374,62 @@ function PendingWorkspacePage() { ); } - const creatingLabel = + const creatingTitle = pending.intent === "adopt" - ? "Adopting worktree..." + ? "Adopting worktree" : pending.intent === "checkout" - ? "Checking out branch..." - : "Creating workspace..."; + ? "Checking out branch" + : "Setting up workspace"; + + // Render the keypad through the "succeeded" hold (and during the brief + // pre-sync window before that) so the last key animates to pressed — + // the host clears progress without ever flagging "registering: done", + // so the success transition is the only signal we have for that frame. + const showKeypad = + pending.status === "creating" || + (pending.status === "succeeded" && !(syncTimedOut && !workspaceSynced)); + + if (showKeypad) { + const isFinalizing = pending.status === "succeeded"; + return ( + + {!isFinalizing && ( + + )} + {isFinalizing && pending.warnings.length > 0 && ( + + )} + + ); + } return (
@@ -389,59 +442,30 @@ function PendingWorkspacePage() {
- {pending.status === "creating" && ( -
-
-

- {isStale - ? "This is taking longer than expected..." - : creatingLabel} -

- - {elapsedLabel} + {pending.status === "succeeded" && syncTimedOut && !workspaceSynced && ( +
+
+ + + Workspace was created but hasn't synced to this device yet. + Check your connection.
- {intentHasProgress && steps.length > 0 ? ( -
- {steps.map((step) => ( -
- {step.status === "done" ? ( - - ) : step.status === "active" ? ( -
-
-
- ) : ( -
-
-
- )} - - {step.label} - -
- ))} -
- ) : ( - // Adopt has no host-side progress steps — show a generic spinner. -
-
-
-
-
- )} -
+
+ + - - -
-
- ) : ( -
-
- - Workspace ready — opening... -
- {pending.warnings.length > 0 && ( -
    - {pending.warnings.map((w) => ( -
  • - - {w} -
  • - ))} -
- )} -
- ))} - {pending.status === "failed" && (
@@ -551,3 +518,24 @@ function PendingWorkspacePage() {
); } + +type HostProgressStep = { + id: string; + label: string; + status: "pending" | "active" | "done"; +}; + +// Maps the host-service's 3-step progress (ensuring_repo → creating_worktree +// → registering) onto the v1 keypad's step vocabulary. Skipped keys (fetching, +// copying_config) press through quickly when the keypad jumps past them. +function mapHostProgressToInitStep( + steps: HostProgressStep[] | null | undefined, +): WorkspaceInitStep | undefined { + if (!steps || steps.length === 0) return undefined; + const byId = new Map(steps.map((s) => [s.id, s.status])); + if (byId.get("registering") === "done") return "ready"; + if (byId.get("registering") === "active") return "finalizing"; + if (byId.get("creating_worktree") === "active") return "creating_worktree"; + if (byId.get("ensuring_repo") === "active") return "syncing"; + return "pending"; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.css b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.css new file mode 100644 index 00000000000..07730377568 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.css @@ -0,0 +1,146 @@ +.keypad-loader { + --travel: 26; + position: relative; + aspect-ratio: 400 / 310; + display: flex; + place-items: center; + width: clamp(260px, 34vw, 420px); + transform-style: preserve-3d; + user-select: none; +} + +.keypad-loader__base { + position: absolute; + bottom: 0; + width: 100%; + pointer-events: none; +} + +.keypad-loader__base img { + width: 100%; + display: block; +} + +.keypad-loader__key { + position: absolute; + width: 21%; + height: 24%; + transform-style: preserve-3d; + clip-path: polygon( + 0 0, + 54% 0, + 89% 24%, + 100% 70%, + 54% 100%, + 46% 100%, + 0 69%, + 12% 23%, + 47% 0% + ); + mask: url("../assets/key-single.png") 50% 50% / 100% 100%; + -webkit-mask: url("../assets/key-single.png") 50% 50% / 100% 100%; +} + +.keypad-loader__key--one { + left: 13.5%; + bottom: 57.2%; +} +.keypad-loader__key--two { + left: 25.8%; + bottom: 48.5%; +} +.keypad-loader__key--three { + left: 38%; + bottom: 39.2%; +} +.keypad-loader__key--four { + left: 50.4%; + bottom: 30.2%; +} +.keypad-loader__key--five { + left: 62.7%; + bottom: 21%; +} + +.keypad-loader__mask { + width: 100%; + height: 100%; + display: inline-block; +} + +.keypad-loader__content { + width: 100%; + height: 100%; + display: inline-block; + position: relative; + container-type: inline-size; + transition: + translate 0.7s cubic-bezier(0.22, 1, 0.36, 1), + filter 0.7s ease-out; +} + +.keypad-loader__content img { + position: absolute; + top: 0; + left: 50%; + width: 96%; + translate: -50% 1%; + pointer-events: none; + filter: hue-rotate(118deg) saturate(1.15) brightness(0.92); + transition: filter 0.7s ease-out; +} + +.keypad-loader__key[data-pressed="true"] .keypad-loader__content { + translate: 0 calc(var(--travel) * 1%); +} + +.keypad-loader__key[data-pressed="true"] .keypad-loader__content img { + filter: hue-rotate(118deg) saturate(1.35) brightness(1.05); +} + +.keypad-loader__key[data-active="true"] .keypad-loader__content { + animation: keypad-loader-bob 2.2s ease-in-out infinite; +} + +@keyframes keypad-loader-bob { + 0%, + 100% { + translate: 0 0; + } + 50% { + translate: 0 calc(var(--travel) * 0.28%); + } +} + +.keypad-loader__text { + position: absolute; + top: 5%; + left: 0; + width: 52%; + height: 62%; + z-index: 21; + font-size: 18cqi; + color: #fff; + translate: 45% -16%; + transform: rotateX(36deg) rotateY(45deg) rotateX(-90deg); + display: grid; + place-items: center; + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.45)); +} + +.keypad-loader__text svg { + width: 62%; + height: 62%; +} + +.keypad-loader__key[data-pressed="true"] .keypad-loader__text { + opacity: 0.85; +} + +@media (prefers-reduced-motion: reduce) { + .keypad-loader__content, + .keypad-loader__key[data-active="true"] .keypad-loader__content { + transition: none; + animation: none; + } +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.tsx new file mode 100644 index 00000000000..ef5f10be5f9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/KeypadLoader.tsx @@ -0,0 +1,110 @@ +import { cn } from "@superset/ui/utils"; +import type { ComponentType } from "react"; +import { + LuDatabase, + LuDownload, + LuFileCog, + LuGitBranch, + LuRefreshCw, +} from "react-icons/lu"; +import { + getStepIndex, + type WorkspaceInitStep, +} from "shared/types/workspace-init"; +import keySingleUrl from "../assets/key-single.png"; +import keypadBaseUrl from "../assets/keypad-base.png"; +import "./KeypadLoader.css"; + +type KeyId = "one" | "two" | "three" | "four" | "five"; + +interface KeyDef { + id: KeyId; + pressedAfter: WorkspaceInitStep; + activeSteps: readonly WorkspaceInitStep[]; + Icon: ComponentType<{ className?: string }>; + label: string; +} + +const KEYS: readonly KeyDef[] = [ + { + id: "one", + pressedAfter: "verifying", + activeSteps: ["pending", "syncing", "verifying"], + Icon: LuRefreshCw, + label: "Syncing", + }, + { + id: "two", + pressedAfter: "fetching", + activeSteps: ["fetching"], + Icon: LuDownload, + label: "Fetching", + }, + { + id: "three", + pressedAfter: "creating_worktree", + activeSteps: ["creating_worktree"], + Icon: LuGitBranch, + label: "Creating worktree", + }, + { + id: "four", + pressedAfter: "copying_config", + activeSteps: ["copying_config"], + Icon: LuFileCog, + label: "Copying config", + }, + { + id: "five", + pressedAfter: "finalizing", + activeSteps: ["finalizing"], + Icon: LuDatabase, + label: "Finalizing", + }, +]; + +interface KeypadLoaderProps { + currentStep: WorkspaceInitStep; + className?: string; +} + +export function KeypadLoader({ currentStep, className }: KeypadLoaderProps) { + const currentIdx = getStepIndex(currentStep); + + return ( +
k.activeSteps.includes(currentStep))?.label ?? + "Preparing" + }`} + > +
+ +
+ {KEYS.map(({ id, pressedAfter, activeSteps, Icon }) => { + const thresholdIdx = getStepIndex(pressedAfter); + const isPressed = currentIdx > thresholdIdx; + const isActive = activeSteps.includes(currentStep); + return ( +
+ + + + + + + + +
+ ); + })} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/index.ts new file mode 100644 index 00000000000..d2bb8f2de8f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/KeypadLoader/index.ts @@ -0,0 +1 @@ +export { KeypadLoader } from "./KeypadLoader"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.css b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.css new file mode 100644 index 00000000000..828f1e522b9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.css @@ -0,0 +1,123 @@ +.step-progress { + position: relative; + width: 100%; + max-width: 20rem; + height: 1.75rem; + overflow: hidden; + margin: 0 auto; + mask-image: linear-gradient( + to bottom, + transparent, + black 35%, + black 65%, + transparent + ); + -webkit-mask-image: linear-gradient( + to bottom, + transparent, + black 35%, + black 65%, + transparent + ); +} + +.step-progress__list { + position: relative; + width: 100%; + height: 100%; +} + +.step-progress__item { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: + opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.55s cubic-bezier(0.22, 1, 0.36, 1), + color 0.3s ease-out; + will-change: transform, opacity; +} + +.step-progress__icon { + display: grid; + place-items: center; + font-size: 0.875rem; + flex-shrink: 0; +} + +.step-progress__check-stroke { + stroke: var(--background, #fff); +} + +.step-progress__title { + font-size: 0.8125rem; + font-weight: 500; + line-height: 1; + display: inline-flex; + align-items: baseline; +} + +.step-progress__ellipsis { + display: inline-flex; + width: 0.9em; + margin-left: 0.05em; + letter-spacing: 0.05em; +} + +.step-progress__ellipsis-dot { + visibility: hidden; + animation: step-progress-dot-1 1.6s steps(1, end) infinite; +} + +.step-progress__ellipsis-dot:nth-child(2) { + animation-name: step-progress-dot-2; +} + +.step-progress__ellipsis-dot:nth-child(3) { + animation-name: step-progress-dot-3; +} + +@keyframes step-progress-dot-1 { + 0% { + visibility: hidden; + } + 25%, + 100% { + visibility: visible; + } +} + +@keyframes step-progress-dot-2 { + 0%, + 25% { + visibility: hidden; + } + 50%, + 100% { + visibility: visible; + } +} + +@keyframes step-progress-dot-3 { + 0%, + 50% { + visibility: hidden; + } + 75%, + 100% { + visibility: visible; + } +} + +@media (prefers-reduced-motion: reduce) { + .step-progress__item { + transition: none; + } + .step-progress__ellipsis-dot { + animation: none; + visibility: visible; + } +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.tsx new file mode 100644 index 00000000000..6c6d8b0454e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/StepProgress.tsx @@ -0,0 +1,184 @@ +import { cn } from "@superset/ui/utils"; +import { useEffect, useState } from "react"; +import { + getStepIndex, + INIT_STEP_MESSAGES, + INIT_STEP_ORDER, + type WorkspaceInitStep, +} from "shared/types/workspace-init"; +import "./StepProgress.css"; + +const DONE_HOLD_MS = 750; + +const DISPLAY_STEPS: readonly WorkspaceInitStep[] = INIT_STEP_ORDER.filter( + (s) => s !== "ready", +); + +type StepState = "waiting" | "progress" | "done"; + +interface StepProgressProps { + currentStep: WorkspaceInitStep; +} + +export function StepProgress({ currentStep }: StepProgressProps) { + const targetIdx = getStepIndex(currentStep); + const [renderIdx, setRenderIdx] = useState(targetIdx); + const [holdDoneIdx, setHoldDoneIdx] = useState(null); + + useEffect(() => { + if (targetIdx === renderIdx) { + setHoldDoneIdx(null); + return; + } + if (targetIdx < renderIdx) { + setRenderIdx(targetIdx); + setHoldDoneIdx(null); + return; + } + setHoldDoneIdx(renderIdx); + const t = window.setTimeout(() => { + setHoldDoneIdx(null); + setRenderIdx((prev) => Math.min(prev + 1, targetIdx)); + }, DONE_HOLD_MS); + return () => window.clearTimeout(t); + }, [targetIdx, renderIdx]); + + return ( +
+
+ {DISPLAY_STEPS.map((step) => { + const idx = getStepIndex(step); + const distance = idx - renderIdx; + const isHeldDone = holdDoneIdx === idx; + const state: StepState = isHeldDone + ? "done" + : distance < 0 + ? "done" + : distance === 0 + ? "progress" + : "waiting"; + const fade = Math.abs(distance); + + return ( +
+ + + + + {stripEllipsis(INIT_STEP_MESSAGES[step])} + {state === "progress" ? : null} + +
+ ); + })} +
+
+ ); +} + +function stripEllipsis(s: string) { + return s.replace(/[.…]+$/, ""); +} + +function StepIcon({ state }: { state: StepState }) { + if (state === "done") { + return ; + } + if (state === "progress") { + return ; + } + return ; +} + +function CheckCircle() { + return ( + + ); +} + +function EmptyCircle() { + const angles = Array.from({ length: 16 }, (_, i) => (360 / 16) * i); + return ( + + ); +} + +function HalfCircle() { + return ( + + ); +} + +function Ellipsis() { + return ( + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/index.ts new file mode 100644 index 00000000000..18126b94971 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/StepProgress/index.ts @@ -0,0 +1 @@ +export { StepProgress } from "./StepProgress"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/V2WorkspaceLoadingView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/V2WorkspaceLoadingView.tsx new file mode 100644 index 00000000000..806015907ae --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/V2WorkspaceLoadingView.tsx @@ -0,0 +1,68 @@ +import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; +import { + INIT_STEP_ORDER, + type WorkspaceInitStep, +} from "shared/types/workspace-init"; +import { KeypadLoader } from "./KeypadLoader"; +import { StepProgress } from "./StepProgress"; + +interface V2WorkspaceLoadingViewProps { + workspaceName?: string; + title?: string; + description?: string; + children?: ReactNode; + /** + * Drives the keypad directly. When omitted, the loader cycles through + * steps on a short timer so the keys still animate even with no real + * backend progress data (e.g. adopt intent, cold-load route). + */ + currentStep?: WorkspaceInitStep; +} + +const VISIBLE_STEPS: readonly WorkspaceInitStep[] = INIT_STEP_ORDER.filter( + (s) => s !== "ready", +); + +const STEP_INTERVAL_MS = 400; + +export function V2WorkspaceLoadingView({ + workspaceName, + title = "Loading workspace", + description = "Hang tight while we get things ready", + children, + currentStep: currentStepProp, +}: V2WorkspaceLoadingViewProps) { + const [stepIdx, setStepIdx] = useState(0); + + useEffect(() => { + if (currentStepProp !== undefined) return; + const id = window.setInterval(() => { + setStepIdx((prev) => Math.min(prev + 1, VISIBLE_STEPS.length - 1)); + }, STEP_INTERVAL_MS); + return () => window.clearInterval(id); + }, [currentStepProp]); + + const currentStep = currentStepProp ?? VISIBLE_STEPS[stepIdx] ?? "pending"; + + return ( +
+
+ + +
+

{title}

+ {workspaceName ? ( +

{workspaceName}

+ ) : null} +
+ + + +

{description}

+ + {children} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/key-single.png b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/key-single.png new file mode 100644 index 00000000000..251661fa55d Binary files /dev/null and b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/key-single.png differ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/keypad-base.png b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/keypad-base.png new file mode 100644 index 00000000000..b3dc1ddf578 Binary files /dev/null and b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/assets/keypad-base.png differ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/index.ts new file mode 100644 index 00000000000..2ad1ee0046d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/components/V2WorkspaceLoadingView/index.ts @@ -0,0 +1 @@ +export { V2WorkspaceLoadingView } from "./V2WorkspaceLoadingView"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/page.tsx new file mode 100644 index 00000000000..518ce5aa0be --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/page.tsx @@ -0,0 +1,38 @@ +import { eq } from "@tanstack/db"; +import { useLiveQuery } from "@tanstack/react-db"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useEffect } from "react"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { V2WorkspaceLoadingView } from "./components/V2WorkspaceLoadingView"; + +export const Route = createFileRoute( + "/_authenticated/_dashboard/v2-workspace-loading/$workspaceId/", +)({ + component: V2WorkspaceLoadingPage, +}); + +function V2WorkspaceLoadingPage() { + const { workspaceId } = Route.useParams(); + const navigate = useNavigate(); + const collections = useCollections(); + + const { data: workspaces, isReady } = useLiveQuery( + (q) => + q + .from({ v2Workspaces: collections.v2Workspaces }) + .where(({ v2Workspaces }) => eq(v2Workspaces.id, workspaceId)), + [collections, workspaceId], + ); + const workspace = workspaces?.[0] ?? null; + + useEffect(() => { + if (!isReady) return; + void navigate({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId }, + replace: true, + }); + }, [isReady, navigate, workspaceId]); + + return ; +} 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 bff5eaab35f..18d23eb4443 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 @@ -1,5 +1,4 @@ import type { ChatLaunchConfig } from "shared/tabs-types"; -import { SessionSelector } from "./components/SessionSelector"; import { ChatPaneInterface as WorkspaceChatInterface } from "./components/WorkspaceChatInterface"; import { useWorkspaceChatController } from "./hooks/useWorkspaceChatController"; @@ -16,46 +15,24 @@ export function ChatPane({ initialLaunchConfig?: ChatLaunchConfig | null; onConsumeLaunchConfig?: () => void; }) { - const { - organizationId, - workspacePath, - sessionItems, - handleSelectSession, - handleNewChat, - handleDeleteSession, - getOrCreateSession, - } = useWorkspaceChatController({ - onSessionIdChange, - sessionId, - workspaceId, - }); + const { organizationId, workspacePath, handleNewChat, getOrCreateSession } = + useWorkspaceChatController({ + onSessionIdChange, + sessionId, + workspaceId, + }); return ( -
-
- -
- -
- -
-
+ ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx new file mode 100644 index 00000000000..a9dc3377b4f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/ChatPaneTitle.tsx @@ -0,0 +1,53 @@ +import type { RendererContext } from "@superset/panes"; +import { useCallback } from "react"; +import { getV2NotificationSourcesForPane } from "renderer/stores/v2-notifications"; +import { V2NotificationStatusIndicator } from "../../../../../../components/V2NotificationStatusIndicator"; +import type { ChatPaneData, PaneViewerData } from "../../../../../../types"; +import { useWorkspaceChatController } from "../../hooks/useWorkspaceChatController"; +import { SessionSelector } from "../SessionSelector"; + +interface ChatPaneTitleProps { + context: RendererContext; + workspaceId: string; +} + +export function ChatPaneTitle({ context, workspaceId }: ChatPaneTitleProps) { + const data = context.pane.data as ChatPaneData; + const { sessionId } = data; + const { actions } = context; + + const onSessionIdChange = useCallback( + (nextSessionId: string | null) => { + actions.updateData({ ...data, sessionId: nextSessionId }); + }, + [actions, data], + ); + + const { + sessionItems, + handleSelectSession, + handleNewChat, + handleDeleteSession, + } = useWorkspaceChatController({ + workspaceId, + sessionId, + onSessionIdChange, + }); + + return ( +
+ + +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/index.ts new file mode 100644 index 00000000000..30ae09d8f9b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/ChatPaneTitle/index.ts @@ -0,0 +1 @@ +export { ChatPaneTitle } from "./ChatPaneTitle"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx index 625f7d7fd3e..e3d3ee846e0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/ChatPaneInterface.tsx @@ -224,7 +224,6 @@ export function ChatPaneInterface({ getOrCreateSession, onResetSession, onUserMessageSubmitted, - onRawSnapshotChange, }: ChatPaneInterfaceProps) { const { models: availableModels, @@ -529,23 +528,6 @@ export function ChatPaneInterface({ setSubmitStatus(undefined); }, [isRunning]); - useEffect(() => { - onRawSnapshotChange?.({ - sessionId, - isRunning: canAbort, - currentMessage: currentMessage ?? null, - messages: messages ?? [], - error, - }); - }, [ - canAbort, - currentMessage, - error, - messages, - onRawSnapshotChange, - sessionId, - ]); - useEffect(() => { messagesLengthRef.current = messages?.length ?? 0; }, [messages]); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/types.ts index 37b3be8a2f1..6c4f6b99b53 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/components/WorkspaceChatInterface/types.ts @@ -1,14 +1,5 @@ -import type { UseChatDisplayReturn } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/ChatPane/hooks/useWorkspaceChatDisplay"; import type { ChatLaunchConfig } from "shared/tabs-types"; -export interface ChatRawSnapshot { - sessionId: string | null; - isRunning: boolean; - currentMessage: UseChatDisplayReturn["currentMessage"] | null; - messages: UseChatDisplayReturn["messages"]; - error: unknown; -} - export interface ChatPaneInterfaceProps { sessionId: string | null; initialLaunchConfig: ChatLaunchConfig | null; @@ -25,5 +16,4 @@ export interface ChatPaneInterfaceProps { getOrCreateSession: () => Promise; onResetSession: () => Promise; onUserMessageSubmitted?: (message: string) => void; - onRawSnapshotChange?: (snapshot: ChatRawSnapshot) => void; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/CommentPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/CommentPane.tsx index 2ce60b51616..3b247b2551d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/CommentPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/CommentPane.tsx @@ -1,6 +1,5 @@ import { mermaid } from "@streamdown/mermaid"; import type { RendererContext } from "@superset/panes"; -import { Avatar, AvatarFallback, AvatarImage } from "@superset/ui/avatar"; import { type ReactNode, useCallback, @@ -8,7 +7,7 @@ import { useRef, useState, } from "react"; -import { LuCheck, LuCopy } from "react-icons/lu"; +import { LuCheck } from "react-icons/lu"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { @@ -31,84 +30,18 @@ interface CommentPaneProps { export function CommentPane({ context }: CommentPaneProps) { const data = context.pane.data as CommentPaneData; - const [copied, setCopied] = useState(false); - const copyTimerRef = useRef | null>(null); - const isMountedRef = useRef(true); - - useEffect(() => { - return () => { - isMountedRef.current = false; - if (copyTimerRef.current) clearTimeout(copyTimerRef.current); - }; - }, []); - - const handleCopyAll = useCallback(() => { - void electronTrpcClient.external.copyText - .mutate(data.body) - .then(() => { - if (!isMountedRef.current) return; - if (copyTimerRef.current) clearTimeout(copyTimerRef.current); - setCopied(true); - copyTimerRef.current = setTimeout(() => { - if (!isMountedRef.current) return; - setCopied(false); - copyTimerRef.current = null; - }, 1500); - }) - .catch((err) => { - console.warn("Failed to copy comment text", err); - }); - }, [data.body]); return ( -
-
- - {data.avatarUrl ? ( - - ) : null} - - {data.authorLogin.slice(0, 2).toUpperCase()} - - - - {data.authorLogin} - - {data.path && ( - - {data.path} - {data.line != null ? `:${data.line}` : ""} - - )} - -
-
-
- - {data.body} - -
-
+ {data.body} + +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/CommentPaneHeaderExtras.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/CommentPaneHeaderExtras.tsx new file mode 100644 index 00000000000..6aa00db8dbc --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/CommentPaneHeaderExtras.tsx @@ -0,0 +1,87 @@ +import type { RendererContext } from "@superset/panes"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { FaGithub } from "react-icons/fa"; +import { LuCheck, LuCopy } from "react-icons/lu"; +import { electronTrpcClient } from "renderer/lib/trpc-client"; +import type { CommentPaneData, PaneViewerData } from "../../../../../../types"; + +interface CommentPaneHeaderExtrasProps { + context: RendererContext; +} + +export function CommentPaneHeaderExtras({ + context, +}: CommentPaneHeaderExtrasProps) { + const data = context.pane.data as CommentPaneData; + const [copied, setCopied] = useState(false); + const copyTimerRef = useRef | null>(null); + const isMountedRef = useRef(true); + + useEffect(() => { + return () => { + isMountedRef.current = false; + if (copyTimerRef.current) clearTimeout(copyTimerRef.current); + }; + }, []); + + const handleCopyAll = useCallback(() => { + void electronTrpcClient.external.copyText + .mutate(data.body) + .then(() => { + if (!isMountedRef.current) return; + if (copyTimerRef.current) clearTimeout(copyTimerRef.current); + setCopied(true); + copyTimerRef.current = setTimeout(() => { + if (!isMountedRef.current) return; + setCopied(false); + copyTimerRef.current = null; + }, 1500); + }) + .catch((err) => { + console.warn("Failed to copy comment text", err); + }); + }, [data.body]); + + return ( + <> + {data.url && ( + + + + + + + + Open on GitHub + + + )} + + + + + + {copied ? "Copied" : "Copy comment"} + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/index.ts new file mode 100644 index 00000000000..3f6f732e669 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneHeaderExtras/index.ts @@ -0,0 +1 @@ +export { CommentPaneHeaderExtras } from "./CommentPaneHeaderExtras"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/CommentPaneTitle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/CommentPaneTitle.tsx new file mode 100644 index 00000000000..d46141028ee --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/CommentPaneTitle.tsx @@ -0,0 +1,45 @@ +import type { RendererContext } from "@superset/panes"; +import { cn } from "@superset/ui/utils"; +import { MessageSquare } from "lucide-react"; +import type { CommentPaneData, PaneViewerData } from "../../../../../../types"; + +interface CommentPaneTitleProps { + context: RendererContext; +} + +export function CommentPaneTitle({ context }: CommentPaneTitleProps) { + const data = context.pane.data as CommentPaneData; + const { isActive } = context; + + return ( +
+ {data.avatarUrl ? ( + + ) : ( + + )} + + {data.authorLogin} + + {data.path && ( + + {data.path} + {data.line != null ? `:${data.line}` : ""} + + )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/index.ts new file mode 100644 index 00000000000..5b3ca95ae72 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/CommentPane/components/CommentPaneTitle/index.ts @@ -0,0 +1 @@ +export { CommentPaneTitle } from "./CommentPaneTitle"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index 2f3ca1ab9fc..1aaf0a55fae 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -17,10 +17,8 @@ import { TerminalSquare, } from "lucide-react"; import { useMemo } from "react"; -import { FaGithub } from "react-icons/fa"; import { LuArrowDownToLine, - LuArrowUpRight, LuClipboard, LuClipboardCopy, LuEraser, @@ -50,7 +48,10 @@ import type { } from "../../types"; import { BrowserPane, BrowserPaneToolbar } from "./components/BrowserPane"; import { ChatPane } from "./components/ChatPane"; +import { ChatPaneTitle } from "./components/ChatPane/components/ChatPaneTitle"; import { CommentPane } from "./components/CommentPane"; +import { CommentPaneHeaderExtras } from "./components/CommentPane/components/CommentPaneHeaderExtras"; +import { CommentPaneTitle } from "./components/CommentPane/components/CommentPaneTitle"; import { DiffPane } from "./components/DiffPane"; import { FilePane } from "./components/FilePane"; import { FilePaneHeaderExtras } from "./components/FilePane/components/FilePaneHeaderExtras"; @@ -521,21 +522,7 @@ export function usePaneRegistry( getIcon: () => , getTitle: () => "Chat", renderTitle: (ctx: RendererContext) => ( -
- - - Chat - - -
+ ), renderPane: (ctx: RendererContext) => { const data = ctx.pane.data as ChatPaneData; @@ -576,25 +563,15 @@ export function usePaneRegistry( const data = pane.data as CommentPaneData; return data.authorLogin; }, + renderTitle: (ctx: RendererContext) => ( + + ), renderPane: (ctx: RendererContext) => ( ), - renderHeaderExtras: (ctx: RendererContext) => { - const data = ctx.pane.data as CommentPaneData; - if (!data.url) return null; - return ( - - - - - ); - }, + renderHeaderExtras: (ctx: RendererContext) => ( + + ), contextMenuActions: (_ctx, defaults) => defaults.map((d) => d.key === "close-pane" ? { ...d, label: "Close Comment" } : d, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx index 5cd7cbebe16..f087fe18bd5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx @@ -1,7 +1,12 @@ import { buildHostRoutingKey } from "@superset/shared/host-routing"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; -import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; +import { + createFileRoute, + Outlet, + useMatchRoute, + useNavigate, +} from "@tanstack/react-router"; import { useEffect, useRef } from "react"; import { env } from "renderer/env.renderer"; import { @@ -22,6 +27,7 @@ export const Route = createFileRoute("/_authenticated/_dashboard/v2-workspace")( function V2WorkspaceLayout() { const matchRoute = useMatchRoute(); + const navigate = useNavigate(); const workspaceMatch = matchRoute({ to: "/v2-workspace/$workspaceId", }); @@ -67,6 +73,16 @@ function V2WorkspaceLayout() { ensureWorkspaceInSidebar(workspace.id, workspace.projectId); }, [ensureWorkspaceInSidebar, workspace]); + useEffect(() => { + if (workspaceId && !isReady) { + void navigate({ + to: "/v2-workspace-loading/$workspaceId", + params: { workspaceId }, + replace: true, + }); + } + }, [workspaceId, isReady, navigate]); + if (!workspaceId || !isReady) { return null; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx index 9a66ef86583..eebdd85d454 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx @@ -2,12 +2,9 @@ import { ChatRuntimeServiceProvider, ChatServiceProvider, } from "@superset/chat/client"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { CopyIcon } from "lucide-react"; import { useCallback } from "react"; import type { MosaicBranch } from "react-mosaic-component"; import { createChatServiceIpcClient } from "renderer/components/Chat/utils/chat-service-client"; -import { env } from "renderer/env.renderer"; import { electronQueryClient } from "renderer/providers/ElectronTRPCProvider"; import { useTabsStore } from "renderer/stores/tabs/store"; import type { SplitPaneOptions, Tab } from "renderer/stores/tabs/types"; @@ -16,7 +13,6 @@ import { BasePaneWindow, PaneToolbarActions } from "../components"; import { ChatPaneInterface } from "./ChatPaneInterface"; import { SessionSelector } from "./components/SessionSelector"; import { useChatPaneController } from "./hooks/useChatPaneController"; -import { useChatRawSnapshot } from "./hooks/useChatRawSnapshot"; import { createChatRuntimeServiceIpcClient } from "./utils/chat-runtime-service-client"; const chatRuntimeIpcClient = createChatRuntimeServiceIpcClient(); @@ -68,7 +64,6 @@ export function ChatPane({ onMoveToNewTab, onPopOut, }: ChatPaneProps) { - const showDevToolbarActions = env.NODE_ENV === "development"; const isFocused = useTabsStore((s) => s.focusedPaneIds[tabId] === paneId); const equalizePaneSplits = useTabsStore((s) => s.equalizePaneSplits); const paneName = useTabsStore((s) => s.panes[paneId]?.name ?? "New Chat"); @@ -92,11 +87,6 @@ export function ChatPane({ paneId, workspaceId, }); - const { - snapshotAvailableForSession, - handleRawSnapshotChange, - handleCopyRawSnapshot, - } = useChatRawSnapshot({ sessionId }); const applySubmittedMessageFallbackTitle = useCallback( (message: string) => { @@ -171,28 +161,10 @@ export function ChatPane({ onSplitPane={handlers.onSplitPane} onSplitPaneOpposite={handlers.onSplitPaneOpposite} onClosePane={handlers.onClosePane} + // FORK NOTE: v1 ChatPane の onPopOut は維持。upstream + // #3805 で dev raw snapshot copy は ChatPaneInterface + // の onRawSnapshotChange 削除に伴い消えるため取り込み。 onPopOut={handlers.onPopOut} - leadingActions={ - showDevToolbarActions ? ( - - - - - - Copy raw chat JSON (dev) - - - ) : null - } closeHotkeyId="CLOSE_TERMINAL" />
@@ -231,9 +203,6 @@ export function ChatPane({ onStartFreshSession={handleStartFreshSession} onConsumeLaunchConfig={consumeLaunchConfig} onUserMessageSubmitted={applySubmittedMessageFallbackTitle} - onRawSnapshotChange={ - showDevToolbarActions ? handleRawSnapshotChange : undefined - } />
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/ChatPaneInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/ChatPaneInterface.tsx index c7840b9059f..57dc8e2d133 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/ChatPaneInterface.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/ChatPaneInterface.tsx @@ -224,7 +224,6 @@ export function ChatPaneInterface({ onStartFreshSession, onConsumeLaunchConfig, onUserMessageSubmitted, - onRawSnapshotChange, }: ChatPaneInterfaceProps) { const { models: availableModels, @@ -552,23 +551,6 @@ export function ChatPaneInterface({ setSubmitStatus(undefined); }, [isRunning]); - useEffect(() => { - onRawSnapshotChange?.({ - sessionId, - isRunning: canAbort, - currentMessage: currentMessage ?? null, - messages: messages ?? [], - error, - }); - }, [ - canAbort, - currentMessage, - error, - messages, - onRawSnapshotChange, - sessionId, - ]); - useEffect(() => { messagesLengthRef.current = messages?.length ?? 0; }, [messages]); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/types.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/types.ts index b3b8061ad1c..e9bb74c24ba 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/types.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/types.ts @@ -1,15 +1,6 @@ -import type { UseChatDisplayReturn } from "@superset/chat/client"; import type { StartFreshSessionResult } from "renderer/components/Chat/ChatInterface/types"; import type { ChatLaunchConfig } from "shared/tabs-types"; -export interface ChatRawSnapshot { - sessionId: string | null; - isRunning: boolean; - currentMessage: UseChatDisplayReturn["currentMessage"] | null; - messages: UseChatDisplayReturn["messages"]; - error: unknown; -} - export interface ChatPaneInterfaceProps { paneId: string; sessionId: string | null; @@ -23,5 +14,4 @@ export interface ChatPaneInterfaceProps { onStartFreshSession: () => Promise; onConsumeLaunchConfig: () => void; onUserMessageSubmitted?: (message: string) => void; - onRawSnapshotChange?: (snapshot: ChatRawSnapshot) => void; } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/index.ts deleted file mode 100644 index 0c9cdaecc48..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useChatRawSnapshot } from "./useChatRawSnapshot"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/useChatRawSnapshot.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/useChatRawSnapshot.ts deleted file mode 100644 index 37742633a11..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/hooks/useChatRawSnapshot/useChatRawSnapshot.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { toast } from "@superset/ui/sonner"; -import { useCallback, useRef, useState } from "react"; -import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; -import type { ChatRawSnapshot } from "../../ChatPaneInterface/types"; - -interface UseChatRawSnapshotOptions { - sessionId: string | null; -} - -interface UseChatRawSnapshotReturn { - snapshotAvailableForSession: boolean; - handleRawSnapshotChange: (snapshot: ChatRawSnapshot) => void; - handleCopyRawSnapshot: () => void; -} - -export function useChatRawSnapshot({ - sessionId, -}: UseChatRawSnapshotOptions): UseChatRawSnapshotReturn { - const rawSnapshotRef = useRef(null); - const [rawSnapshotSessionId, setRawSnapshotSessionId] = useState< - string | null - >(null); - - const handleRawSnapshotChange = useCallback((snapshot: ChatRawSnapshot) => { - rawSnapshotRef.current = snapshot; - setRawSnapshotSessionId((previousSessionId) => - previousSessionId === snapshot.sessionId - ? previousSessionId - : snapshot.sessionId, - ); - }, []); - - const { copyToClipboard } = useCopyToClipboard(); - - const handleCopyRawSnapshot = useCallback(() => { - const rawSnapshot = rawSnapshotRef.current; - if (!rawSnapshot || rawSnapshot.sessionId !== sessionId) { - toast.error("No raw chat data to copy yet"); - return; - } - - copyToClipboard(JSON.stringify(rawSnapshot, null, 2)); - toast.success("Copied raw chat JSON"); - }, [sessionId, copyToClipboard]); - - return { - snapshotAvailableForSession: - Boolean(rawSnapshotRef.current) && rawSnapshotSessionId === sessionId, - handleRawSnapshotChange, - handleCopyRawSnapshot, - }; -}