Skip to content
Draft
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
28 changes: 23 additions & 5 deletions apps/mobile/src/agent-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
Users,
X,
} from "lucide-react-native";
import { useEffect, useState } from "react";
import { type ReactNode, useEffect, useState } from "react";
import { ActivityIndicator, Image, Linking, Pressable, Text, View } from "react-native";
import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
import type { Artifact, BrowserSession } from "../../../packages/domain/src";
Expand Down Expand Up @@ -1042,7 +1042,7 @@ export function IdeasScreen() {
</View>
);
}
function TaskLink({ taskId, onOpen }: { taskId: string; onOpen?: () => void }) {
export function TaskLink({ taskId, onOpen }: { taskId: string; onOpen?: () => void }) {
const { open } = useWorkspace();
return (
<Button
Expand All @@ -1057,7 +1057,7 @@ function TaskLink({ taskId, onOpen }: { taskId: string; onOpen?: () => void }) {
</Button>
);
}
function IdeaCard({ idea }: { idea: Idea }) {
export function IdeaCard({ idea }: { idea: Idea }) {
const { mutate } = useAgentWorkspace();
const { open } = useWorkspace();
const [expanded, setExpanded] = useState(false);
Expand Down Expand Up @@ -1339,7 +1339,15 @@ function GoalForm({ onDone, category }: { onDone: () => void; category?: string
</Card>
);
}
function GoalCard({ goal, onOpenTask }: { goal: Goal; onOpenTask?: () => void }) {
export function GoalCard({
goal,
onOpenTask,
children,
}: {
goal: Goal;
onOpenTask?: () => void;
children?: ReactNode;
}) {
const { data, mutate, delegate } = useAgentWorkspace();
const { open } = useWorkspace();
const [busy, setBusy] = useState(false);
Expand Down Expand Up @@ -1425,6 +1433,7 @@ function GoalCard({ goal, onOpenTask }: { goal: Goal; onOpenTask?: () => void })
.map((task) => (
<TaskCard key={task.id} task={task} compact onOpen={onOpenTask} />
))}
{children}
</Card>
);
}
Expand Down Expand Up @@ -1530,7 +1539,15 @@ function MonitorForm({ onDone }: { onDone: () => void }) {
</Card>
);
}
function MonitorCard({ monitor, onOpenTask }: { monitor: Monitor; onOpenTask?: () => void }) {
export function MonitorCard({
monitor,
onOpenTask,
children,
}: {
monitor: Monitor;
onOpenTask?: () => void;
children?: ReactNode;
}) {
const { mutate } = useAgentWorkspace();
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
Expand Down Expand Up @@ -1611,6 +1628,7 @@ function MonitorCard({ monitor, onOpenTask }: { monitor: Monitor; onOpenTask?: (
</Button>
)}
<TaskLink taskId={monitor.taskId} onOpen={onOpenTask} />
{children}
</Card>
);
}
Expand Down
34 changes: 20 additions & 14 deletions apps/mobile/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { BrowserRunContext, BrowserToolCard } from "./browser-tool-card";
import { BrowserThreadCard } from "./computer";
import { ConversationQueue, type QueuedMessage } from "./conversation-queue";
import { runConversationTurn } from "./conversation-run";
import { GoalToolCard, IdeasToolCard, TrackingToolCard } from "./goal-tool-cards";
import { MailToolCard } from "./mail-tool-card";
import { FileThreadCard, TaskThreadCard } from "./thread-artifacts";
import { type Selection, useMuseThread } from "./threads";
Expand Down Expand Up @@ -86,15 +87,31 @@ export function WorkspaceTools() {
description: "Display a saved goal",
parameters: displayParameters,
render: ({ result, status }) => (
<ServerToolCard name="Goal" result={result} loading={status !== "complete"} />
<GoalToolCard result={result} loading={status !== "complete"} />
),
});
useRenderTool({
name: "update_goal",
description: "Display a goal after recording progress",
parameters: displayParameters,
render: ({ result, status }) => (
<GoalToolCard updating result={result} loading={status !== "complete"} />
),
});
useRenderTool({
name: "watch_page",
description: "Display a saved page watch",
parameters: displayParameters,
render: ({ result, status }) => (
<ServerToolCard name="Tracking" result={result} loading={status !== "complete"} />
<TrackingToolCard result={result} loading={status !== "complete"} />
),
});
useRenderTool({
name: "find_ideas",
description: "Display suggestions the person can start or dismiss",
parameters: displayParameters,
render: ({ result, status }) => (
<IdeasToolCard result={result} loading={status !== "complete"} />
),
});
useRenderTool({
Expand Down Expand Up @@ -147,18 +164,7 @@ function ServerToolCard({
{loading ? "Waiting for the server." : "Open the workspace to see the saved result."}
</Text>
)}
<Button
small
onPress={() =>
navigate(
name === "Goal" || name === "Tracking"
? "goals"
: name === "Memory"
? "apps"
: "activity",
)
}
>
<Button small onPress={() => navigate(name === "Memory" ? "apps" : "activity")}>
View {name.toLowerCase()}
</Button>
</Card>
Expand Down
176 changes: 176 additions & 0 deletions apps/mobile/src/goal-tool-cards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { ArrowRight } from "lucide-react-native";
import { Text, View } from "react-native";
import { z } from "zod";
import type { Section } from "../../../packages/domain/src";
import { GoalCard, IdeaCard, MonitorCard, TaskLink } from "./agent-ui";
import { useAgentWorkspace } from "./agent-workspace";
import { Button, Card, Chip, colors, ErrorNotice, s } from "./ui";
import { useWorkspace } from "./workspace";

// Tool results are only pointers. Every card reads the saved record from the polled
// workspace, so a milestone checked in chat or on the Goals tab shows in both places.
function parse<T extends z.ZodType>(schema: T, result: unknown): z.infer<T> | undefined {
let value = result;
if (typeof value === "string") {
try {
value = JSON.parse(value);
} catch {
return undefined;
}
}
const parsed = schema.safeParse(value);
return parsed.success ? parsed.data : undefined;
}
const savedResult = z.object({ id: z.string().optional(), error: z.string().optional() });

function ViewButton({ section, children }: { section: Section; children: string }) {
const { navigate } = useWorkspace();
return (
<Button
small
icon={ArrowRight}
style={{ alignSelf: "flex-start" }}
onPress={() => navigate(section)}
>
{children}
</Button>
);
}
function PendingCard({
heading,
detail,
error,
section,
view,
}: {
heading: string;
detail: string;
error?: string;
section: Section;
view: string;
}) {
return (
<Card style={{ padding: 16, gap: 10 }}>
<Text style={s.heading}>{heading}</Text>
{error ? <ErrorNotice error={error} /> : <Text style={s.muted}>{detail}</Text>}
<ViewButton section={section}>{view}</ViewButton>
</Card>
);
}

export function GoalToolCard({
result,
loading,
updating,
}: {
result: unknown;
loading: boolean;
updating?: boolean;
}) {
const { data } = useAgentWorkspace();
const saved = parse(savedResult, result);
const goal = saved?.id ? data?.goals.find((item) => item.id === saved.id) : undefined;
if (goal)
return (
<GoalCard goal={goal}>
<ViewButton section="goals">View goal</ViewButton>
</GoalCard>
);
return (
<PendingCard
heading={loading ? (updating ? "Updating goal…" : "Saving goal…") : "Goal"}
detail={loading ? "Waiting for the server." : "Open Goals to see the saved result."}
error={saved?.error}
section="goals"
view="View goal"
/>
);
}

export function TrackingToolCard({ result, loading }: { result: unknown; loading: boolean }) {
const { data } = useAgentWorkspace();
const saved = parse(savedResult, result);
const monitor = saved?.id ? data?.monitors.find((item) => item.id === saved.id) : undefined;
if (monitor)
return (
<MonitorCard monitor={monitor}>
<ViewButton section="goals">View tracking</ViewButton>
</MonitorCard>
);
return (
<PendingCard
heading={loading ? "Saving tracking…" : "Tracking"}
detail={loading ? "Waiting for the server." : "Open Goals to see the saved result."}
error={saved?.error}
section="goals"
view="View tracking"
/>
);
}

const ideasResult = z.object({
ideas: z.array(z.object({ id: z.string(), title: z.string(), reason: z.string() })).default([]),
more: z.number().default(0),
error: z.string().optional(),
});

export function IdeasToolCard({ result, loading }: { result: unknown; loading: boolean }) {
const { data } = useAgentWorkspace();
const found = parse(ideasResult, result);
if (loading || !found || found.error)
return (
<PendingCard
heading={loading ? "Looking for ideas…" : "Ideas"}
detail={loading ? "Checking your connected apps." : "Open Ideas to see suggestions."}
error={found?.error}
section="ideas"
view="View ideas"
/>
);
return (
<Card style={{ padding: 16, gap: 4 }}>
<Text style={s.heading}>Ideas for you</Text>
<Text style={s.small}>Inspired by your connected apps</Text>
{found.ideas.map((summary) => {
const idea = data?.ideas.find((item) => item.id === summary.id);
if (idea?.status === "new") return <IdeaCard key={idea.id} idea={idea} />;
return (
<View
key={summary.id}
style={{
paddingVertical: 14,
gap: 7,
borderBottomWidth: 1,
borderBottomColor: colors.line,
}}
>
<Text style={[s.heading, { fontSize: 16, lineHeight: 23 }]}>
{idea?.title ?? summary.title}
</Text>
{idea?.status === "accepted" ? (
<>
<Chip tint={colors.green}>Started</Chip>
{!!idea.taskId && <TaskLink taskId={idea.taskId} />}
</>
) : idea?.status === "dismissed" ? (
<Chip>Dismissed</Chip>
) : (
<Text style={s.muted}>{summary.reason}</Text>
)}
</View>
);
})}
{!found.ideas.length && (
<Text style={[s.muted, { paddingVertical: 10 }]}>
Nothing new to suggest right now. Ideas come from the apps and goals you’ve shared.
</Text>
)}
{found.more > 0 && (
<Text style={[s.small, { paddingTop: 8 }]}>{found.more} more on the Ideas tab</Text>
)}
<View style={{ paddingTop: 10 }}>
<ViewButton section="ideas">View ideas</ViewButton>
</View>
</Card>
);
}
Loading
Loading