Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 73 additions & 46 deletions apps/app/src/react-app/domains/session/chat/session-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ import { workspaceSettingsRoute } from "../../../shell/workspace-routes";

import { isElectronRuntime } from "../../../../app/utils";
import { isCollectibleArtifactTarget, isLocalhostBrowserTarget, isOpenableFileTarget, type OpenTarget } from "../artifacts/open-target";
import type { OpenTargetOptions } from "@/lib/target-provider";
import { VoicePanel } from "../voice/voice-panel";
import { DesignPanel } from "../design/design-panel";
import type { OpenTargetOptions } from "@/lib/target-provider";
import { VoicePanel } from "../voice/voice-panel";
import { designAiSelectionToken, type DesignAiSelectionContext } from "@ipollowork/design-studio";
import { useDesignAiSelectionStore } from "../design/design-ai-selection-store";
import { waitForTemplateEntrySurface } from "../templates/template-entry-route";
Expand Down Expand Up @@ -616,10 +615,23 @@ export function SessionPage(props: SessionPageProps) {
));
const sessionPanelState = useSessionPanelState(props.selectedSessionId ?? "");
const activePanelTab = useActivePanelTab(props.selectedSessionId ?? "");
const { workspaceApps } = useInstalledPluginContributions(
const { workspaceApps, nativeWorkspaces, loaded: pluginContributionsLoaded } = useInstalledPluginContributions(
props.ipolloworkServerClient,
props.runtimeWorkspaceId,
);
const designWorkspaceEnabled = !pluginContributionsLoaded
|| nativeWorkspaces.some((workspace) => workspace.kind === "design");
const videoWorkspaceEnabled = !pluginContributionsLoaded
|| nativeWorkspaces.some((workspace) => workspace.kind === "video");
useEffect(() => {
if (!pluginContributionsLoaded || !props.selectedSessionId) return;
for (const tab of sessionPanelState.tabs) {
if ((tab.type === "design" && !designWorkspaceEnabled)
|| (tab.type === "video" && !videoWorkspaceEnabled)) {
closeTab(props.selectedSessionId, tab.id);
}
}
}, [closeTab, designWorkspaceEnabled, pluginContributionsLoaded, props.selectedSessionId, sessionPanelState.tabs, videoWorkspaceEnabled]);
const [hiddenTargetRevision, setHiddenTargetRevision] = useState(0);
const [, setExtensionStateVersion] = useState(0);
const hiddenAccessibleTargetIds = useMemo(
Expand Down Expand Up @@ -775,12 +787,19 @@ export function SessionPage(props: SessionPageProps) {
: undefined,
[artifactCatalogState, artifactContext, artifactScopeKey],
);
const videoOutput = useMemo(() => (
currentVideoEntryPath
? getArtifactsFromMessages(conversationMessages, accessibleTargets, { includeTargetFallbacks: true })
.find((artifact) => artifactPathMatchesTarget(artifact.path, currentVideoEntryPath)) ?? null
: null
), [accessibleTargets, conversationMessages, currentVideoEntryPath]);
const videoOutput = useMemo(() => (
currentVideoEntryPath
? getArtifactsFromMessages(conversationMessages, accessibleTargets, { includeTargetFallbacks: true })
.find((artifact) => artifactPathMatchesTarget(artifact.path, currentVideoEntryPath)) ?? null
: null
), [accessibleTargets, conversationMessages, currentVideoEntryPath]);
const availableStarterTemplateCatalog = useMemo(() => (
pluginContributionsLoaded
? starterTemplateCatalog.filter((template) => (
template.manifest.surface === "design" ? designWorkspaceEnabled : videoWorkspaceEnabled
))
: starterTemplateCatalog
), [designWorkspaceEnabled, pluginContributionsLoaded, starterTemplateCatalog, videoWorkspaceEnabled]);
const autoCollapsedSidebarRef = useRef(false);
const autoCollapsedSidePanelRef = useRef<SessionPanelView | null>(null);
const lastRightPanelViewRef = useRef<SessionPanelView>("launcher");
Expand All @@ -797,14 +816,15 @@ export function SessionPage(props: SessionPageProps) {
const templateBriefDismissed = Boolean(
props.selectedSessionId && dismissedTemplateBriefSessionIds.has(props.selectedSessionId),
);
const activateVideoStudio = useCallback((sessionId: string) => {
// Mark the agent turn as a video task so it receives the session-owned
const activateVideoStudio = useCallback((sessionId: string) => {
if (!videoWorkspaceEnabled) return;
// Mark the agent turn as a video task so it receives the session-owned
// project contract. The Studio itself opens only after an output exists.
setSessionType(sessionId, "video");
setSessionTypeRevision((value) => value + 1);
}, []);
}, [videoWorkspaceEnabled]);
const openCurrentVideoStudio = useCallback((options?: { auto?: boolean }) => {
if (!props.selectedSessionId) return;
if (!props.selectedSessionId || !videoWorkspaceEnabled) return;
if (!options?.auto) prioritizeRightPanel();
const videoTabId = `video:${props.selectedSessionId}`;
openTab(props.selectedSessionId, {
Expand All @@ -814,7 +834,7 @@ export function SessionPage(props: SessionPageProps) {
sessionId: props.selectedSessionId,
});
setSidePanelState(props.selectedSessionId, "panel");
}, [openTab, prioritizeRightPanel, props.selectedSessionId, selectedSessionTitle, setSidePanelState]);
}, [openTab, prioritizeRightPanel, props.selectedSessionId, selectedSessionTitle, setSidePanelState, videoWorkspaceEnabled]);
const refreshTemplateCatalog = useCallback(async () => {
if (!props.ipolloworkServerClient || !props.runtimeWorkspaceId) return;
const requestId = ++templateCatalogRequestIdRef.current;
Expand Down Expand Up @@ -1308,8 +1328,9 @@ export function SessionPage(props: SessionPageProps) {
const [mainWorkspaceView, setMainWorkspaceView] = useState<"extensions" | null>(null);
const preserveSidePanelOnPanelOpenRef = useRef(false);

const setCurrentSidePanel = useCallback((panel: SidePanelItem | null) => {
if (panel === "design" && props.selectedSessionId) {
const setCurrentSidePanel = useCallback((panel: SidePanelItem | null) => {
if (panel === "design" && props.selectedSessionId) {
if (!designWorkspaceEnabled) return;
const entryPath = designTemplateEntryPath?.replaceAll("\\", "/").trim() || "";
const designTabId = entryPath
? `design:${props.selectedSessionId}:${encodeURIComponent(entryPath)}`
Expand Down Expand Up @@ -1337,10 +1358,10 @@ export function SessionPage(props: SessionPageProps) {
setSidePanelState(GLOBAL_VOICE_SIDE_PANEL_KEY, panel === "voice" ? "voice" : null);
if (panel === "voice") return;
setSidePanelState(props.selectedSessionId, panel);
}, [designTemplateEntryPath, openTab, props.selectedSessionId, selectTab, sessionPanelState.tabs, setSidePanelState]);
const openDesignTab = useCallback((path?: string) => {
if (!props.selectedSessionId) return;
}, [designTemplateEntryPath, designWorkspaceEnabled, openTab, props.selectedSessionId, selectTab, sessionPanelState.tabs, setSidePanelState]);

const openDesignTab = useCallback((path?: string) => {
if (!props.selectedSessionId || !designWorkspaceEnabled) return;
const normalizedPath = path?.replaceAll("\\", "/").trim() || designTemplateEntryPath?.replaceAll("\\", "/").trim() || "";
if (!normalizedPath) {
setCurrentSidePanel("panel");
Expand All @@ -1363,7 +1384,7 @@ export function SessionPage(props: SessionPageProps) {
selectTab(props.selectedSessionId, designTabId);
}
setCurrentSidePanel("panel");
}, [designTemplateEntryPath, openTab, props.selectedSessionId, selectTab, sessionPanelState.tabs, setCurrentSidePanel]);
}, [designTemplateEntryPath, designWorkspaceEnabled, openTab, props.selectedSessionId, selectTab, sessionPanelState.tabs, setCurrentSidePanel]);

useEffect(() => {
if (!props.selectedSessionId || !designTemplateEntryPath) return;
Expand Down Expand Up @@ -2194,14 +2215,14 @@ export function SessionPage(props: SessionPageProps) {
onClick: addBrowserPanelTab,
disabled: !isElectronRuntime(),
},
{
id: "design",
label: "Design",
iconSrc: publicAssetUrl("sidebar-entry-code.svg"),
active: panelRailActive && activePanelTab?.type === "design",
onClick: showDesignRailPane,
disabled: !props.selectedSessionId || props.selectedWorkspaceDisplay.workspaceType === "remote",
},
...(designWorkspaceEnabled ? [{
id: "design",
label: "Design",
iconSrc: publicAssetUrl("sidebar-entry-code.svg"),
active: panelRailActive && activePanelTab?.type === "design",
onClick: showDesignRailPane,
disabled: !props.selectedSessionId || props.selectedWorkspaceDisplay.workspaceType === "remote",
}] : []),
{
id: "files",
label: t("session.side_panel.files"),
Expand All @@ -2211,14 +2232,14 @@ export function SessionPage(props: SessionPageProps) {
onClick: showArtifactRailPane,
disabled: !hasArtifactTargets,
},
{
id: "video",
label: t("session.side_panel.video"),
iconSrc: publicAssetUrl("sidebar-entry-video.svg"),
active: videoRailActive,
...(videoWorkspaceEnabled ? [{
id: "video",
label: t("session.side_panel.video"),
iconSrc: publicAssetUrl("sidebar-entry-video.svg"),
active: videoRailActive,
onClick: showVideoRailPane,
disabled: !props.selectedSessionId || props.selectedWorkspaceDisplay.workspaceType === "remote",
},
}] : []),
...workspaceApps.map((surface) => ({
id: `workspace-app:${surface.id}`,
label: surface.label,
Expand All @@ -2229,7 +2250,7 @@ export function SessionPage(props: SessionPageProps) {
onClick: () => openWorkspaceApp(surface),
disabled: !props.selectedSessionId,
})),
], [activePanelTab, addBrowserPanelTab, hasArtifactTargets, locale, openWorkspaceApp, panelRailActive, props.selectedSessionId, props.selectedWorkspaceDisplay.workspaceType, showArtifactRailPane, showDesignRailPane, showVideoRailPane, videoRailActive, workspaceApps]);
], [activePanelTab, addBrowserPanelTab, designWorkspaceEnabled, hasArtifactTargets, locale, openWorkspaceApp, panelRailActive, props.selectedSessionId, props.selectedWorkspaceDisplay.workspaceType, showArtifactRailPane, showDesignRailPane, showVideoRailPane, videoRailActive, videoWorkspaceEnabled, workspaceApps]);
const removeAccessibleTarget = useCallback((target: OpenTarget) => {
const nextHiddenIds = new Set(hiddenAccessibleTargetIds);
nextHiddenIds.add(target.id);
Expand Down Expand Up @@ -2838,9 +2859,9 @@ export function SessionPage(props: SessionPageProps) {
{mainWorkspaceView === null && !showDelayedSessionLoadingState && canRenderReactSurface ? (
<div className="flex h-full min-h-0 flex-col lg:flex-row">
<div className="min-h-0 min-w-0 flex-1">
{isDesignSession && templateSessionLoading ? (
<div className="flex h-full items-center justify-center gap-2 text-sm text-dls-secondary"><LoaderCircle className="size-4 animate-spin" />{t("templates.preparing")}</div>
) : isDesignSession && !hasTemplateSession && props.ipolloworkServerClient && props.runtimeWorkspaceId ? (
{isDesignSession && designWorkspaceEnabled && templateSessionLoading ? (
<div className="flex h-full items-center justify-center gap-2 text-sm text-dls-secondary"><LoaderCircle className="size-4 animate-spin" />{t("templates.preparing")}</div>
) : isDesignSession && designWorkspaceEnabled && !hasTemplateSession && props.ipolloworkServerClient && props.runtimeWorkspaceId ? (
<DesignStarter
client={props.ipolloworkServerClient}
workspaceId={props.runtimeWorkspaceId}
Expand Down Expand Up @@ -2890,15 +2911,17 @@ export function SessionPage(props: SessionPageProps) {
? pendingVideoArtifactCompletion.requirement
: undefined}
onArtifactCompletionRequirementConsumed={consumePendingVideoArtifactCompletion}
onOpenVideoStudio={openCurrentVideoStudio}
onOpenVideoStudio={videoWorkspaceEnabled ? openCurrentVideoStudio : undefined}
onCreateSession={(type, templateId) => props.sidebar.onCreateTaskInWorkspace(
props.selectedWorkspaceId,
type,
templateId,
PERSONAL_WORK_CONTEXT_ID,
)}
onMaterializeTemplate={async (templateId, surface) => {
if (!props.ipolloworkServerClient || !props.runtimeWorkspaceId || !props.selectedSessionId) return;
onMaterializeTemplate={async (templateId, surface) => {
if ((surface === "design" && !designWorkspaceEnabled)
|| (surface === "video" && !videoWorkspaceEnabled)) return;
if (!props.ipolloworkServerClient || !props.runtimeWorkspaceId || !props.selectedSessionId) return;
const result = await props.ipolloworkServerClient.materializeTemplate(
props.runtimeWorkspaceId,
templateId,
Expand Down Expand Up @@ -2928,8 +2951,8 @@ export function SessionPage(props: SessionPageProps) {
}
openCurrentVideoStudio();
}}
onActivateVideoStudio={activateVideoStudio}
designTemplates={starterTemplateCatalog}
onActivateVideoStudio={videoWorkspaceEnabled ? activateVideoStudio : undefined}
designTemplates={availableStarterTemplateCatalog}
designTemplatesLoading={starterTemplateCatalogLoading}
designTemplateBusyId={templateBusyId}
onInstallDesignTemplate={(templateId) => void installStarterTemplate(templateId)}
Expand Down Expand Up @@ -3103,13 +3126,17 @@ export function SessionPage(props: SessionPageProps) {
left: shellConfig.sidebar && sidebarOpen ? `${effectiveLeftSidebarWidth}px` : "0",
} : undefined}
>
<SidePanel
<SidePanel
sessionId={props.selectedSessionId}
client={props.ipolloworkServerClient}
workspaceId={props.runtimeWorkspaceId}
workspaceRoot={props.selectedWorkspaceRoot}
isRemoteWorkspace={props.surface?.isRemoteWorkspace ?? false}
launcherItems={sidePanelLauncherItems}
enabledNativeWorkspaces={[
...(designWorkspaceEnabled ? ["design" as const] : []),
...(videoWorkspaceEnabled ? ["video" as const] : []),
]}
aiEditing={isStreamingSessionStatus(props.sidebar.sessionStatusById[props.selectedSessionId])}
onAskAi={handleDesignAskAi}
onSendWorkspaceAppMessage={sendWorkspaceAppMessage}
Expand Down
9 changes: 7 additions & 2 deletions apps/app/src/react-app/domains/session/panel/side-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { DesignPanel } from "../design/design-panel";
import type { DesignAiSelectionContext } from "@ipollowork/design-studio";
import { VideoPanel } from "../video/video-panel";
import { WorkspaceAppFrame, type WorkspaceAppModelContext } from "@/react-app/plugin-ui/workspace-app-frame";
import type { PluginNativeWorkspace } from "@/react-app/plugin-ui/plugin-ui-contributions";
import {
computeBounds,
getElectronBrowser,
Expand All @@ -60,6 +61,7 @@ type SidePanelProps = {
workspaceRoot: string;
isRemoteWorkspace?: boolean;
launcherItems?: SidePanelLauncherItem[];
enabledNativeWorkspaces?: PluginNativeWorkspace["kind"][];
onClose: () => void;
onAskAi?: (context: DesignAiSelectionContext) => void;
onSendWorkspaceAppMessage?: (input: { text: string; modelContext: WorkspaceAppModelContext | null }) => boolean | Promise<boolean>;
Expand Down Expand Up @@ -460,6 +462,7 @@ export function SidePanel({
workspaceRoot,
isRemoteWorkspace = false,
launcherItems = [],
enabledNativeWorkspaces = ["design", "video"],
onAskAi,
onSendWorkspaceAppMessage,
onSaveAsTemplate,
Expand All @@ -471,6 +474,8 @@ export function SidePanel({
}: SidePanelProps) {
const { tabs } = useSessionPanelState(sessionId);
const activeTab = useActivePanelTab(sessionId);
const designEnabled = enabledNativeWorkspaces.includes("design");
const videoEnabled = enabledNativeWorkspaces.includes("video");
const isBrowserAvailable = Boolean(getElectronBrowser());

const { createTab, closeTab, selectTab, reorderTabs } = useSidePanelTabs(sessionId);
Expand Down Expand Up @@ -715,7 +720,7 @@ export function SidePanel({
{!activeTab ? (
<PanelEmpty />
) : null}
{activeTab?.type === "design" ? (
{activeTab?.type === "design" && designEnabled ? (
<DesignPanelErrorBoundary resetKey={`${activeTab.id}:${activeTab.path}`}>
<DesignPanel
sessionId={activeTab.sessionId}
Expand All @@ -728,7 +733,7 @@ export function SidePanel({
onSaveAsTemplate={onSaveAsTemplate}
/>
</DesignPanelErrorBoundary>
) : activeTab?.type === "video" ? (
) : activeTab?.type === "video" && videoEnabled ? (
<VideoPanel
key={activeTab.id}
title={activeTab.label}
Expand Down
12 changes: 12 additions & 0 deletions apps/app/src/react-app/domains/settings/plugin-packages-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,18 @@ export function PluginPackagesPanel(props: PluginPackagesPanelProps) {
...capability,
type: `${binding.engine}/${capability.kind}`,
}))) ?? [],
...item.manifest.contributions?.flatMap((contribution) => (
contribution.type === "session-side-panel"
&& contribution.location === "session-right-pane"
&& contribution.ref
? [{
id: contribution.ref,
type: "workspace",
label: contribution.label,
description: contribution.description,
}]
: []
)) ?? [],
];
const skillResources = item.manifest.resources.filter((resource) => resource.type === "skill");
const relatedSkillNames = item.manifest.relatedSkills ?? [];
Expand Down
Loading
Loading