feat(web): connect mobile workspace to live data
This commit is contained in:
@@ -5,8 +5,9 @@ import {
|
|||||||
MobileWorkListScreen,
|
MobileWorkListScreen,
|
||||||
MobileWorkUnitDetailScreen,
|
MobileWorkUnitDetailScreen,
|
||||||
} from "@code/ui/components/mobile-product";
|
} 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";
|
import { useMobileWorkspace } from "@/hooks/use-mobile-workspace";
|
||||||
|
|
||||||
export type MobileFlowScreen =
|
export type MobileFlowScreen =
|
||||||
@@ -21,29 +22,41 @@ interface MobileFlowPageProps {
|
|||||||
|
|
||||||
export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const workspace = useMobileWorkspace();
|
const { workUnitId } = useParams();
|
||||||
const homePath = "/";
|
const projectWorkspace = useMobileProjectWorkspace({
|
||||||
|
selectedWorkUnitId: workUnitId,
|
||||||
|
});
|
||||||
|
const workspace = useMobileWorkspace({
|
||||||
|
onSend: projectWorkspace.sendMessage,
|
||||||
|
});
|
||||||
const workPath = "/work";
|
const workPath = "/work";
|
||||||
const chatPath = "/chat";
|
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) {
|
if (screen === "work-list" && !workspace.expanded) {
|
||||||
workspace.setExpanded(true);
|
workspace.setExpanded(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate(detailPath);
|
navigate(`/work/${targetId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const screenProps = {
|
const screenProps = {
|
||||||
composerValue: workspace.composerValue,
|
composerValue: workspace.composerValue,
|
||||||
onBack: () => navigate(homePath),
|
data: projectWorkspace.data,
|
||||||
|
onBack: () => navigate(workPath),
|
||||||
onComposerChange: workspace.handleComposerChange,
|
onComposerChange: workspace.handleComposerChange,
|
||||||
onComposerSubmit: workspace.handleComposerSubmit,
|
onComposerSubmit: workspace.handleComposerSubmit,
|
||||||
|
onManageProjects: () => navigate("/dashboard"),
|
||||||
onOpenAssistant: () => navigate(chatPath),
|
onOpenAssistant: () => navigate(chatPath),
|
||||||
onOpenUnit: handleOpenUnit,
|
onOpenUnit: handleOpenUnit,
|
||||||
onViewWork: () => navigate(workPath),
|
onViewWork: () => navigate(workPath),
|
||||||
statusMessage: workspace.statusMessage,
|
statusMessage: workspace.statusMessage ?? projectWorkspace.error?.message,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (screen === "assistant-chat") {
|
if (screen === "assistant-chat") {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useFlueAgent } from "@flue/react";
|
import { useFlueAgent } from "@flue/react";
|
||||||
|
|
||||||
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
|
import { usePersonalOrganization } from "@/hooks/use-personal-organization";
|
||||||
|
import type { PersonalOrganizationState } from "@/hooks/use-personal-organization";
|
||||||
import { CHAT_AGENT } from "@/lib/chat/constants";
|
import { CHAT_AGENT } from "@/lib/chat/constants";
|
||||||
import type { ChatAgentState } from "@/lib/chat/types";
|
import type { ChatAgentState } from "@/lib/chat/types";
|
||||||
|
|
||||||
export const useChatAgent = (): ChatAgentState => {
|
export const useOrganizationChatAgent = (
|
||||||
// The agent session is not created until the authenticated user's personal
|
organization: PersonalOrganizationState
|
||||||
// organization has been ensured and its id is available.
|
): ChatAgentState => {
|
||||||
const organization = usePersonalOrganization();
|
|
||||||
const agent = useFlueAgent({
|
const agent = useFlueAgent({
|
||||||
...CHAT_AGENT,
|
...CHAT_AGENT,
|
||||||
id: organization.organizationId,
|
id: organization.organizationId,
|
||||||
@@ -36,3 +36,6 @@ export const useChatAgent = (): ChatAgentState => {
|
|||||||
status,
|
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 type { FormEvent } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const useMobileWorkspace = (initialExpanded = false) => {
|
interface UseMobileWorkspaceOptions {
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
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 [composerValue, setComposerValue] = useState("");
|
||||||
const [expanded, setExpanded] = useState(initialExpanded);
|
const [expanded, setExpanded] = useState(initialExpanded);
|
||||||
const [statusMessage, setStatusMessage] = useState<string>();
|
const [statusMessage, setStatusMessage] = useState<string>();
|
||||||
@@ -18,15 +28,22 @@ export const useMobileWorkspace = (initialExpanded = false) => {
|
|||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setComposerValue("");
|
setStatusMessage("Sending to Zopu…");
|
||||||
setStatusMessage("Added to Zopu’s queue");
|
const sendMessage = async () => {
|
||||||
|
try {
|
||||||
|
await onSend(message);
|
||||||
|
setComposerValue("");
|
||||||
|
setStatusMessage("Sent to Zopu");
|
||||||
|
} catch (error) {
|
||||||
|
setStatusMessage(errorMessage(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void sendMessage();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeIndex,
|
|
||||||
composerValue,
|
composerValue,
|
||||||
expanded,
|
expanded,
|
||||||
handleActiveIndexChange: setActiveIndex,
|
|
||||||
handleComposerChange,
|
handleComposerChange,
|
||||||
handleComposerSubmit,
|
handleComposerSubmit,
|
||||||
setExpanded,
|
setExpanded,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
|
|||||||
export interface PersonalOrganizationState {
|
export interface PersonalOrganizationState {
|
||||||
readonly error?: Error;
|
readonly error?: Error;
|
||||||
readonly organizationId?: Id<"organizations">;
|
readonly organizationId?: Id<"organizations">;
|
||||||
|
readonly organizationName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure the authenticated user has its personal tenancy boundary. */
|
/** Ensure the authenticated user has its personal tenancy boundary. */
|
||||||
@@ -16,10 +17,12 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
|||||||
const ensurePersonalOrganization = useMutation(
|
const ensurePersonalOrganization = useMutation(
|
||||||
api.organizations.ensurePersonalOrganization
|
api.organizations.ensurePersonalOrganization
|
||||||
);
|
);
|
||||||
const [organizationId, setOrganizationId] = useState<
|
const [state, setState] = useState<{
|
||||||
Id<"organizations"> | undefined
|
readonly error?: Error;
|
||||||
>();
|
readonly organizationId?: Id<"organizations">;
|
||||||
const [error, setError] = useState<Error | undefined>();
|
readonly organizationName?: string;
|
||||||
|
readonly userId: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -31,16 +34,21 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
|||||||
try {
|
try {
|
||||||
const organization = await ensurePersonalOrganization();
|
const organization = await ensurePersonalOrganization();
|
||||||
if (active) {
|
if (active) {
|
||||||
setOrganizationId(organization._id);
|
setState({
|
||||||
setError(undefined);
|
organizationId: organization._id,
|
||||||
|
organizationName: organization.name,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (caughtError: unknown) {
|
} catch (caughtError: unknown) {
|
||||||
if (active) {
|
if (active) {
|
||||||
setError(
|
setState({
|
||||||
caughtError instanceof Error
|
error:
|
||||||
? caughtError
|
caughtError instanceof Error
|
||||||
: new Error(String(caughtError))
|
? caughtError
|
||||||
);
|
: new Error(String(caughtError)),
|
||||||
|
userId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -51,9 +59,13 @@ export const usePersonalOrganization = (): PersonalOrganizationState => {
|
|||||||
};
|
};
|
||||||
}, [ensurePersonalOrganization, userId]);
|
}, [ensurePersonalOrganization, userId]);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId || state?.userId !== userId) {
|
||||||
return {};
|
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 artifactModel from "../artifactModel.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
import type * as authz from "../authz.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 daemonCommands from "../daemonCommands.js";
|
||||||
import type * as daemonRuntime from "../daemonRuntime.js";
|
import type * as daemonRuntime from "../daemonRuntime.js";
|
||||||
import type * as daemons from "../daemons.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 privateData from "../privateData.js";
|
||||||
import type * as projectArtifacts from "../projectArtifacts.js";
|
import type * as projectArtifacts from "../projectArtifacts.js";
|
||||||
import type * as projectIssues from "../projectIssues.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 projects from "../projects.js";
|
||||||
|
import type * as signals from "../signals.js";
|
||||||
import type * as todos from "../todos.js";
|
import type * as todos from "../todos.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -36,6 +39,7 @@ declare const fullApi: ApiFromModules<{
|
|||||||
artifactModel: typeof artifactModel;
|
artifactModel: typeof artifactModel;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
authz: typeof authz;
|
authz: typeof authz;
|
||||||
|
conversationMessages: typeof conversationMessages;
|
||||||
daemonCommands: typeof daemonCommands;
|
daemonCommands: typeof daemonCommands;
|
||||||
daemonRuntime: typeof daemonRuntime;
|
daemonRuntime: typeof daemonRuntime;
|
||||||
daemons: typeof daemons;
|
daemons: typeof daemons;
|
||||||
@@ -46,7 +50,9 @@ declare const fullApi: ApiFromModules<{
|
|||||||
privateData: typeof privateData;
|
privateData: typeof privateData;
|
||||||
projectArtifacts: typeof projectArtifacts;
|
projectArtifacts: typeof projectArtifacts;
|
||||||
projectIssues: typeof projectIssues;
|
projectIssues: typeof projectIssues;
|
||||||
|
projectStore: typeof projectStore;
|
||||||
projects: typeof projects;
|
projects: typeof projects;
|
||||||
|
signals: typeof signals;
|
||||||
todos: typeof todos;
|
todos: typeof todos;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,12 @@ export {
|
|||||||
MobileWorkListScreen,
|
MobileWorkListScreen,
|
||||||
MobileWorkUnitDetailScreen,
|
MobileWorkUnitDetailScreen,
|
||||||
} from "./mobile-workspace/index";
|
} 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 { MobileHomeScreen } from "./mobile-home-screen";
|
||||||
export { MobileWorkListScreen } from "./mobile-work-list-screen";
|
export { MobileWorkListScreen } from "./mobile-work-list-screen";
|
||||||
export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-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,
|
MobileWorkspaceRendererProps,
|
||||||
"variant"
|
"variant"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
MobileAssistantMessageView,
|
||||||
|
MobileAssistantView,
|
||||||
|
MobileSignalView,
|
||||||
|
MobileWorkspaceView,
|
||||||
|
MobileWorkUnitTone,
|
||||||
|
MobileWorkUnitView,
|
||||||
|
} from "./models";
|
||||||
|
|||||||
Reference in New Issue
Block a user