@@ -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}
+
+
);
}
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
- }
/>