From eaa23611ac983171ce01483a4ecb1f373557e613 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 17:23:50 +0000 Subject: [PATCH] feat(app): Activity screen - the command queue on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Manage → Activity lists every control command with status filter chips, when/where it ran (project, agent, claiming worker or 'unclaimed'), and tap-to-expand result/error text; a Sweep CI button enqueues the global poll. The list auto-refreshes every 5s while open, so a login_start or spawn can be watched to completion — this was the one web dashboard page missing from the phone, and exactly the view needed to diagnose a stuck command. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48 --- CHANGELOG.md | 8 ++ app/App.tsx | 2 + app/README.md | 1 + app/src/screens/manage/ActivityScreen.tsx | 160 ++++++++++++++++++++++ app/src/screens/manage/ManageScreen.tsx | 1 + app/src/state/AppState.tsx | 1 + 6 files changed, 173 insertions(+) create mode 100644 app/src/screens/manage/ActivityScreen.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index b39e51e..13d8dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ the image workflows publish (plus `latest` from every push to `main`). ## [Unreleased] +### Added + +- **Activity screen in the mobile app** (Settings → Manage → Activity): the + control-command queue with status filters, per-row worker attribution + (`on ` / `unclaimed`), expandable result/error text, a Sweep CI action, and + a 5s auto-refresh — the screen that answers "why is my login/spawn/sync stuck" from + the phone. + ### Fixed - **Untrusted-workspace wedge on headless runs.** Phase 4's tmux-path deletion also diff --git a/app/App.tsx b/app/App.tsx index d4e93f4..96f61d7 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -41,6 +41,7 @@ import { PermissionsScreen } from "./src/screens/manage/PermissionsScreen"; import { RepositoriesScreen } from "./src/screens/manage/RepositoriesScreen"; import { GitServersScreen } from "./src/screens/manage/GitServersScreen"; import { ApprovalsScreen } from "./src/screens/manage/ApprovalsScreen"; +import { ActivityScreen } from "./src/screens/manage/ActivityScreen"; import { SharedContextScreen } from "./src/screens/manage/SharedContextScreen"; import { UsersScreen } from "./src/screens/manage/UsersScreen"; import { AccountScreen } from "./src/screens/manage/AccountScreen"; @@ -78,6 +79,7 @@ function Router() { repositories: RepositoriesScreen, gitServers: GitServersScreen, approvals: ApprovalsScreen, + activity: ActivityScreen, shared: SharedContextScreen, users: UsersScreen, account: AccountScreen, diff --git a/app/README.md b/app/README.md index 1ae33af..2041382 100644 --- a/app/README.md +++ b/app/README.md @@ -46,6 +46,7 @@ The full admin surface — everything the web dashboard can do, under | Plugins | Marketplace plugins pinned to their repo | | Permissions | Default permission mode + allow/deny/ask rules over the read-only env baseline | | Claude login | Drive the worker's `claude /login` (authorize in browser, paste the code back) | +| Activity | The control-command queue: status filters, worker attribution, result/error detail, 5s auto-refresh | | Repositories | Register repos (git-server or manual mode, optional mise-init bootstrap), sync, delete | | Git servers | Forge hosts: encrypted tokens, generated deploy keys (public half copyable) | | Approvals | Record operator approve / reject verdicts per project + branch | diff --git a/app/src/screens/manage/ActivityScreen.tsx b/app/src/screens/manage/ActivityScreen.tsx new file mode 100644 index 0000000..8195be8 --- /dev/null +++ b/app/src/screens/manage/ActivityScreen.tsx @@ -0,0 +1,160 @@ +import React, { useEffect, useState } from "react"; +import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +import { text } from "../../theme/tokens"; +import { useTheme } from "../../theme/useTheme"; +import { Badge } from "../../components/Badge"; +import { Button } from "../../components/Button"; +import { Chip } from "../../components/Chip"; +import { ErrorNotice, ManageShell } from "../../components/ManageShell"; +import { Card, Divider, Mono, SectionLabel } from "../../components/primitives"; +import { useAppState } from "../../state/AppState"; +import { useResource } from "../../state/useResource"; +import { statusLabel, statusTone, timeAgo } from "../../api/format"; +import type { Command } from "../../api/client"; + +/** + * Activity — the control-command queue: every enqueued action and its status + * (queued → running → done/failed), the audit log of what the operator triggered. + * This is the screen that answers "why is my login/spawn/sync stuck": a row stuck + * `queued` means no worker is claiming; a `failed` row carries the worker's error + * verbatim. Auto-refreshes while open so a command can be watched to completion. + */ + +const FILTERS = ["all", "queued", "running", "failed", "done"]; + +export function ActivityScreen() { + const { colors } = useTheme(); + const { client } = useAppState(); + const { data, error, loading, reload } = useResource("/commands?limit=100"); + + const [filter, setFilter] = useState("all"); + const [openId, setOpenId] = useState(null); + const [sweepNote, setSweepNote] = useState(null); + const [sweepError, setSweepError] = useState(null); + + // The whole point of this screen is watching a command land — poll while open. + useEffect(() => { + const id = setInterval(reload, 5000); + return () => clearInterval(id); + }, [reload]); + + const commands = (data ?? []).filter((c) => + filter === "all" ? true : c.status === filter, + ); + + async function sweepCi() { + if (!client) return; + setSweepError(null); + try { + await client.api("/poll-ci", { method: "POST" }); + setSweepNote("CI sweep queued."); + setTimeout(() => setSweepNote(null), 4000); + reload(); + } catch (e) { + setSweepError(e instanceof Error ? e.message : "Couldn't queue the sweep."); + } + } + + return ( + + + + {FILTERS.map((f) => ( + setFilter(f)} + /> + ))} + + + + + {sweepNote ? ( + + {sweepNote} + + ) : null} + + + {loading && data === null ? ( + Loading… + ) : commands.length === 0 ? ( + + {filter === "all" ? "No commands yet." : `No ${filter} commands.`} + + ) : ( + <> + + {`${commands.length} command${commands.length === 1 ? "" : "s"} · refreshes every 5s`} + + + {commands.map((c, i) => { + const open = openId === c.id; + const detail = c.error || (c.result ? JSON.stringify(c.result) : null); + return ( + + {i > 0 && } + setOpenId(open ? null : c.id)} + > + + + {c.type} + + {statusLabel(c.status)} + + {timeAgo(c.created_at)} + + + + {c.project_id ?? "—"} + {c.agent_name ? ` · ${c.agent_name}` : ""} + {c.claimed_by ? ` · on ${c.claimed_by}` : " · unclaimed"} + + {detail ? ( + + {detail} + + ) : null} + + + ); + })} + + + )} + + ); +} + +const styles = StyleSheet.create({ + topRow: { + flexDirection: "row", + alignItems: "center", + gap: 10, + marginBottom: 14, + }, + filters: { flexDirection: "row", gap: 8, paddingRight: 10 }, + row: { paddingVertical: 12, paddingHorizontal: 16 }, + titleRow: { flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" }, +}); diff --git a/app/src/screens/manage/ManageScreen.tsx b/app/src/screens/manage/ManageScreen.tsx index a43c12b..29e5e2e 100644 --- a/app/src/screens/manage/ManageScreen.tsx +++ b/app/src/screens/manage/ManageScreen.tsx @@ -29,6 +29,7 @@ const CLAUDE_ROWS: Row[] = [ ]; const SERVER_ROWS: Row[] = [ + { screen: "activity", title: "Activity", subtitle: "The command queue — see why an action is stuck" }, { screen: "repositories", title: "Repositories", subtitle: "Register + sync project repos" }, { screen: "gitServers", title: "Git servers", subtitle: "Forge hosts, tokens, deploy keys" }, { screen: "approvals", title: "Approvals", subtitle: "Approve or reject protected branches" }, diff --git a/app/src/state/AppState.tsx b/app/src/state/AppState.tsx index 3153b5e..accf3d2 100644 --- a/app/src/state/AppState.tsx +++ b/app/src/state/AppState.tsx @@ -55,6 +55,7 @@ export type Screen = | "repositories" | "gitServers" | "approvals" + | "activity" | "shared" | "users" | "account"