feat: wire mobile workspace into project loop

This commit is contained in:
-Puter
2026-07-24 03:17:59 +05:30
parent 9d54470774
commit fb438ba9a7
16 changed files with 780 additions and 85 deletions

View File

@@ -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,10 @@ 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 publicGit from "../publicGit.js";
import type * as signals from "../signals.js";
import type * as todos from "../todos.js";
import type {
@@ -36,6 +40,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 +51,10 @@ declare const fullApi: ApiFromModules<{
privateData: typeof privateData;
projectArtifacts: typeof projectArtifacts;
projectIssues: typeof projectIssues;
projectStore: typeof projectStore;
projects: typeof projects;
publicGit: typeof publicGit;
signals: typeof signals;
todos: typeof todos;
}>;

View File

@@ -0,0 +1,125 @@
import {
CONTEXT_KINDS,
contextPathForKind,
MAX_CONTEXT_DOCUMENT_CHARACTERS,
type PreparedPublicGitSource,
type PublicGitImportResult,
type RepositoryContextDocument,
} from "@code/primitives/project";
const GITHUB_HOST = "github.com";
const REQUEST_HEADERS = {
Accept: "application/vnd.github+json",
"User-Agent": "zopu-project-importer",
} as const;
const candidatePaths = {
agents: ["AGENTS.md"],
business: ["business.md", "docs/business.md"],
design: ["design.md", "docs/design.md"],
product: ["product.md", "docs/product.md"],
readme: ["README.md", "README", "readme.md"],
tech: ["tech.md", "docs/tech.md"],
} as const;
const encodePath = (path: string) =>
path
.split("/")
.map((segment) => encodeURIComponent(segment))
.join("/");
const fetchTextCandidate = async (
repositoryPath: string,
branch: string,
path: string
): Promise<string | null> => {
const url = `https://raw.githubusercontent.com/${encodePath(repositoryPath)}/${encodePath(branch)}/${encodePath(path)}`;
const response = await fetch(url, { headers: REQUEST_HEADERS });
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`GitHub returned ${response.status} for ${path}`);
}
const content = await response.text();
if (content.trim().length === 0) {
return null;
}
if (content.length > MAX_CONTEXT_DOCUMENT_CHARACTERS) {
throw new Error(`${path} exceeds the 200000 character import limit`);
}
return content;
};
const fetchContextDocument = async (
source: PreparedPublicGitSource,
branch: string,
kind: (typeof CONTEXT_KINDS)[number]
): Promise<RepositoryContextDocument | null> => {
for (const candidate of candidatePaths[kind]) {
const content = await fetchTextCandidate(
source.repositoryPath,
branch,
candidate
);
if (content) {
return {
content,
kind,
path: contextPathForKind(kind),
};
}
}
return null;
};
const readDefaultBranch = async (
source: PreparedPublicGitSource
): Promise<string> => {
if (source.host !== GITHUB_HOST) {
throw new Error("Repository import currently supports public GitHub URLs");
}
const response = await fetch(
`https://api.github.com/repos/${encodePath(source.repositoryPath)}`,
{ headers: REQUEST_HEADERS }
);
if (!response.ok) {
throw new Error(
response.status === 404
? "Public GitHub repository not found"
: `GitHub returned ${response.status} while reading the repository`
);
}
const metadata = (await response.json()) as { default_branch?: unknown };
if (
typeof metadata.default_branch !== "string" ||
metadata.default_branch.length === 0
) {
throw new Error("GitHub did not return a default branch");
}
return metadata.default_branch;
};
export const inspectPublicGit = async (
source: PreparedPublicGitSource
): Promise<PublicGitImportResult> => {
const defaultBranch = await readDefaultBranch(source);
const candidates = await Promise.all(
CONTEXT_KINDS.map((kind) =>
fetchContextDocument(source, defaultBranch, kind)
)
);
const documents = candidates.filter(
(document): document is RepositoryContextDocument => document !== null
);
return {
defaultBranch,
documents,
warnings: CONTEXT_KINDS.filter(
(kind) => !documents.some((document) => document.kind === kind)
).map((kind) => ({
message: "Canonical context file was not found in the repository",
path: contextPathForKind(kind),
})),
};
};

View File

@@ -1,6 +1,10 @@
import { describe, expect, test } from "vitest";
import { getProjectWorkProgress, getProjectWorkStatus } from "./project-work";
import {
getProjectWorkNextAction,
getProjectWorkProgress,
getProjectWorkStatus,
} from "./project-work";
describe("project work state mapping", () => {
test("maps issue and run statuses to the same product language", () => {
@@ -27,4 +31,14 @@ describe("project work state mapping", () => {
getProjectWorkProgress("completed")
);
});
test("maps each state to the next product action", () => {
expect(getProjectWorkNextAction("open")).toBe("Start the project manager");
expect(getProjectWorkNextAction("failed")).toBe(
"Review the failure and retry"
);
expect(getProjectWorkNextAction("completed")).toBe(
"Review the pull request"
);
});
});

View File

@@ -41,6 +41,18 @@ export interface ProjectWorkStatus {
readonly verification: ProjectVerificationStatus;
}
const NEXT_ACTION_MAP: Readonly<
Record<ProjectIssueStatus | ProjectRunStatus, string>
> = {
completed: "Review the pull request",
failed: "Review the failure and retry",
"needs-input": "Resolve the open question",
open: "Start the project manager",
queued: "Watch the run progress",
ready: "Watch the run progress",
working: "Wait for verification",
};
const STATUS_MAP: Readonly<
Record<ProjectIssueStatus | ProjectRunStatus, ProjectWorkStatus>
> = {
@@ -113,3 +125,7 @@ export const getProjectWorkProgress = (
}
}
};
export const getProjectWorkNextAction = (
status: ProjectIssueStatus | ProjectRunStatus
): string => NEXT_ACTION_MAP[status];

View File

@@ -8,6 +8,7 @@ export {
export type {
MobileAssistantMessageView,
MobileAssistantView,
MobileProjectView,
MobileSignalView,
MobileWorkspaceScreenProps,
MobileWorkspaceView,

View File

@@ -4,8 +4,10 @@ import {
ArrowUp,
Check,
ChevronRight,
ExternalLink,
FileText,
ListChecks,
LoaderCircle,
Mic,
MoreHorizontal,
Paperclip,
@@ -16,6 +18,7 @@ import {
import type { FormEvent, ReactNode } from "react";
import type {
MobileProjectView,
MobileWorkspaceView,
MobileWorkUnitTone,
MobileWorkUnitView,
@@ -30,14 +33,24 @@ export type MobileWorkspaceVariant =
export interface MobileWorkspaceRendererProps {
composerValue: string;
createIssueBody: string;
createIssueTitle: string;
data: MobileWorkspaceView;
onBack?: () => void;
onComposerChange: (value: string) => void;
onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
onCreateBodyChange: (value: string) => void;
onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
onCreateIssueFromSignal?: () => void;
onCreateTitleChange: (value: string) => void;
onManageProjects?: () => void;
onOpenAssistant?: () => void;
onOpenUnit?: (workUnitId?: string) => void;
onProjectSelect?: (projectId: string) => void;
onReviewUnit?: (reviewUrl: string) => void;
onStartUnit?: (workUnitId: string) => void;
onViewWork?: () => void;
pendingAction?: string | null;
statusMessage?: string;
variant: MobileWorkspaceVariant;
}
@@ -203,6 +216,142 @@ const ReferenceComposer = ({
</footer>
);
interface CreateWorkFormProps {
readonly body: string;
readonly onBodyChange: (value: string) => void;
readonly onSubmit: (event: FormEvent<HTMLFormElement>) => void;
readonly onTitleChange: (value: string) => void;
readonly pending: boolean;
readonly title: string;
}
const CreateWorkForm = ({
body,
onBodyChange,
onSubmit,
onTitleChange,
pending,
title,
}: CreateWorkFormProps) => (
<form className="mt-4 rounded-[22px] bg-white p-3" onSubmit={onSubmit}>
<div className="flex items-center">
<p className="text-[10px] font-semibold uppercase text-[#807b76]">
Create project work
</p>
{pending ? (
<LoaderCircle className="ml-auto size-3 animate-spin" />
) : null}
</div>
<input
aria-label="Work title"
className="mt-2 h-9 w-full rounded-[12px] bg-[#f2f3ef] px-3 text-[13px] outline-none placeholder:text-[#b3afab]"
disabled={pending}
onChange={(event) => onTitleChange(event.target.value)}
placeholder="Outcome or issue title"
required
value={title}
/>
<textarea
aria-label="Work description"
className="mt-2 min-h-16 w-full resize-none rounded-[12px] bg-[#f2f3ef] px-3 py-2 text-[12px] leading-4 outline-none placeholder:text-[#b3afab]"
disabled={pending}
onChange={(event) => onBodyChange(event.target.value)}
placeholder="Describe the desired result or constraints"
required
value={body}
/>
<button
className="mt-2 h-9 w-full rounded-[12px] bg-black text-[12px] font-semibold text-white disabled:opacity-50"
disabled={pending || !title.trim() || !body.trim()}
type="submit"
>
{pending ? "Creating…" : "Create work"}
</button>
</form>
);
const ProjectSwitcher = ({
data,
onManageProjects,
onProjectSelect,
}: {
readonly data: MobileWorkspaceView;
readonly onManageProjects?: () => void;
readonly onProjectSelect?: (projectId: string) => void;
}) => (
<div className="flex items-center gap-2 border-b border-[#eff0ec] bg-white px-4 py-2">
<label className="sr-only" htmlFor="mobile-project-selector">
Active project
</label>
<select
className="min-w-0 flex-1 bg-transparent text-[12px] font-semibold outline-none"
id="mobile-project-selector"
onChange={(event) => onProjectSelect?.(event.target.value)}
value={data.selectedProjectId ?? ""}
>
{data.projects.length === 0 ? (
<option value="">No connected projects</option>
) : (
data.projects.map((project: MobileProjectView) => (
<option key={project.id} value={project.id}>
{project.name} {project.connected ? "" : "(not connected)"}
</option>
))
)}
</select>
<button
aria-label="Manage projects"
className="flex size-8 items-center justify-center rounded-full bg-[#f2f3ef]"
onClick={onManageProjects}
type="button"
>
<MoreHorizontal className="size-4" />
</button>
</div>
);
const WorkUnitActions = ({
onReview,
onStart,
pending,
workUnit,
}: {
readonly onReview?: (reviewUrl: string) => void;
readonly onStart?: (workUnitId: string) => void;
readonly pending: boolean;
readonly workUnit?: MobileWorkUnitView;
}) => {
if (!workUnit) {
return null;
}
return (
<div className="mt-3 flex gap-2">
{workUnit.canStart || workUnit.canRetry ? (
<button
className="flex h-9 flex-1 items-center justify-center gap-2 rounded-[12px] bg-black text-[12px] font-semibold text-white disabled:opacity-50"
disabled={pending}
onClick={() => onStart?.(workUnit.id)}
type="button"
>
{pending ? <LoaderCircle className="size-3 animate-spin" /> : null}
{workUnit.canRetry ? "Retry work" : "Start work"}
</button>
) : null}
{workUnit.reviewUrl ? (
<button
className="flex h-9 flex-1 items-center justify-center gap-2 rounded-[12px] bg-[#c8ff00] text-[12px] font-semibold text-black"
onClick={() => onReview?.(workUnit.reviewUrl as string)}
type="button"
>
<ExternalLink className="size-3.5" />
Review PR
{workUnit.pullRequestNumber ? ` #${workUnit.pullRequestNumber}` : ""}
</button>
) : null}
</div>
);
};
const workUnitToneClass: Record<MobileWorkUnitTone, string> = {
blue: "bg-[#e8efff] text-[#14265f]",
green: "bg-[#e8f7ef] text-[#105c35]",
@@ -250,20 +399,38 @@ const EmptyWorkState = ({
);
const WorkFeedScreen = ({
createIssueBody,
createIssueTitle,
data,
onChange,
onCreateBodyChange,
onCreateIssue,
onCreateTitleChange,
onManageProjects,
onOpenAssistant,
onOpen,
onProjectSelect,
onReviewUnit,
onStartUnit,
onSubmit,
onViewWork,
pendingAction,
statusMessage,
value,
}: ComposerProps & {
createIssueBody: string;
createIssueTitle: string;
data: MobileWorkspaceView;
onCreateBodyChange: (value: string) => void;
onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
onCreateTitleChange: (value: string) => void;
onManageProjects?: () => void;
onOpen?: (workUnitId?: string) => void;
onOpenAssistant?: () => void;
onProjectSelect?: (projectId: string) => void;
onReviewUnit?: (reviewUrl: string) => void;
onStartUnit?: (workUnitId: string) => void;
pendingAction?: string | null;
onViewWork?: () => void;
}) => {
const primaryWorkUnit = data.selectedWorkUnit;
@@ -278,6 +445,11 @@ const WorkFeedScreen = ({
onManageProjects={onManageProjects}
subtitle={`${data.activeCount} active work units`}
/>
<ProjectSwitcher
data={data}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
/>
<main className="min-h-0 flex-1 bg-[#f7f8f5] px-4 pt-4">
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
Good morning.
@@ -372,6 +544,12 @@ const WorkFeedScreen = ({
Open unit
</button>
</div>
<WorkUnitActions
onReview={onReviewUnit}
onStart={onStartUnit}
pending={pendingAction === `issue:${primaryWorkUnit.id}`}
workUnit={primaryWorkUnit}
/>
</article>
) : (
<EmptyWorkState
@@ -379,6 +557,14 @@ const WorkFeedScreen = ({
onManageProjects={onManageProjects}
/>
)}
<CreateWorkForm
body={createIssueBody}
onBodyChange={onCreateBodyChange}
onSubmit={onCreateIssue}
onTitleChange={onCreateTitleChange}
pending={pendingAction === "issue"}
title={createIssueTitle}
/>
{secondaryWorkUnits[0] ? (
<article
className={cn(
@@ -592,9 +778,15 @@ const ExpandedWorkUnitActivity = ({
const ExpandedWorkUnitContent = ({
compact = false,
data,
onReview,
onStart,
pendingAction,
}: {
compact?: boolean;
data: MobileWorkspaceView;
onReview?: (reviewUrl: string) => void;
onStart?: (workUnitId: string) => void;
pendingAction?: string | null;
}) => {
const workUnit = data.selectedWorkUnit;
const title = getExpandedTitle(workUnit, data.isLoading);
@@ -689,22 +881,46 @@ const ExpandedWorkUnitContent = ({
data={data}
workUnit={workUnit}
/>
<WorkUnitActions
onReview={onReview}
onStart={onStart}
pending={pendingAction === `issue:${workUnit?.id}`}
workUnit={workUnit}
/>
</article>
);
};
const WorkUnitScreen = ({
createIssueBody,
createIssueTitle,
data,
onBack,
onChange,
onCreateBodyChange,
onCreateIssue,
onCreateTitleChange,
onManageProjects,
onSubmit,
onProjectSelect,
onReviewUnit,
onStartUnit,
pendingAction,
statusMessage,
value,
}: ComposerProps & {
createIssueBody: string;
createIssueTitle: string;
data: MobileWorkspaceView;
onCreateBodyChange: (value: string) => void;
onCreateIssue: (event: FormEvent<HTMLFormElement>) => void;
onCreateTitleChange: (value: string) => void;
onBack?: () => void;
onManageProjects?: () => void;
onProjectSelect?: (projectId: string) => void;
onReviewUnit?: (reviewUrl: string) => void;
onStartUnit?: (workUnitId: string) => void;
pendingAction?: string | null;
}) => (
<DeviceFrame402>
<header className="flex h-20 shrink-0 items-center bg-white px-4">
@@ -733,9 +949,27 @@ const WorkUnitScreen = ({
<MoreHorizontal className="size-5" />
</button>
</header>
<ProjectSwitcher
data={data}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
/>
<main className="min-h-0 flex-1 bg-[#f7f8f5] px-[17px] pt-4">
<ExpandedWorkUnitContent data={data} />
<ExpandedWorkUnitContent
data={data}
onReview={onReviewUnit}
onStart={onStartUnit}
pendingAction={pendingAction}
/>
</main>
<CreateWorkForm
body={createIssueBody}
onBodyChange={onCreateBodyChange}
onSubmit={onCreateIssue}
onTitleChange={onCreateTitleChange}
pending={pendingAction === "issue"}
title={createIssueTitle}
/>
<ReferenceComposer
contextLabel={
data.selectedWorkUnit?.title.toUpperCase() ?? "PROJECT WORK"
@@ -756,6 +990,7 @@ const VerticalStack = ({
onChange,
onManageProjects,
onOpen,
onProjectSelect,
onSubmit,
statusMessage,
value,
@@ -763,6 +998,7 @@ const VerticalStack = ({
data: MobileWorkspaceView;
onManageProjects?: () => void;
onOpen?: (workUnitId?: string) => void;
onProjectSelect?: (projectId: string) => void;
}) => {
const { selectedWorkUnit } = data;
const secondaryWorkUnits = data.workUnits
@@ -783,6 +1019,11 @@ const VerticalStack = ({
profileInset
subtitle={`${data.activeCount} active work units`}
/>
<ProjectSwitcher
data={data}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
/>
<main className="relative min-h-0 flex-1 bg-[#f7f8f5] px-5 pt-8">
<div className="flex items-center">
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
@@ -908,6 +1149,7 @@ const HomeExpanded = ({
onChange,
onManageProjects,
onOpen,
onProjectSelect,
onSubmit,
statusMessage,
value,
@@ -915,6 +1157,7 @@ const HomeExpanded = ({
data: MobileWorkspaceView;
onManageProjects?: () => void;
onOpen?: (workUnitId?: string) => void;
onProjectSelect?: (projectId: string) => void;
}) => (
<DeviceFrame402>
<ProductHeader
@@ -924,6 +1167,11 @@ const HomeExpanded = ({
profileInset
subtitle={`${data.activeCount} active work units`}
/>
<ProjectSwitcher
data={data}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
/>
<main className="relative min-h-0 flex-1 bg-[#f7f8f5] px-5 pt-[27px]">
<div className="flex items-center">
<h2 className="text-[29px] leading-9 font-semibold tracking-[-0.05em]">
@@ -979,10 +1227,14 @@ const assistantStatusDotClass: Record<
const CharacterChat = ({
data,
onChange,
onCreateIssueFromSignal,
onSubmit,
statusMessage,
value,
}: ComposerProps & { data: MobileWorkspaceView }) => (
}: ComposerProps & {
data: MobileWorkspaceView;
onCreateIssueFromSignal?: () => void;
}) => (
<div className="mx-auto h-[min(100dvh,858px)] w-full max-w-[390px] overflow-hidden bg-black">
<main className="relative h-full overflow-hidden rounded-[30px] bg-white px-4 text-[#11110f]">
<header className="absolute inset-x-4 top-8 flex items-center">
@@ -1063,6 +1315,13 @@ const CharacterChat = ({
<p className="mt-3 text-[13px] leading-[18px]">
{data.latestSignal.summary}
</p>
<button
className="mt-3 h-8 rounded-[11px] bg-black px-3 text-[11px] font-semibold text-white"
onClick={onCreateIssueFromSignal}
type="button"
>
Create work from signal
</button>
</section>
) : null}
</>
@@ -1140,14 +1399,24 @@ const CharacterChat = ({
export const MobileWorkspaceScreenRenderer = ({
composerValue,
createIssueBody,
createIssueTitle,
data,
onBack,
onComposerChange,
onComposerSubmit,
onCreateBodyChange,
onCreateIssue,
onCreateIssueFromSignal,
onCreateTitleChange,
onManageProjects,
onOpenAssistant,
onOpenUnit,
onProjectSelect,
onReviewUnit,
onStartUnit,
onViewWork,
pendingAction,
statusMessage,
variant,
}: MobileWorkspaceRendererProps) => {
@@ -1160,7 +1429,13 @@ export const MobileWorkspaceScreenRenderer = ({
};
if (variant === "character-pass") {
return <CharacterChat {...composerProps} data={data} />;
return (
<CharacterChat
{...composerProps}
data={data}
onCreateIssueFromSignal={onCreateIssueFromSignal}
/>
);
}
if (variant === "expanded-work-unit") {
return (
@@ -1168,7 +1443,16 @@ export const MobileWorkspaceScreenRenderer = ({
{...composerProps}
data={data}
onBack={onBack}
createIssueBody={createIssueBody}
createIssueTitle={createIssueTitle}
onCreateBodyChange={onCreateBodyChange}
onCreateIssue={onCreateIssue}
onCreateTitleChange={onCreateTitleChange}
onManageProjects={onManageProjects}
onProjectSelect={onProjectSelect}
onReviewUnit={onReviewUnit}
onStartUnit={onStartUnit}
pendingAction={pendingAction}
/>
);
}
@@ -1179,6 +1463,7 @@ export const MobileWorkspaceScreenRenderer = ({
data={data}
onManageProjects={onManageProjects}
onOpen={onOpenUnit}
onProjectSelect={onProjectSelect}
/>
);
}
@@ -1189,6 +1474,7 @@ export const MobileWorkspaceScreenRenderer = ({
data={data}
onManageProjects={onManageProjects}
onOpen={onOpenUnit}
onProjectSelect={onProjectSelect}
/>
);
}
@@ -1200,6 +1486,15 @@ export const MobileWorkspaceScreenRenderer = ({
onManageProjects={onManageProjects}
onOpen={onOpenUnit}
onOpenAssistant={onOpenAssistant}
onProjectSelect={onProjectSelect}
onReviewUnit={onReviewUnit}
onStartUnit={onStartUnit}
createIssueBody={createIssueBody}
createIssueTitle={createIssueTitle}
onCreateBodyChange={onCreateBodyChange}
onCreateIssue={onCreateIssue}
onCreateTitleChange={onCreateTitleChange}
pendingAction={pendingAction}
onViewWork={onViewWork}
/>
);

View File

@@ -6,6 +6,7 @@ export { MobileWorkUnitDetailScreen } from "./mobile-work-unit-detail-screen";
export type {
MobileAssistantMessageView,
MobileAssistantView,
MobileProjectView,
MobileSignalView,
MobileWorkspaceScreenProps,
MobileWorkspaceView,

View File

@@ -1,11 +1,21 @@
export type MobileWorkUnitTone = "blue" | "green" | "orange" | "purple";
export interface MobileProjectView {
readonly connected: boolean;
readonly id: string;
readonly name: string;
}
export interface MobileWorkUnitView {
readonly artifactCount: number;
readonly code: string;
readonly id: string;
readonly canRetry: boolean;
readonly canStart: boolean;
readonly nextAction: string;
readonly pullRequestNumber?: number;
readonly progress: number;
readonly reviewUrl?: string;
readonly statusLabel: string;
readonly summary: string;
readonly title: string;
@@ -16,6 +26,7 @@ export interface MobileWorkUnitView {
export interface MobileSignalView {
readonly desiredOutcome: string;
readonly id: string;
readonly projectId?: string;
readonly summary: string;
readonly title: string;
}
@@ -42,7 +53,9 @@ export interface MobileWorkspaceView {
readonly latestSignal?: MobileSignalView;
readonly needsAttentionCount: number;
readonly organizationLabel: string;
readonly projects: readonly MobileProjectView[];
readonly projectName?: string;
readonly selectedProjectId?: string;
readonly selectedWorkUnit?: MobileWorkUnitView;
readonly shippedCount: number;
readonly totalCount: number;

View File

@@ -8,6 +8,7 @@ export type MobileWorkspaceScreenProps = Omit<
export type {
MobileAssistantMessageView,
MobileAssistantView,
MobileProjectView,
MobileSignalView,
MobileWorkspaceView,
MobileWorkUnitTone,