feat(web): connect mobile workspace to live data
This commit is contained in:
@@ -5,8 +5,9 @@ import {
|
||||
MobileWorkListScreen,
|
||||
MobileWorkUnitDetailScreen,
|
||||
} from "@code/ui/components/mobile-product";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
|
||||
import { useMobileProjectWorkspace } from "@/hooks/use-mobile-project-workspace";
|
||||
import { useMobileWorkspace } from "@/hooks/use-mobile-workspace";
|
||||
|
||||
export type MobileFlowScreen =
|
||||
@@ -21,29 +22,41 @@ interface MobileFlowPageProps {
|
||||
|
||||
export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
const workspace = useMobileWorkspace();
|
||||
const homePath = "/";
|
||||
const { workUnitId } = useParams();
|
||||
const projectWorkspace = useMobileProjectWorkspace({
|
||||
selectedWorkUnitId: workUnitId,
|
||||
});
|
||||
const workspace = useMobileWorkspace({
|
||||
onSend: projectWorkspace.sendMessage,
|
||||
});
|
||||
const workPath = "/work";
|
||||
const chatPath = "/chat";
|
||||
const detailPath = "/work/flow-08";
|
||||
|
||||
const handleOpenUnit = () => {
|
||||
const handleOpenUnit = (selectedWorkUnitId?: string) => {
|
||||
const targetId =
|
||||
selectedWorkUnitId ?? projectWorkspace.data.selectedWorkUnit?.id;
|
||||
if (!targetId) {
|
||||
navigate("/dashboard");
|
||||
return;
|
||||
}
|
||||
if (screen === "work-list" && !workspace.expanded) {
|
||||
workspace.setExpanded(true);
|
||||
return;
|
||||
}
|
||||
navigate(detailPath);
|
||||
navigate(`/work/${targetId}`);
|
||||
};
|
||||
|
||||
const screenProps = {
|
||||
composerValue: workspace.composerValue,
|
||||
onBack: () => navigate(homePath),
|
||||
data: projectWorkspace.data,
|
||||
onBack: () => navigate(workPath),
|
||||
onComposerChange: workspace.handleComposerChange,
|
||||
onComposerSubmit: workspace.handleComposerSubmit,
|
||||
onManageProjects: () => navigate("/dashboard"),
|
||||
onOpenAssistant: () => navigate(chatPath),
|
||||
onOpenUnit: handleOpenUnit,
|
||||
onViewWork: () => navigate(workPath),
|
||||
statusMessage: workspace.statusMessage,
|
||||
statusMessage: workspace.statusMessage ?? projectWorkspace.error?.message,
|
||||
};
|
||||
|
||||
if (screen === "assistant-chat") {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useFlueAgent } from "@flue/react";
|
||||
|
||||
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
|
||||
import type { PersonalOrganizationState } from "@/hooks/use-personal-organization";
|
||||
import { CHAT_AGENT } from "@/lib/chat/constants";
|
||||
import type { ChatAgentState } from "@/lib/chat/types";
|
||||
|
||||
export const useChatAgent = (): ChatAgentState => {
|
||||
// The agent session is not created until the authenticated user's personal
|
||||
// organization has been ensured and its id is available.
|
||||
const organization = usePersonalOrganization();
|
||||
export const useOrganizationChatAgent = (
|
||||
organization: PersonalOrganizationState
|
||||
): ChatAgentState => {
|
||||
const agent = useFlueAgent({
|
||||
...CHAT_AGENT,
|
||||
id: organization.organizationId,
|
||||
@@ -36,3 +36,6 @@ export const useChatAgent = (): ChatAgentState => {
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
export const useChatAgent = (): ChatAgentState =>
|
||||
useOrganizationChatAgent(usePersonalOrganization());
|
||||
|
||||
83
apps/web/src/hooks/use-mobile-project-workspace.ts
Normal file
83
apps/web/src/hooks/use-mobile-project-workspace.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { api } from "@code/backend/convex/_generated/api";
|
||||
import type {
|
||||
MobileAssistantMessageView,
|
||||
MobileAssistantView,
|
||||
} from "@code/ui/components/mobile-product";
|
||||
import { useQuery } from "convex/react";
|
||||
|
||||
import { useOrganizationChatAgent } from "@/hooks/chat/use-chat-agent";
|
||||
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
|
||||
import { useProjectWorkspace } from "@/hooks/use-project-workspace";
|
||||
import { STATUS_COPY } from "@/lib/chat/constants";
|
||||
import { getMessageText } from "@/lib/chat/transforms";
|
||||
import { buildMobileWorkspaceView } from "@/lib/mobile-workspace/build-mobile-workspace-view";
|
||||
|
||||
const toAssistantMessages = (
|
||||
messages: ReturnType<typeof useOrganizationChatAgent>["messages"]
|
||||
): MobileAssistantMessageView[] =>
|
||||
messages.flatMap((message) => {
|
||||
if (message.role !== "assistant" && message.role !== "user") {
|
||||
return [];
|
||||
}
|
||||
const text = getMessageText(message);
|
||||
return text
|
||||
? [
|
||||
{
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
text,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
|
||||
const getStatusTone = (
|
||||
status: ReturnType<typeof useOrganizationChatAgent>["status"]
|
||||
): MobileAssistantView["statusTone"] => {
|
||||
if (status === "error") {
|
||||
return "red";
|
||||
}
|
||||
return status === "connecting" ? "amber" : "green";
|
||||
};
|
||||
|
||||
interface UseMobileProjectWorkspaceOptions {
|
||||
readonly selectedWorkUnitId?: string;
|
||||
}
|
||||
|
||||
export const useMobileProjectWorkspace = ({
|
||||
selectedWorkUnitId,
|
||||
}: UseMobileProjectWorkspaceOptions) => {
|
||||
const organization = usePersonalOrganization();
|
||||
const projectWorkspace = useProjectWorkspace();
|
||||
const chatAgent = useOrganizationChatAgent(organization);
|
||||
const signals = useQuery(
|
||||
api.signals.list,
|
||||
organization.organizationId
|
||||
? { organizationId: organization.organizationId }
|
||||
: "skip"
|
||||
);
|
||||
const assistant: MobileAssistantView = {
|
||||
isBusy:
|
||||
chatAgent.status === "connecting" ||
|
||||
chatAgent.status === "streaming" ||
|
||||
chatAgent.status === "submitted",
|
||||
messages: toAssistantMessages(chatAgent.messages),
|
||||
statusLabel: STATUS_COPY[chatAgent.status],
|
||||
statusTone: getStatusTone(chatAgent.status),
|
||||
};
|
||||
|
||||
return {
|
||||
data: buildMobileWorkspaceView({
|
||||
artifacts: projectWorkspace.artifacts,
|
||||
assistant,
|
||||
issues: projectWorkspace.issues,
|
||||
organizationLabel: organization.organizationName,
|
||||
projects: projectWorkspace.projects,
|
||||
selectedProject: projectWorkspace.selectedProject,
|
||||
selectedWorkUnitId,
|
||||
signals,
|
||||
}),
|
||||
error: organization.error ?? chatAgent.error,
|
||||
sendMessage: chatAgent.sendMessage,
|
||||
} as const;
|
||||
};
|
||||
@@ -1,8 +1,18 @@
|
||||
import type { FormEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useMobileWorkspace = (initialExpanded = false) => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
interface UseMobileWorkspaceOptions {
|
||||
readonly initialExpanded?: boolean;
|
||||
readonly onSend: (message: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const errorMessage = (error: unknown) =>
|
||||
error instanceof Error ? error.message : "Message could not be sent";
|
||||
|
||||
export const useMobileWorkspace = ({
|
||||
initialExpanded = false,
|
||||
onSend,
|
||||
}: UseMobileWorkspaceOptions) => {
|
||||
const [composerValue, setComposerValue] = useState("");
|
||||
const [expanded, setExpanded] = useState(initialExpanded);
|
||||
const [statusMessage, setStatusMessage] = useState<string>();
|
||||
@@ -18,15 +28,22 @@ export const useMobileWorkspace = (initialExpanded = false) => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
setComposerValue("");
|
||||
setStatusMessage("Added to Zopu’s queue");
|
||||
setStatusMessage("Sending to Zopu…");
|
||||
const sendMessage = async () => {
|
||||
try {
|
||||
await onSend(message);
|
||||
setComposerValue("");
|
||||
setStatusMessage("Sent to Zopu");
|
||||
} catch (error) {
|
||||
setStatusMessage(errorMessage(error));
|
||||
}
|
||||
};
|
||||
void sendMessage();
|
||||
};
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
composerValue,
|
||||
expanded,
|
||||
handleActiveIndexChange: setActiveIndex,
|
||||
handleComposerChange,
|
||||
handleComposerSubmit,
|
||||
setExpanded,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
|
||||
export interface PersonalOrganizationState {
|
||||
readonly error?: Error;
|
||||
readonly organizationId?: Id<"organizations">;
|
||||
readonly organizationName?: string;
|
||||
}
|
||||
|
||||
/** Ensure the authenticated user has its personal tenancy boundary. */
|
||||
@@ -16,10 +17,12 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
||||
const ensurePersonalOrganization = useMutation(
|
||||
api.organizations.ensurePersonalOrganization
|
||||
);
|
||||
const [organizationId, setOrganizationId] = useState<
|
||||
Id<"organizations"> | undefined
|
||||
>();
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const [state, setState] = useState<{
|
||||
readonly error?: Error;
|
||||
readonly organizationId?: Id<"organizations">;
|
||||
readonly organizationName?: string;
|
||||
readonly userId: string;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
@@ -31,16 +34,21 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
||||
try {
|
||||
const organization = await ensurePersonalOrganization();
|
||||
if (active) {
|
||||
setOrganizationId(organization._id);
|
||||
setError(undefined);
|
||||
setState({
|
||||
organizationId: organization._id,
|
||||
organizationName: organization.name,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
} catch (caughtError: unknown) {
|
||||
if (active) {
|
||||
setError(
|
||||
caughtError instanceof Error
|
||||
? caughtError
|
||||
: new Error(String(caughtError))
|
||||
);
|
||||
setState({
|
||||
error:
|
||||
caughtError instanceof Error
|
||||
? caughtError
|
||||
: new Error(String(caughtError)),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -51,9 +59,13 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
||||
};
|
||||
}, [ensurePersonalOrganization, userId]);
|
||||
|
||||
if (!userId) {
|
||||
if (!userId || state?.userId !== userId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { error, organizationId };
|
||||
return {
|
||||
error: state.error,
|
||||
organizationId: state.organizationId,
|
||||
organizationName: state.organizationName,
|
||||
};
|
||||
};
|
||||
|
||||
164
apps/web/src/lib/mobile-workspace/build-mobile-workspace-view.ts
Normal file
164
apps/web/src/lib/mobile-workspace/build-mobile-workspace-view.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { api } from "@code/backend/convex/_generated/api";
|
||||
import type { Doc } from "@code/backend/convex/_generated/dataModel";
|
||||
import type {
|
||||
MobileAssistantView,
|
||||
MobileSignalView,
|
||||
MobileWorkspaceView,
|
||||
MobileWorkUnitTone,
|
||||
MobileWorkUnitView,
|
||||
} from "@code/ui/components/mobile-product";
|
||||
import type { FunctionReturnType } from "convex/server";
|
||||
|
||||
type SignalList = FunctionReturnType<typeof api.signals.list>;
|
||||
type ProjectView = FunctionReturnType<typeof api.projects.list>[number];
|
||||
|
||||
interface BuildMobileWorkspaceViewInput {
|
||||
readonly artifacts: readonly Doc<"projectArtifacts">[] | undefined;
|
||||
readonly assistant: MobileAssistantView;
|
||||
readonly issues: readonly Doc<"projectIssues">[] | undefined;
|
||||
readonly organizationLabel?: string;
|
||||
readonly projects: readonly ProjectView[] | undefined;
|
||||
readonly selectedProject: ProjectView | null;
|
||||
readonly selectedWorkUnitId?: string;
|
||||
readonly signals: SignalList | undefined;
|
||||
}
|
||||
|
||||
const updatedDateFormatter = new Intl.DateTimeFormat("en", {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
});
|
||||
|
||||
const statusPresentation: Record<
|
||||
Doc<"projectIssues">["status"],
|
||||
{
|
||||
readonly nextAction: string;
|
||||
readonly progress: number;
|
||||
readonly statusLabel: string;
|
||||
readonly tone: MobileWorkUnitTone;
|
||||
}
|
||||
> = {
|
||||
completed: {
|
||||
nextAction: "Review completed work",
|
||||
progress: 100,
|
||||
statusLabel: "Ready for review",
|
||||
tone: "green",
|
||||
},
|
||||
failed: {
|
||||
nextAction: "Retry this work unit",
|
||||
progress: 45,
|
||||
statusLabel: "Blocked",
|
||||
tone: "orange",
|
||||
},
|
||||
"needs-input": {
|
||||
nextAction: "Add the missing context",
|
||||
progress: 55,
|
||||
statusLabel: "Needs you",
|
||||
tone: "orange",
|
||||
},
|
||||
open: {
|
||||
nextAction: "Start this work unit",
|
||||
progress: 25,
|
||||
statusLabel: "Researching",
|
||||
tone: "purple",
|
||||
},
|
||||
queued: {
|
||||
nextAction: "Zopu is preparing",
|
||||
progress: 40,
|
||||
statusLabel: "Queued",
|
||||
tone: "blue",
|
||||
},
|
||||
working: {
|
||||
nextAction: "Review current progress",
|
||||
progress: 68,
|
||||
statusLabel: "In progress",
|
||||
tone: "blue",
|
||||
},
|
||||
};
|
||||
|
||||
const toWorkUnit = (
|
||||
issue: Doc<"projectIssues">,
|
||||
artifactCount: number
|
||||
): MobileWorkUnitView => {
|
||||
const presentation = statusPresentation[issue.status];
|
||||
return {
|
||||
artifactCount,
|
||||
code: `#${issue.number}`,
|
||||
id: issue._id,
|
||||
nextAction: presentation.nextAction,
|
||||
progress: presentation.progress,
|
||||
statusLabel: presentation.statusLabel,
|
||||
summary: issue.body,
|
||||
title: issue.title,
|
||||
tone: presentation.tone,
|
||||
updatedLabel: updatedDateFormatter.format(issue.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
const toLatestSignalView = (
|
||||
signal: SignalList[number]["signal"] | undefined
|
||||
): MobileSignalView | undefined => {
|
||||
if (!signal) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
desiredOutcome: signal.problemStatement.desiredOutcome,
|
||||
id: signal._id,
|
||||
summary: signal.problemStatement.summary,
|
||||
title: signal.problemStatement.title,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildMobileWorkspaceView = ({
|
||||
artifacts,
|
||||
assistant,
|
||||
issues,
|
||||
organizationLabel = "Personal workspace",
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedWorkUnitId,
|
||||
signals,
|
||||
}: BuildMobileWorkspaceViewInput): MobileWorkspaceView => {
|
||||
const artifactCount = artifacts?.length ?? 0;
|
||||
const workUnits =
|
||||
issues?.map((issue) => toWorkUnit(issue, artifactCount)) ?? [];
|
||||
const selectedWorkUnit =
|
||||
workUnits.find((workUnit) => workUnit.id === selectedWorkUnitId) ??
|
||||
workUnits.find((workUnit) => workUnit.tone === "blue") ??
|
||||
workUnits[0];
|
||||
const selectedProjectId = selectedProject?.id;
|
||||
const relevantSignals =
|
||||
signals?.filter(
|
||||
({ signal }) =>
|
||||
!signal.projectId ||
|
||||
String(signal.projectId) === String(selectedProjectId)
|
||||
) ?? [];
|
||||
const latestSignal = relevantSignals[0]?.signal;
|
||||
const activeCount = workUnits.filter(
|
||||
(workUnit) => workUnit.progress < 100
|
||||
).length;
|
||||
const needsAttentionCount = issues?.filter(
|
||||
(issue) => issue.status === "failed" || issue.status === "needs-input"
|
||||
).length;
|
||||
const shippedCount = issues?.filter(
|
||||
(issue) => issue.status === "completed"
|
||||
).length;
|
||||
const hasSelectedProject = Boolean(selectedProject);
|
||||
|
||||
return {
|
||||
activeCount,
|
||||
activityCount: workUnits.length + relevantSignals.length,
|
||||
artifactCount,
|
||||
assistant,
|
||||
isLoading:
|
||||
projects === undefined ||
|
||||
(hasSelectedProject && (artifacts === undefined || issues === undefined)),
|
||||
latestSignal: toLatestSignalView(latestSignal),
|
||||
needsAttentionCount: needsAttentionCount ?? 0,
|
||||
organizationLabel,
|
||||
projectName: selectedProject?.name,
|
||||
selectedWorkUnit,
|
||||
shippedCount: shippedCount ?? 0,
|
||||
totalCount: workUnits.length,
|
||||
workUnits,
|
||||
};
|
||||
};
|
||||
6
packages/backend/convex/_generated/api.d.ts
vendored
6
packages/backend/convex/_generated/api.d.ts
vendored
@@ -12,6 +12,7 @@ import type * as agentWorkspace from "../agentWorkspace.js";
|
||||
import type * as artifactModel from "../artifactModel.js";
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as authz from "../authz.js";
|
||||
import type * as conversationMessages from "../conversationMessages.js";
|
||||
import type * as daemonCommands from "../daemonCommands.js";
|
||||
import type * as daemonRuntime from "../daemonRuntime.js";
|
||||
import type * as daemons from "../daemons.js";
|
||||
@@ -22,7 +23,9 @@ import type * as organizations from "../organizations.js";
|
||||
import type * as privateData from "../privateData.js";
|
||||
import type * as projectArtifacts from "../projectArtifacts.js";
|
||||
import type * as projectIssues from "../projectIssues.js";
|
||||
import type * as projectStore from "../projectStore.js";
|
||||
import type * as projects from "../projects.js";
|
||||
import type * as signals from "../signals.js";
|
||||
import type * as todos from "../todos.js";
|
||||
|
||||
import type {
|
||||
@@ -36,6 +39,7 @@ declare const fullApi: ApiFromModules<{
|
||||
artifactModel: typeof artifactModel;
|
||||
auth: typeof auth;
|
||||
authz: typeof authz;
|
||||
conversationMessages: typeof conversationMessages;
|
||||
daemonCommands: typeof daemonCommands;
|
||||
daemonRuntime: typeof daemonRuntime;
|
||||
daemons: typeof daemons;
|
||||
@@ -46,7 +50,9 @@ declare const fullApi: ApiFromModules<{
|
||||
privateData: typeof privateData;
|
||||
projectArtifacts: typeof projectArtifacts;
|
||||
projectIssues: typeof projectIssues;
|
||||
projectStore: typeof projectStore;
|
||||
projects: typeof projects;
|
||||
signals: typeof signals;
|
||||
todos: typeof todos;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -5,4 +5,12 @@ export {
|
||||
MobileWorkListScreen,
|
||||
MobileWorkUnitDetailScreen,
|
||||
} from "./mobile-workspace/index";
|
||||
export type { MobileWorkspaceScreenProps } from "./mobile-workspace/index";
|
||||
export type {
|
||||
MobileAssistantMessageView,
|
||||
MobileAssistantView,
|
||||
MobileSignalView,
|
||||
MobileWorkspaceScreenProps,
|
||||
MobileWorkspaceView,
|
||||
MobileWorkUnitTone,
|
||||
MobileWorkUnitView,
|
||||
} from "./mobile-workspace/index";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,12 @@ export { MobileExpandedWorkScreen } from "./mobile-expanded-work-screen";
|
||||
export { MobileHomeScreen } from "./mobile-home-screen";
|
||||
export { MobileWorkListScreen } from "./mobile-work-list-screen";
|
||||
export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-screen";
|
||||
export type { MobileWorkspaceScreenProps } from "./types";
|
||||
export type {
|
||||
MobileAssistantMessageView,
|
||||
MobileAssistantView,
|
||||
MobileSignalView,
|
||||
MobileWorkspaceScreenProps,
|
||||
MobileWorkspaceView,
|
||||
MobileWorkUnitTone,
|
||||
MobileWorkUnitView,
|
||||
} from "./types";
|
||||
|
||||
50
packages/ui/src/components/mobile-workspace/models.ts
Normal file
50
packages/ui/src/components/mobile-workspace/models.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export type MobileWorkUnitTone = "blue" | "green" | "orange" | "purple";
|
||||
|
||||
export interface MobileWorkUnitView {
|
||||
readonly artifactCount: number;
|
||||
readonly code: string;
|
||||
readonly id: string;
|
||||
readonly nextAction: string;
|
||||
readonly progress: number;
|
||||
readonly statusLabel: string;
|
||||
readonly summary: string;
|
||||
readonly title: string;
|
||||
readonly tone: MobileWorkUnitTone;
|
||||
readonly updatedLabel: string;
|
||||
}
|
||||
|
||||
export interface MobileSignalView {
|
||||
readonly desiredOutcome: string;
|
||||
readonly id: string;
|
||||
readonly summary: string;
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
export interface MobileAssistantMessageView {
|
||||
readonly id: string;
|
||||
readonly role: "assistant" | "user";
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
export interface MobileAssistantView {
|
||||
readonly isBusy: boolean;
|
||||
readonly messages: readonly MobileAssistantMessageView[];
|
||||
readonly statusLabel: string;
|
||||
readonly statusTone: "amber" | "green" | "red";
|
||||
}
|
||||
|
||||
export interface MobileWorkspaceView {
|
||||
readonly activeCount: number;
|
||||
readonly activityCount: number;
|
||||
readonly artifactCount: number;
|
||||
readonly assistant: MobileAssistantView;
|
||||
readonly isLoading: boolean;
|
||||
readonly latestSignal?: MobileSignalView;
|
||||
readonly needsAttentionCount: number;
|
||||
readonly organizationLabel: string;
|
||||
readonly projectName?: string;
|
||||
readonly selectedWorkUnit?: MobileWorkUnitView;
|
||||
readonly shippedCount: number;
|
||||
readonly totalCount: number;
|
||||
readonly workUnits: readonly MobileWorkUnitView[];
|
||||
}
|
||||
@@ -4,3 +4,12 @@ export type MobileWorkspaceScreenProps = Omit<
|
||||
MobileWorkspaceRendererProps,
|
||||
"variant"
|
||||
>;
|
||||
|
||||
export type {
|
||||
MobileAssistantMessageView,
|
||||
MobileAssistantView,
|
||||
MobileSignalView,
|
||||
MobileWorkspaceView,
|
||||
MobileWorkUnitTone,
|
||||
MobileWorkUnitView,
|
||||
} from "./models";
|
||||
|
||||
Reference in New Issue
Block a user