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
27 changes: 14 additions & 13 deletions apps/mobile/src/features/assistant/AssistantQuickActionsPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { useAndroidWorkspaceSelection } from "../workspace/useAndroidWorkspaceSe
import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext";
import {
buildAssistantAttentionSnapshot,
filterAssistantAttentionItems,
type AssistantAttentionFilter,
type AssistantAttentionKind,
type AssistantAttentionItem,
} from "./attentionSnapshot";
Expand All @@ -48,8 +50,6 @@ const liveShellsAtom = Atom.make((get) => {
return readiness;
}).pipe(Atom.withLabel("mobile:assistant:live-shell-readiness"));

type AttentionFilter = "all" | "needs-me" | "working";

const LABELS: Readonly<Record<AssistantAttentionKind, string>> = {
approval: "Approval needed",
question: "Answer needed",
Expand All @@ -58,10 +58,6 @@ const LABELS: Readonly<Record<AssistantAttentionKind, string>> = {
working: "Working",
};

function isNeedsMe(kind: AssistantAttentionKind): boolean {
return kind === "approval" || kind === "question" || kind === "plan" || kind === "failed";
}

/** Opt-in downstream text experiment, not a voice or autonomous executor. */
export function AssistantQuickActionsPreview() {
const insets = useSafeAreaInsets();
Expand Down Expand Up @@ -110,7 +106,7 @@ function AssistantQuickActionsContent(props: { readonly onClose: () => void }) {
const [input, setInput] = useState("");
const [notice, setNotice] = useState<string | null>(null);
const [matches, setMatches] = useState<ReadonlyArray<VisibleAssistantThread>>([]);
const [filter, setFilter] = useState<AttentionFilter>("all");
const [filter, setFilter] = useState<AssistantAttentionFilter>("all");
const threads = useNavigationThreadShells();
const { connectedEnvironments } = useRemoteConnectionStatus();
const shellLive = useAtomValue(liveShellsAtom);
Expand Down Expand Up @@ -211,7 +207,7 @@ function AssistantQuickActionsContent(props: { readonly onClose: () => void }) {
return;
}
if (result.action.kind === "inspect") {
setFilter(result.action.queue === "working" ? "working" : "needs-me");
setFilter(result.action.queue);
setNotice(null);
setInput("");
return;
Expand All @@ -236,9 +232,7 @@ function AssistantQuickActionsContent(props: { readonly onClose: () => void }) {
setNotice("No live thread has that exact title. Search Chats for other titles.");
}, [input, liveIds, navigate, openThread, threads]);

const visibleItems = attention.items.filter((item) =>
filter === "all" ? true : filter === "working" ? item.kind === "working" : isNeedsMe(item.kind),
);
const visibleItems = filterAssistantAttentionItems(attention.items, filter);
const shownItems = visibleItems.slice(0, 24);
const needsMeCount =
attention.counts.approval +
Expand Down Expand Up @@ -282,10 +276,11 @@ function AssistantQuickActionsContent(props: { readonly onClose: () => void }) {
{notice}
</Text>
) : null}
<View className="flex-row gap-2">
<View className="flex-row flex-wrap gap-2">
{([
["all", "All", attention.items.length],
["needs-me", "Needs me", needsMeCount],
["approvals", "Approvals", attention.counts.approval],
["working", "Working", attention.counts.working],
] as const).map(([nextFilter, label, count]) => (
<Pressable
Expand Down Expand Up @@ -345,7 +340,13 @@ function AssistantQuickActionsContent(props: { readonly onClose: () => void }) {
) : null}
{shownItems.length === 0 ? (
<Text className="py-4 text-sm text-foreground-muted">
No current {filter === "working" ? "working threads" : "attention items"} from live environments.
No current{" "}
{filter === "working"
? "working threads"
: filter === "approvals"
? "approvals"
: "attention items"}{" "}
from live environments.
</Text>
) : (
shownItems.map((item: AssistantAttentionItem) => (
Expand Down
21 changes: 21 additions & 0 deletions apps/mobile/src/features/assistant/attentionSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "@effect/vitest";

import {
buildAssistantAttentionSnapshot,
filterAssistantAttentionItems,
type AssistantAttentionEnvironment,
type AssistantAttentionFacts,
} from "./attentionSnapshot";
Expand Down Expand Up @@ -31,6 +32,26 @@ function thread(
}

describe("assistant attention snapshot", () => {
it("keeps approvals exact while needs-me also includes questions, plans and failures", () => {
const snapshot = buildAssistantAttentionSnapshot(
[live],
[
thread({ threadId: "approval", needsApproval: true }),
thread({ threadId: "question", needsAnswer: true }),
thread({ threadId: "plan", needsPlanReview: true }),
thread({ threadId: "failure", failed: true }),
thread({ threadId: "working", working: true }),
],
);
const ids = (filter: "approvals" | "needs-me" | "working" | "all") =>
filterAssistantAttentionItems(snapshot.items, filter).map((item) => item.threadId);

expect(ids("approvals")).toEqual(["approval"]);
expect(ids("needs-me")).toEqual(["approval", "question", "plan", "failure"]);
expect(ids("working")).toEqual(["working"]);
expect(ids("all")).toEqual(["approval", "question", "plan", "failure", "working"]);
});

it("ranks human decisions above failures and working; chooses one state per thread", () => {
const snapshot = buildAssistantAttentionSnapshot(
[live],
Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/features/assistant/attentionSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export interface AssistantAttentionSnapshot {
readonly unavailableEnvironments: ReadonlyArray<string>;
}

export type AssistantAttentionFilter = "all" | "needs-me" | "approvals" | "working";

/** Exact read-only filtering; approvals must never include other blockers. */
export function filterAssistantAttentionItems(
items: ReadonlyArray<AssistantAttentionItem>,
filter: AssistantAttentionFilter,
): ReadonlyArray<AssistantAttentionItem> {
if (filter === "all") return items;
if (filter === "approvals") return items.filter((item) => item.kind === "approval");
if (filter === "working") return items.filter((item) => item.kind === "working");
return items.filter((item) => item.kind !== "working");
}

const PRIORITY: Readonly<Record<AssistantAttentionKind, number>> = {
approval: 0,
question: 1,
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/src/features/assistant/fastActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,16 @@ describe("app assistant fast actions", () => {
});

it("keeps inspection read-only and sends complex requests to the agent", () => {
expect(resolveAssistantFastAction("What's waiting on me?")).toEqual({
expect(resolveAssistantFastAction("Show approvals")).toEqual({
kind: "action",
source: "rule",
action: { kind: "inspect", queue: "approvals" },
});
expect(resolveAssistantFastAction("What's waiting on me?")).toEqual({
kind: "action",
source: "rule",
action: { kind: "inspect", queue: "needs-me" },
});
expect(resolveAssistantFastAction("show working agents")).toEqual({
kind: "action",
source: "rule",
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/assistant/fastActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type AssistantDestination = "chats" | "pulls" | "environments" | "usage";

export type AssistantFastAction =
| { readonly kind: "navigate"; readonly destination: AssistantDestination }
| { readonly kind: "inspect"; readonly queue: "approvals" | "working" }
| { readonly kind: "inspect"; readonly queue: "approvals" | "needs-me" | "working" }
| { readonly kind: "find-thread"; readonly title: string };

export type AssistantFastResolution =
Expand Down Expand Up @@ -62,9 +62,12 @@ export function resolveAssistantFastAction(utterance: string): AssistantFastReso
return { kind: "action", source: "rule", action: { kind: "navigate", destination } };
}

if (["show approvals", "what needs my approval", "what's waiting on me"].includes(phrase)) {
if (["show approvals", "what needs my approval"].includes(phrase)) {
return { kind: "action", source: "rule", action: { kind: "inspect", queue: "approvals" } };
}
if (["what's waiting on me", "what needs my attention", "show what needs me"].includes(phrase)) {
return { kind: "action", source: "rule", action: { kind: "inspect", queue: "needs-me" } };
}
if (["show working agents", "who is working", "what's running"].includes(phrase)) {
return { kind: "action", source: "rule", action: { kind: "inspect", queue: "working" } };
}
Expand Down
Loading