Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

The phone app no longer has g1t's own agents, to match the site. The Agents tab is gone from the bar, and Customize (skills, connectors and plugins) is gone with its row in More. New message finds and messages people only, and its search and heading say so. A card an agent posted earlier still shows with its title, state, details and links, but its other buttons are no longer drawn, since chat no longer answers them; an old agent's messages and direct messages still show, under the name chat gives them, marked Deleted agent. The app stops asking for agents:run when it signs in. An old g1t:// link to Agents, the skill library, Customize or the Marketplace opens Home, while the integrations directory opens on the site. Notifications, the sign-in screen and the access-token error no longer mention agents waiting on you or agent tokens, and the sign-in screen speaks of reviewing what you and your coding agents make. Pull requests still show the coding agent they were made with.

syntaqxcommitted Parentd0c3abaBrowse files
23 files+80−3800/23 viewed
+3−9
11 # g1t for Android and iOS
22
33 The phone app: [Expo](https://expo.dev) (SDK 57, React Native 0.86) with
4−Expo Router, in TypeScript. Chat, Code and Customize, signed in with g1t's
4+Expo Router, in TypeScript. Chat and Code, signed in with g1t's
55 own OAuth. Android is the first target. What it does for people is in the
66 guide, [The phone app](../docs/src/content/docs/guides/mobile.md).
77
1919 (app)/ signed in: the tabs
2020 chat/ the list, a conversation, a thread, new, browse
2121 code/ repositories, pull requests, issues, diffs
22− more/ Customize, notifications, workspaces, sign out
23− customize/ Skills, Connectors, Plugins (from More)
24− home/, notifications/, agents/ coming: they open the site
22+ more/ notifications, workspaces, sign out
23+ home/, notifications/ coming: they open the site
2524 +native-intent.tsx g1t:// links to screens (lib/links.ts)
2625 components/ the building blocks, themed (ui.tsx); chat/ and Code's
2726 lib/
3433 live.ts the site's live sockets, with tickets, kept open
3534 feed.tsx counts, banners and presence while the app is open
3635 push.ts notifications on the phone
37− customize.ts Customize's listings, from @g1t/contracts' catalogs
3836 theme/ packages/theme/tokens.css's colours, for React Native
3937 ```
4038
4139 Shared types and catalogs come from `@g1t/contracts` (by subpath, such as
4240 `@g1t/contracts/oauth`, so the app does not bundle every module).
43−Customize's three parts are defined there too (`customize.ts`), so the
44−site can move the Marketplace onto the same model.
4541
4642 ## How it talks to g1t
4743
260256 - **The brand's faces**: TTF copies of Hanken Grotesk, Bricolage
261257 Grotesque and IBM Plex Mono (packages/theme ships woff2), loaded with
262258 `expo-font`.
263−- **Agents' faces**: `components/ui.tsx`'s `Avatar` draws a letter for an
264− agent; the site's `AgentFace` should be shared and drawn here.
265259 - **The store listing**: screenshots, a privacy policy URL, and Play's
266260 data safety form (the app keeps the server and credentials in the
267261 keystore, registers a push token with the g1t it is pointed at, and
+4−6
11 import * as Notifications from "expo-notifications";
22 import { Tabs, router, useGlobalSearchParams, useSegments } from "expo-router";
3−import { Bell, Bot, Code2, House, Menu, MessagesSquare } from "lucide-react-native";
3+import { Bell, Code2, House, Menu, MessagesSquare } from "lucide-react-native";
44 import type { LucideIcon } from "lucide-react-native";
55 import { type ComponentType, useEffect, useRef } from "react";
66 import { StyleSheet, View } from "react-native";
1515
1616 /**
1717 * The signed-in app. Its bar is the site's phone bar
18− * (apps/web/app/components/mobile.tsx): Home, Chat, Notifications, Agents,
19− * Code, and More. Chat and Code have their screens; the others say so and
20− * open the site. Customize and your account are under More.
18+ * (apps/web/app/components/mobile.tsx): Home, Chat, Notifications, Code,
19+ * and More. Chat and Code have their screens; the others say so and open
20+ * the site. Your account is under More.
2121 */
2222 export default function AppLayout() {
2323 return (
7373 }}
7474 />
7575 <Tabs.Screen name="notifications" options={{ title: "Notifications", tabBarIcon: (props) => <BarIcon icon={Bell} {...props} /> }} />
76− <Tabs.Screen name="agents" options={{ title: "Agents", tabBarIcon: (props) => <BarIcon icon={Bot} {...props} /> }} />
7776 <Tabs.Screen name="code" options={{ title: "Code", href: code ? undefined : null, tabBarIcon: (props) => <BarIcon icon={Code2} {...props} /> }} />
78− <Tabs.Screen name="customize" options={{ href: null }} />
7977 <Tabs.Screen name="more" options={{ title: "More", tabBarIcon: (props) => <BarIcon icon={Menu} {...props} /> }} />
8078 </Tabs>
8179 );
+0−3
1−import { TabStack } from "@/components/stack";
2−
3−export default TabStack;
+0−15
1−import { Bot } from "lucide-react-native";
2−
3−import { ComingSoon } from "@/components/coming-soon";
4−
5−/** Agents: the workspace's agents, their sessions, budgets and memory. */
6−export default function Agents() {
7− return (
8− <ComingSoon
9− title="Agents"
10− icon={Bot}
11− body="Your workspace's agents, what they are doing and what they cost. Message any of them from Chat meanwhile."
12− path={(workspace) => `/${workspace}/-/agents`}
13− />
14− );
15−}
+2−2
9191 search ? (
9292 <Empty title="Nothing matches" />
9393 ) : (
94− <Empty icon={MessagesSquare} title="No conversations yet" body="Message a person or an agent, or join a channel." action={<Button title="New message" onPress={() => router.push("/chat/new")} />} />
94+ <Empty icon={MessagesSquare} title="No conversations yet" body="Message a person, or join a channel." action={<Button title="New message" onPress={() => router.push("/chat/new")} />} />
9595 )
9696 }
9797 ListFooterComponent={
151151 </View>
152152 <View style={{ flexDirection: "row", alignItems: "center", gap: space.sm }}>
153153 <Text size="small" tone="muted" numberOfLines={1} style={{ flex: 1 }}>
154− {dm ? (other?.kind === "agent" ? (other.title ?? other.role ?? "Agent") : `@${other?.name ?? ""}`) : (channel.topic ?? (entry.muted ? "Muted" : " "))}
154+ {dm ? (other?.kind === "agent" ? "Deleted agent" : `@${other?.name ?? ""}`) : (channel.topic ?? (entry.muted ? "Muted" : " "))}
155155 </Text>
156156 {entry.mentions > 0 ? <Badge label={`@ ${entry.mentions}`} tone="accent" /> : loud ? <View style={[styles.dot, { backgroundColor: colors.accent }]} /> : null}
157157 </View>
+11−26
1−import type { MemberProfile } from "@g1t/contracts";
21 import { useQuery } from "@tanstack/react-query";
32 import { Stack, router } from "expo-router";
43 import { Check, Hash, Search } from "lucide-react-native";
76
87 import { PromptSheet } from "@/components/chat/sheet";
98 import { Avatar, Button, Loading, Row, Screen, SectionHeader, Text } from "@/components/ui";
10−import { useChatSidebar, useStartConversation } from "@/lib/chat";
9+import { useStartConversation } from "@/lib/chat";
1110 import { useSignedIn } from "@/lib/session";
1211 import { radius, space, useColors } from "@/theme";
1312
14−type Person = { key: string; name: string; handle: string; avatar: string | null; agent: boolean; detail: string | null };
13+type Person = { key: string; name: string; handle: string; avatar: string | null };
1514
1615 /**
17− * A new conversation: pick people and agents for a direct message (one,
18− * or a group), or make a channel. People come from the workspace's
19− * members (`GET /workspaces/<ws>/members`); agents from the conversations
20− * you already have with them.
16+ * A new conversation: pick people for a direct message (one, or a
17+ * group), or make a channel. People come from the workspace's members
18+ * (`GET /workspaces/<ws>/members`).
2119 */
2220 export default function NewConversation() {
2321 const { client, workspace, me, server } = useSignedIn();
2422 const slug = workspace?.slug ?? "";
2523 const colors = useColors();
26− const sidebar = useChatSidebar();
2724 const start = useStartConversation();
2825 const [search, setSearch] = useState("");
2926 const [picked, setPicked] = useState<Person[]>([]);
3835 });
3936
4037 const everyone = useMemo(() => {
41− const agents = new Map<string, MemberProfile>();
42− for (const entry of sidebar.data?.entries ?? []) for (const other of entry.others) if (other.kind === "agent") agents.set(other.id, other);
4338 const people: Person[] = (members.data ?? [])
4439 .filter((member) => member.username.toLowerCase() !== me.username.toLowerCase())
4540 .map((member) => ({
4742 name: member.name || member.display_username || member.username,
4843 handle: member.display_username ?? member.username,
4944 avatar: member.avatar ?? null,
50− agent: false,
51− detail: null,
5245 }));
53− const bots: Person[] = [...agents.values()].map((agent) => ({
54− key: `agent:${agent.id}`,
55− name: agent.display_name,
56− handle: agent.name,
57− avatar: agent.avatar,
58− agent: true,
59− detail: agent.title ?? agent.role,
60− }));
6146 const words = search.trim().toLowerCase();
62− return [...bots, ...people].filter((one) => !words || `${one.name} ${one.handle}`.toLowerCase().includes(words));
63− }, [members.data, sidebar.data, me.username, search]);
47+ return people.filter((one) => !words || `${one.name} ${one.handle}`.toLowerCase().includes(words));
48+ }, [members.data, me.username, search]);
6449
6550 const toggle = (person: Person) =>
6651 setPicked((now) => (now.some((one) => one.key === person.key) ? now.filter((one) => one.key !== person.key) : [...now, person]));
8570 autoFocus
8671 value={search}
8772 onChangeText={setSearch}
88− placeholder="Find a person or an agent"
73+ placeholder="Find a person"
8974 placeholderTextColor={colors.faint}
9075 style={{ flex: 1, color: colors.fg, fontSize: 15, paddingVertical: 8 }}
9176 autoCorrect={false}
10792 ListHeaderComponent={
10893 <>
10994 <Row leading={<Hash size={20} color={colors.accent} />} title="Make a channel" subtitle="A conversation for a team or a topic" onPress={() => setMaking(true)} />
110− <SectionHeader title="People and agents" />
95+ <SectionHeader title="People" />
11196 </>
11297 }
11398 renderItem={({ item }) => {
11499 const on = picked.some((one) => one.key === item.key);
115100 return (
116101 <Pressable onPress={() => toggle(item)} style={({ pressed }) => [styles.person, { backgroundColor: pressed ? colors.raised : "transparent" }]}>
117− <Avatar name={item.name} uri={item.avatar ? `${server.site}/avatars/${item.avatar}` : null} agent={item.agent} size={36} />
102+ <Avatar name={item.name} uri={item.avatar ? `${server.site}/avatars/${item.avatar}` : null} size={36} />
118103 <View style={{ flex: 1 }}>
119104 <Text style={{ fontWeight: "600" }} numberOfLines={1}>
120105 {item.name}
121106 </Text>
122107 <Text size="small" tone="muted" numberOfLines={1}>
123− {item.agent ? (item.detail ?? "Agent") : `@${item.handle}`}
108+ @{item.handle}
124109 </Text>
125110 </View>
126111 <View style={[styles.check, { borderColor: on ? colors.accent : colors.lineStrong, backgroundColor: on ? colors.accent : "transparent" }]}>
+1−1
2121 return (
2222 <Screen>
2323 <Stack.Screen options={{ title: "Code" }} />
24− <Empty icon={Code2} title="Code is not on for you here" body={`${workspace.name ?? workspace.slug} gives you Chat, Docs and agents. Ask an owner for Code.`} />
24+ <Empty icon={Code2} title="Code is not on for you here" body={`${workspace.name ?? workspace.slug} gives you Chat and Docs. Ask an owner for Code.`} />
2525 </Screen>
2626 );
2727 }
+0−47
1−import { CUSTOMIZE_SECTIONS, type CustomizeSection } from "@g1t/contracts/customize";
2−import { Redirect, Stack, useLocalSearchParams } from "expo-router";
3−import * as WebBrowser from "expo-web-browser";
4−import { ExternalLink } from "lucide-react-native";
5−import { SectionList, View } from "react-native";
6−
7−import { Badge, Button, Row, Screen, SectionHeader, Text } from "@/components/ui";
8−import { CUSTOMIZE, type CustomizeItem, customizeItems, customizePath } from "@/lib/customize";
9−import { siteUrl } from "@/lib/server";
10−import { useSignedIn } from "@/lib/session";
11−import { space } from "@/theme";
12−
13−/** One part of Customize: its listings, grouped, each opening where it is set up. */
14−export default function CustomizeSectionScreen() {
15− const { section } = useLocalSearchParams<{ section: string }>();
16− const { workspace, server } = useSignedIn();
17− if (!CUSTOMIZE_SECTIONS.includes(section as CustomizeSection)) return <Redirect href="/customize" />;
18− const id = section as CustomizeSection;
19− const slug = workspace?.slug ?? "";
20− const items = customizeItems(id, slug);
21− const groups = [...new Set(items.map((item) => item.group))].map((group) => ({ title: group, data: items.filter((item) => item.group === group) }));
22− const open = (path: string) => WebBrowser.openBrowserAsync(siteUrl(server, path));
23−
24− return (
25− <Screen>
26− <Stack.Screen options={{ title: CUSTOMIZE[id].title }} />
27− <SectionList
28− sections={groups}
29− keyExtractor={(item) => item.id}
30− stickySectionHeadersEnabled={false}
31− ListHeaderComponent={
32− <View style={{ padding: space.lg, gap: space.md }}>
33− <Text tone="muted">{CUSTOMIZE[id].description}</Text>
34− <Button title={`Manage ${CUSTOMIZE[id].title.toLowerCase()} on g1t`} variant="secondary" icon={ExternalLink} onPress={() => open(customizePath(id, slug))} />
35− </View>
36− }
37− renderSectionHeader={({ section: group }) => <SectionHeader title={group.title} />}
38− renderItem={({ item }) => <Item item={item} onPress={() => open(item.sitePath)} />}
39− />
40− </Screen>
41− );
42−}
43−
44−function Item({ item, onPress }: { item: CustomizeItem; onPress: () => void }) {
45− const badge = item.status === "available" ? <Badge label="Available" tone="success" /> : item.status === "soon" ? <Badge label="Soon" /> : <Badge label="Built in" tone="accent" />;
46− return <Row title={item.name} subtitle={item.description} trailing={badge} onPress={item.status === "soon" ? undefined : onPress} />;
47−}
+0−3
1−import { TabStack } from "@/components/stack";
2−
3−export default TabStack;
+0−43
1−import { CUSTOMIZE_SECTIONS } from "@g1t/contracts/customize";
2−import { Stack, router } from "expo-router";
3−import { Blocks, Plug, Sparkles } from "lucide-react-native";
4−import { View } from "react-native";
5−
6−import { Row, Screen, Text } from "@/components/ui";
7−import { CUSTOMIZE, customizeItems } from "@/lib/customize";
8−import { useSignedIn } from "@/lib/session";
9−import { space, useColors } from "@/theme";
10−
11−const ICONS = { skills: Sparkles, connectors: Plug, plugins: Blocks } as const;
12−
13−/**
14− * Customize: what shapes how this workspace's agents work. Skills are what
15− * they know how to do, Connectors what they can reach, Plugins what adds
16− * to the workspace. It replaces the Marketplace.
17− */
18−export default function Customize() {
19− const { workspace } = useSignedIn();
20− const colors = useColors();
21− const slug = workspace?.slug ?? "";
22− return (
23− <Screen>
24− <Stack.Screen options={{ title: "Customize" }} />
25− <View style={{ padding: space.lg }}>
26− <Text tone="muted">Skills, connectors and plugins for {workspace?.name ?? slug}. Owners add them; anyone else asks an owner.</Text>
27− </View>
28− {CUSTOMIZE_SECTIONS.map((section) => {
29− const Icon = ICONS[section];
30− const count = customizeItems(section, slug).length;
31− return (
32− <Row
33− key={section}
34− leading={<Icon size={22} color={colors.accent} />}
35− title={`${CUSTOMIZE[section].title} · ${count}`}
36− subtitle={CUSTOMIZE[section].description}
37− onPress={() => router.push({ pathname: "/customize/[section]", params: { section } })}
38− />
39− );
40− })}
41− </Screen>
42− );
43−}
+5−6
11 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22 import Constants from "expo-constants";
3−import { Stack, router } from "expo-router";
3+import { Stack } from "expo-router";
44 import * as WebBrowser from "expo-web-browser";
5−import { Bell, BellRing, Blocks, Check, ExternalLink, LogOut } from "lucide-react-native";
5+import { Bell, BellRing, Check, ExternalLink, LogOut } from "lucide-react-native";
66 import { Linking, View } from "react-native";
77
88 import { Avatar, Badge, Button, Row, Screen, SectionHeader, Text } from "@/components/ui";
1111 import { useSignedIn } from "@/lib/session";
1212 import { space, useColors } from "@/theme";
1313
14−/** More: Customize, your workspaces, notifications on this phone, how you signed in, and signing out. */
14+/** More: your workspaces, notifications on this phone, how you signed in, and signing out. */
1515 export default function More() {
1616 const { me, workspace, server, client, selectWorkspace, signOut } = useSignedIn();
1717 const colors = useColors();
2525 unsupported: { label: "Not here", detail: "Notifications need a build of the app on a phone: not Expo Go on Android, nor a simulator." },
2626 off: { label: "Off", detail: `${new URL(server.site).host} sends no notifications to phones. Chat still updates while the app is open.` },
2727 denied: { label: "Off", detail: "Turned off for g1t in your phone's settings. Tap to open them." },
28− undetermined: { label: "Off", detail: "Tap to get notified of messages to you, mentions and agents waiting on you." },
29− granted: { label: "On", detail: "Messages to you, mentions, replies and agents waiting on you, when you're not at g1t elsewhere." },
28+ undetermined: { label: "Off", detail: "Tap to get notified of messages to you, mentions and replies." },
29+ granted: { label: "On", detail: "Messages to you, mentions and replies, when you're not at g1t elsewhere." },
3030 }[push.data ?? "undetermined"];
3131
3232 return (
4343 </View>
4444
4545 <View>
46− <Row leading={<Blocks size={22} color={colors.accent} />} title="Customize" subtitle="Skills, connectors and plugins for your agents" onPress={() => router.push("/customize")} />
4746 <Row
4847 leading={<Bell size={22} color={colors.accent} />}
4948 title="Notifications on this phone"
+1−1
5050 <View style={{ gap: space.md }}>
5151 <G1tMark size={44} />
5252 <Text size="title">{hosted ? "Sign in to g1t" : `Sign in to ${host}`}</Text>
53− <Text tone="muted">Chat with your team and its agents, and review and merge what they make.</Text>
53+ <Text tone="muted">Chat with your team, and review and merge what you and your coding agents make.</Text>
5454 </View>
5555
5656 {mode === "oauth" ? (
+1−6
3939 return (
4040 <Animated.View pointerEvents="box-none" style={[styles.wrap, { top: insets.top + space.sm, transform: [{ translateY: offset }] }]}>
4141 <Pressable onPress={go} style={[styles.banner, { backgroundColor: colors.raised, borderColor: colors.lineStrong }]}>
42− <Avatar
43− name={banner.actor.name}
44− uri={banner.actor.avatar ? `${server.site}/avatars/${banner.actor.avatar}` : null}
45− agent={banner.actor.kind === "agent"}
46− size={36}
47− />
42+ <Avatar name={banner.actor.name} uri={banner.actor.avatar ? `${server.site}/avatars/${banner.actor.avatar}` : null} size={36} />
4843 <View style={{ flex: 1 }}>
4944 <Text style={{ fontWeight: "700" }} numberOfLines={1}>
5045 {banner.title}
+1−11
1−import type { CardAction, ChatMessage } from "@g1t/contracts";
1+import type { ChatMessage } from "@g1t/contracts";
22 import { useQueryClient } from "@tanstack/react-query";
33 import * as Clipboard from "expo-clipboard";
44 import { router, useFocusEffect } from "expo-router";
8888 useEffect(() => {
8989 if (slug && newest && focused.current && !thread) void markRead(client, queries, slug, channelId, newest);
9090 }, [client, queries, slug, channelId, newest, thread]);
91−
92− const onCardAction = useCallback(
93− async (message: ChatMessage, action: CardAction, input?: string | null) => {
94− const answer = await actions.cardAction.mutateAsync({ message, action: action.id, input });
95− if (!answer.ok) throw new Error(answer.message ?? "That didn't work.");
96− return answer.message;
97− },
98− [actions.cardAction],
99− );
10091
10192 const onReact = useCallback((message: ChatMessage, emoji: string, add: boolean) => actions.react.mutate({ message, emoji, add }), [actions.react]);
10293 const onRetry = useCallback((message: Shown) => send(message.body, message), [send]);
137128 onLongPress={setPicked}
138129 onReact={onReact}
139130 onRetry={onRetry}
140− onCardAction={onCardAction}
141131 />
142132 )
143133 }
+27−103
1−import type { CardAction, ChatMessage, MessageCard } from "@g1t/contracts";
1+import type { ChatMessage, MessageCard } from "@g1t/contracts";
22 import * as Haptics from "expo-haptics";
33 import { router } from "expo-router";
44 import * as WebBrowser from "expo-web-browser";
55 import { AlertCircle, Clock, MessageSquareReply } from "lucide-react-native";
6−import { memo, useState } from "react";
7−import { ActivityIndicator, Alert, Pressable, StyleSheet, View } from "react-native";
6+import { memo } from "react";
7+import { Pressable, StyleSheet, View } from "react-native";
88
99 import { Markdown } from "@/components/chat/markdown";
10−import { PromptSheet } from "@/components/chat/sheet";
1110 import { Avatar, Badge, Text } from "@/components/ui";
1211 import type { Shown } from "@/lib/chat";
1312 import { routeFor } from "@/lib/links";
2524 onLongPress: (message: Shown) => void;
2625 onReact: (message: ChatMessage, emoji: string, add: boolean) => void;
2726 onRetry: (message: Shown) => void;
28− onCardAction: (message: ChatMessage, action: CardAction, input?: string | null) => Promise<string | null>;
2927 };
3028
3129 /**
3230 * One message in a conversation. Messages by the same author within a few
33− * minutes run together under one name, as on the site; an agent carries
34− * its marker and an Agent badge, and its cards' buttons work here.
31+ * minutes run together under one name, as on the site. A message an agent
32+ * posted before g1t's own agents were retired still shows, under the name
33+ * chat gives it, with its marker and a Deleted agent badge.
3534 */
36−export const Message = memo(function Message({ message, head, inThread, onLongPress, onReact, onRetry, onCardAction }: Props) {
35+export const Message = memo(function Message({ message, head, inThread, onLongPress, onReact, onRetry }: Props) {
3736 const colors = useColors();
3837 const { server } = useSignedIn();
3938 const { author } = message;
6564 <Text style={{ fontWeight: "700" }} numberOfLines={1}>
6665 {author.display_name}
6766 </Text>
68− {agent ? <Badge label={author.title ?? "Agent"} tone="accent" /> : null}
67+ {agent ? <Badge label="Deleted agent" tone="neutral" /> : null}
6968 <Text size="small" tone="faint">
7069 {clock(message.created_at)}
7170 </Text>
7776 (edited)
7877 </Text>
7978 ) : null}
80− {message.kind === "card" && message.card ? <Card card={message.card} message={message} onAction={onCardAction} site={server.site} /> : null}
79+ {message.kind === "card" && message.card ? <Card card={message.card} site={server.site} /> : null}
8180 {message.reactions && message.reactions.length > 0 ? (
8281 <View style={styles.reactions}>
8382 {message.reactions.map((reaction) => (
130129 });
131130
132131 /**
133− * An agent's card: what it is about, its state, and the buttons that act
134− * on it (approve more budget, stop a session, file a draft issue). A button
135− * may ask first, or ask for a value; its owner answers, and the card
136− * updates in place through the live socket.
132+ * A card an agent posted before g1t's own agents were retired: what it was
133+ * about, its state, and its links. Its other buttons no longer do anything,
134+ * so they are not drawn.
137135 */
138−function Card({
139− card,
140− message,
141− onAction,
142− site,
143−}: {
144− card: MessageCard;
145− message: ChatMessage;
146− onAction: Props["onCardAction"];
147− site: string;
148−}) {
136+function Card({ card, site }: { card: MessageCard; site: string }) {
149137 const colors = useColors();
150− const [busy, setBusy] = useState<string | null>(null);
151− const [asking, setAsking] = useState<CardAction | null>(null);
152− const [said, setSaid] = useState<string | null>(null);
138+ const links = (card.actions ?? []).filter((action) => !!action.href);
153139
154140 const open = (href: string) => {
155141 const route = href.startsWith("/") ? routeFor(href) : null;
156142 if (route) router.push(route as never);
157143 else void WebBrowser.openBrowserAsync(href.startsWith("/") ? siteUrl({ site, api: "" }, href) : href);
158− };
159−
160− const run = async (action: CardAction, input?: string | null) => {
161− setBusy(action.id);
162− setSaid(null);
163− try {
164− const answer = await onAction(message, action, input);
165− if (answer) setSaid(answer);
166− void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => undefined);
167− } catch (error) {
168− setSaid(error instanceof Error ? error.message : "That didn't work.");
169− void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => undefined);
170− } finally {
171− setBusy(null);
172− }
173− };
174−
175− const press = (action: CardAction) => {
176− if (action.href) return open(action.href);
177− if (action.input) return setAsking(action);
178− if (action.confirm) {
179− return Alert.alert(action.label, action.confirm, [
180− { text: "Cancel", style: "cancel" },
181− { text: action.label, style: action.style === "danger" ? "destructive" : "default", onPress: () => void run(action) },
182− ]);
183− }
184− void run(action);
185144 };
186145
187146 return (
212171 </View>
213172 ) : null}
214173 </Pressable>
215− {card.actions?.length ? (
174+ {links.length ? (
216175 <View style={styles.actions}>
217− {card.actions.map((action) => {
218− const primary = action.style === "primary";
219− const danger = action.style === "danger";
220− return (
221− <Pressable
222− key={action.id}
223− disabled={!!busy}
224− onPress={() => press(action)}
225− style={({ pressed }) => [
226− styles.action,
227− {
228− backgroundColor: primary ? colors.accent : colors.bg,
229− borderColor: primary ? colors.accent : danger ? colors.danger : colors.lineStrong,
230− opacity: pressed || (busy && busy !== action.id) ? 0.6 : 1,
231− },
232− ]}
233− >
234− {busy === action.id ? (
235− <ActivityIndicator size="small" color={primary ? colors.onAccent : colors.accent} />
236− ) : (
237− <Text size="small" style={{ fontWeight: "700", color: primary ? colors.onAccent : danger ? colors.danger : colors.fg }}>
238− {action.label}
239− </Text>
240− )}
241− </Pressable>
242− );
243− })}
176+ {links.map((action) => (
177+ <Pressable
178+ key={action.id}
179+ accessibilityRole="link"
180+ onPress={() => action.href && open(action.href)}
181+ style={({ pressed }) => [styles.action, { backgroundColor: colors.bg, borderColor: colors.lineStrong, opacity: pressed ? 0.6 : 1 }]}
182+ >
183+ <Text size="small" style={{ fontWeight: "700", color: colors.fg }}>
184+ {action.label}
185+ </Text>
186+ </Pressable>
187+ ))}
244188 </View>
245− ) : null}
246− {said ? (
247− <Text size="small" tone="muted">
248− {said}
249− </Text>
250189 ) : null}
251− <PromptSheet
252− visible={!!asking}
253− title={asking?.label ?? ""}
254− label={asking?.input?.label ?? ""}
255− placeholder={asking?.input?.placeholder ?? undefined}
256− initial={asking?.input?.initial ?? ""}
257− numeric={asking?.input?.kind === "money"}
258− confirm={asking?.label ?? "Done"}
259− onCancel={() => setAsking(null)}
260− onDone={(value) => {
261− const action = asking;
262− setAsking(null);
263− if (action) void run(action, value);
264− }}
265− />
266190 </View>
267191 );
268192 }
+2−8
239239 onError: () => queries.invalidateQueries({ queryKey: ["chat", slug, "messages", channelId] }),
240240 });
241241
242− /** Presses a button on an agent's card: approve, stop, file. */
243− const cardAction = useMutation({
244− mutationFn: ({ message, action, input }: { message: ChatMessage; action: string; input?: string | null }) =>
245− sendIntent<{ ok: boolean; message: string | null }>(client, slug, "card_action", { channel_id: channelId, message_id: message.id, action_id: action, input: input ?? null }),
246− });
247−
248242 const preferences = useMutation({
249243 mutationFn: (change: { starred?: boolean; muted?: boolean }) => sendIntent(client, slug, "preferences", { channel_id: channelId, ...change }),
250244 onSettled: () => queries.invalidateQueries({ queryKey: chatKeys.sidebar(slug) }),
255249 onSettled: () => queries.invalidateQueries({ queryKey: chatKeys.sidebar(slug) }),
256250 });
257251
258− return { react, edit, remove, cardAction, preferences, leave };
252+ return { react, edit, remove, preferences, leave };
259253 }
260254
261255 /** Marks a conversation read up to a message; the sidebar's counts follow. */
271265 const queries = useQueryClient();
272266 const slug = workspace?.slug ?? "";
273267 return {
274− /** Opens (or finds) a direct message with these members: `user:<username>` or `agent:<id>`. */
268+ /** Opens (or finds) a direct message with these people: `user:<username>`. */
275269 dm: useMutation({
276270 mutationFn: (members: string[]) => sendIntent<{ channel: Channel }>(client, slug, "dm", { members }),
277271 onSuccess: () => queries.invalidateQueries({ queryKey: chatKeys.sidebar(slug) }),
+3−1
99 * fields the app shows are named.
1010 */
1111
12+/** `agent` only on what g1t's own agents wrote before they were retired; shown with an Agent badge. */
1213 export type ApiUser = { id: string; username: string; display_username?: string; kind?: "user" | "workspace" | "agent" | "system" };
1314
1415 export type ApiRepo = {
4546 issue: number | null;
4647 title: string;
4748 body: string | null;
48− agent: string;
49+ /** The coding agent it was made with (`claude-code`, `codex`), from the session recorded on it; null when none was. */
50+ agent: string | null;
4951 status: "draft" | "open" | "merged" | "closed";
5052 branch: string | null;
5153 base: string;
+0−61
1−import { pluginRef } from "@g1t/contracts/claude-plugin";
2−import { connectorPath, connectorsFor } from "@g1t/contracts/connectors";
3−import { CUSTOMIZE, type CustomizeSection, customizePath } from "@g1t/contracts/customize";
4−import { FIRST_PARTY_EXTENSIONS } from "@g1t/contracts/marketplace";
5−import { FOUNDATIONAL_SKILLS } from "@g1t/contracts/skills";
6−
7−/**
8− * Customize on the phone: Skills, Connectors and Plugins
9− * (packages/contracts/src/customize.ts). What is listed comes from the
10− * catalogs every g1t ships with; a workspace's own library, connections
11− * and installs are read on the site for now, and added here once the API
12− * serves them. Every item opens where the site shows it: a connector on
13− * Connectors' Discover (an MCP server on its connect page), g1t's own
14− * announced plugins on their pages in the `g1t` marketplace.
15− */
16−
17−export type CustomizeItem = {
18− id: string;
19− name: string;
20− description: string;
21− /** Available, Soon, or what else a listing says about itself. */
22− status: "available" | "soon" | "built-in";
23− /** A group to list it under. */
24− group: string;
25− /** The site page that sets it up, for this workspace. */
26− sitePath: string;
27−};
28−
29−export function customizeItems(section: CustomizeSection, workspace: string): CustomizeItem[] {
30− const fallback = customizePath(section, workspace);
31− if (section === "skills") {
32− return FOUNDATIONAL_SKILLS.map((skill) => ({
33− id: skill.id,
34− name: skill.name,
35− description: skill.description,
36− status: "built-in" as const,
37− group: "In every agent",
38− sitePath: fallback,
39− }));
40− }
41− if (section === "connectors") {
42− return connectorsFor("workspace").map((connector) => ({
43− id: connector.id,
44− name: connector.name,
45− description: connector.description,
46− status: connector.status,
47− group: connector.status === "available" ? "Connect now" : "Coming",
48− sitePath: connector.href ? connectorPath(connector.href, workspace) : `${fallback}?view=discover&directory=${encodeURIComponent(connector.id)}`,
49− }));
50− }
51− return FIRST_PARTY_EXTENSIONS.map((extension) => ({
52− id: extension.id,
53− name: extension.name,
54− description: extension.tagline,
55− status: extension.status,
56− group: `${extension.publisher.name} · ${extension.category}`,
57− sitePath: customizePath("plugins", workspace, encodeURIComponent(pluginRef(extension.id, "g1t"))),
58− }));
59−}
60−
61−export { CUSTOMIZE, customizePath, type CustomizeSection };
+10−10
2525 assert.equal(routeFor("/acme/rocket/issues/new"), null);
2626 });
2727
28−test("Customize's tabs open the app's Customize; its other pages stay on the site", () => {
29− assert.equal(routeFor("/acme/-/customize"), "/customize?workspace=acme");
30− assert.equal(routeFor("/acme/-/customize/plugins"), "/customize/plugins?workspace=acme");
31− assert.equal(routeFor("/acme/-/customize/connectors?view=discover"), "/customize/connectors?workspace=acme");
32− assert.equal(routeFor("/acme/-/customize/plugins/pdf%40anthropic-agent-skills"), null);
33− assert.equal(routeFor("/acme/-/customize/requests"), null);
28+test("old links to Agents, Customize and the Marketplace open Home", () => {
29+ assert.equal(routeFor("/acme/-/agents"), "/home?workspace=acme");
30+ assert.equal(routeFor("/acme/-/agents/skills"), "/home?workspace=acme");
31+ assert.equal(routeFor("/acme/-/agents/skills/pdf"), "/home?workspace=acme");
32+ assert.equal(routeFor("/acme/-/customize"), "/home?workspace=acme");
33+ assert.equal(routeFor("/acme/-/customize/plugins/pdf%40anthropic-agent-skills"), "/home?workspace=acme");
34+ assert.equal(routeFor("/acme/-/marketplace/integrations"), "/home?workspace=acme");
3435 });
3536
36−test("the Marketplace, integrations and the old skill library open Customize", () => {
37− assert.equal(routeFor("/acme/-/marketplace/integrations"), "/customize?workspace=acme");
38− assert.equal(routeFor("/acme/-/agents/skills"), "/customize?workspace=acme");
39− assert.equal(routeFor("/acme/-/agents/skills/pdf"), null, "a skill's own page is the site's");
37+test("the integrations directory stays on the site", () => {
38+ assert.equal(routeFor("/acme/-/integrations"), null);
39+ assert.equal(routeFor("/acme/-/integrations/github"), null);
4040 });
4141
4242 test("the app's sign-in comes back to its own screen, and the site's own pages stay on the site", () => {
+6−14
1111 /** Where the app's OAuth sign-in comes back to. */
1212 export const OAUTH_CALLBACK_PATH = "/oauth/callback";
1313
14−/** Customize's tabs, each a screen of the app's Customize. */
15−const CUSTOMIZE_TABS = new Set(["skills", "connectors", "plugins"]);
14+/**
15+ * Pages a workspace no longer has: Agents (with the skill library),
16+ * Customize and the Marketplace. An old link to one opens Home.
17+ */
18+const RETIRED = new Set(["agents", "customize", "marketplace"]);
1619
1720 /** Path segments a workspace or account can never be named (the site's own pages). */
1821 const NOT_OWNERS = new Set(["settings", "notifications", "oauth", "device", "login", "register", "-"]);
4245 return null;
4346 }
4447 if (third === "overview" && !fourth) return `/code?${workspace}`;
45− // Customize's tabs open the app's Customize, on the same tab; its other
46− // pages (a plugin, a connector, requests) open on the site. The
47− // Marketplace, the integrations settings and the old skill library were
48− // Customize before it had the name.
49− if (third === "customize") {
50− if (!fourth) return `/customize?${workspace}`;
51− if (CUSTOMIZE_TABS.has(fourth) && !fifth) return `/customize/${fourth}?${workspace}`;
52− return null;
53− }
54− if (third === "marketplace" || third === "integrations" || (third === "agents" && fourth === "skills" && !fifth)) {
55− return `/customize?${workspace}`;
56− }
48+ if (third && RETIRED.has(third)) return `/home?${workspace}`;
5749 return null;
5850 }
5951
+1−2
2727 WebBrowser.maybeCompleteAuthSession();
2828
2929 /**
30− * What the app asks for: what Chat, Code and Customize show and do. The
30+ * What the app asks for: what Chat and Code show and do. The
3131 * person can untick any of these on the approval page. `chat:*` opens
3232 * Chat's routes and live sockets on the site, and `notifications:*` the
3333 * phone's registration for pushes (`/-/notify`), and nothing else there.
4747 "workflows:read",
4848 "notifications:read",
4949 "notifications:write",
50− "agents:run",
5150 "memory:read",
5251 "artifacts:read",
5352 ];
+1−1
7070 if (Platform.OS === "android") {
7171 await Notifications.setNotificationChannelAsync(MESSAGES_CHANNEL, {
7272 name: "Messages",
73− description: "Messages to you, mentions, replies, and agents waiting on you.",
73+ description: "Messages to you, mentions and replies.",
7474 importance: Notifications.AndroidImportance.HIGH,
7575 vibrationPattern: [0, 120, 80, 120],
7676 lightColor: "#b6a8ff",
+1−1
7474 });
7575 const me = await client.api<Me>("/user");
7676 if ((me.kind ?? "user") !== "user") {
77− throw new ApiError("forbidden", "Sign in as yourself: a workspace's or an agent's token cannot use the app.");
77+ throw new ApiError("forbidden", "Sign in as yourself: a workspace's token cannot use the app.");
7878 }
7979 const saved = await SecureStore.getItemAsync(WORKSPACE_KEY).catch(() => null);
8080 const workspaces = me.workspaces ?? [];