Skip to content
155 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
2 * The reply's audience and tools, wired to g1t's services. Each call here
3 * runs only after `Audience` and `ToolBox` decided it may (audience.ts,
4 * tools.ts); the backing services check the viewer again, as they do for
5 * every request.
6 */
7import {
8 type ServiceBinding,
9 type User,
10 chatClient,
11 identityClient,
12 reposClient,
13 searchClient,
14 workClient,
15} from "@g1t/contracts";
16
17import type { AudiencePorts, RepoRef } from "./audience.ts";
18import type { FoundMessage, ToolPorts } from "./tools.ts";
19
20export type PortsEnv = {
21 DB: D1Database;
22 CHAT: ServiceBinding;
23 IDENTITY: ServiceBinding;
24 REPOS: ServiceBinding;
25 WORK: ServiceBinding;
26 SEARCH: ServiceBinding;
27};
28
29const refOf = (repo: { id: string; namespace: string; name: string; isPrivate: boolean; defaultBranch: string; forkOf?: string | null }): RepoRef => ({
30 id: repo.id,
31 namespace: repo.namespace,
32 name: repo.name,
33 isPrivate: repo.isPrivate,
34 defaultBranch: repo.defaultBranch,
35 forkOf: repo.forkOf ?? null,
36});
37
38export function audiencePorts(env: PortsEnv, workspace: string, channelId: string): AudiencePorts {
39 const repos = reposClient(env.REPOS);
40 return {
41 async info() {
42 const found = await chatClient(env.CHAT).audience(workspace, channelId);
43 if (!found.ok) throw new Error(`the conversation's audience could not be read: ${found.error.message}`);
44 return found.value;
45 },
46 users: (ids) => identityClient(env.IDENTITY).usersForAudience(ids),
47 workspaceRepos: async (viewer) => (await repos.list(viewer, { namespace: workspace, memberOnly: true })).map(refOf),
48 readable: async (ids, viewer) => (await repos.readable(ids.slice(0, 500), viewer)).map(refOf),
49 };
50}
51
52const found = (m: { channel_id: string; channel: string | null; message: { id: string; author: { name: string }; body: string; created_at: string; card: { title: string } | null } }): FoundMessage => ({
53 channel: m.channel,
54 channel_id: m.channel_id,
55 id: m.message.id,
56 author: m.message.author.name,
57 body: m.message.body || (m.message.card ? `[card: ${m.message.card.title}]` : ""),
58 created_at: m.message.created_at,
59});
60
61export function toolPorts(
62 env: PortsEnv,
63 workspace: string,
64 workspaceId: string,
65 channelId: string,
66 consult: ToolPorts["consult"],
67): ToolPorts {
68 const repos = reposClient(env.REPOS);
69 const work = workClient(env.WORK);
70 const path = (repo: RepoRef) => ({ namespace: repo.namespace, name: repo.name });
71 return {
72 async readFile(repo, viewer, ref, file) {
73 const blob = await repos.blob(path(repo), viewer, ref, file);
74 return blob.ok ? { text: blob.value.text, size: blob.value.size } : null;
75 },
76 async searchCode(viewer, query, repo) {
77 const results = await searchClient(env.SEARCH).search(viewer, repo ? `${query} repo:${repo.namespace}/${repo.name}` : query, { type: "code", perPage: 20 });
78 if (!results.ok) return [];
79 return results.value.hits
80 .filter((hit) => hit.kind === "code" && hit.repo && hit.path)
81 .map((hit) => ({
82 repo: hit.repo!,
83 path: hit.path!,
84 snippet: hit.lines.map((line) => `${line.number}: ${line.parts.map((part) => part.text).join("")}`).join("\n"),
85 }));
86 },
87 async listIssues(repo, viewer, state) {
88 const issues = await work.listIssues(path(repo), viewer, { state });
89 return issues.ok ? issues.value.map((i) => ({ number: i.number, title: i.title, state: i.state, labels: i.labels })) : null;
90 },
91 async getIssue(repo, number, viewer) {
92 const detail = await work.getIssue(path(repo), number, viewer);
93 if (!detail.ok) return null;
94 const { issue, comments } = detail.value;
95 return {
96 number: issue.number,
97 title: issue.title,
98 state: issue.state,
99 body: issue.body,
100 comments: comments.filter((c) => c.kind === "comment").slice(-20).map((c) => ({ author: c.author.username, body: c.body })),
101 };
102 },
103 async getPull(repo, number, viewer) {
104 const detail = await work.getPull(path(repo), number, viewer);
105 if (!detail.ok) return null;
106 const { pull, checks } = detail.value;
107 return {
108 number: pull.number,
109 title: pull.title,
110 status: pull.status,
111 body: pull.body ?? "",
112 checks: checks ? `${checks.status}${checks.results.length ? `: ${checks.results.map((r) => `${r.command} ${r.passed ? "passed" : "failed"}`).join(", ")}` : ""}` : null,
113 };
114 },
115 async recentPulls(list, viewer) {
116 const byId = new Map(list.map((repo) => [repo.id, repo]));
117 const groups = await work.pullsForRepos([...byId.keys()], viewer, 5);
118 return groups
119 .flatMap((group) => [...group.open, ...group.closed].map((pull) => ({ pull, repo: byId.get(group.repoId) })))
120 .filter((entry) => entry.repo)
121 .map(({ pull, repo }) => ({
122 repo: `${repo!.namespace}/${repo!.name}`,
123 number: pull.number,
124 title: pull.title,
125 status: pull.status,
126 updated_at: pull.mergedAt ?? pull.updatedAt,
127 }))
128 .sort((a, b) => b.updated_at.localeCompare(a.updated_at))
129 .slice(0, 25);
130 },
131 async searchMessages(query) {
132 const result = await chatClient(env.CHAT).searchForAgent(workspace, channelId, query, 20);
133 return result.ok ? result.value.map(found) : null;
134 },
135 async readThread(target, id) {
136 const result = await chatClient(env.CHAT).threadForAgent(workspace, channelId, target, id);
137 return result.ok ? result.value.map(found) : null;
138 },
139 async roster(viewer: User | null) {
140 const identity = identityClient(env.IDENTITY);
141 const [members, teams, agents] = await Promise.all([
142 viewer ? identity.listMembers(workspace, viewer).catch(() => null) : Promise.resolve(null),
143 viewer ? identity.listTeams(viewer, workspace).catch(() => null) : Promise.resolve(null),
144 env.DB.prepare("SELECT handle, display_name, title, role FROM agents WHERE workspace_id = ? AND archived_at IS NULL ORDER BY builtin DESC, handle")
145 .bind(workspaceId)
146 .all<{ handle: string; display_name: string; title: string; role: string }>(),
147 ]);
148 const people = members?.ok ? members.value.map((m) => `- @${m.username}${m.name ? ` (${m.name})` : ""}, ${m.role}`).join("\n") : "(people could not be listed)";
149 const teamLines = teams?.ok ? teams.value.map((t) => `- ${t.slug}: ${t.name}${t.description ? `, ${t.description}` : ""}`).join("\n") : "";
150 const agentLines = agents.results.map((a) => `- @${a.handle} (${a.display_name}): ${a.title || a.role}`).join("\n");
151 return `People:\n${people}${teamLines ? `\n\nTeams:\n${teamLines}` : ""}\n\nAgents:\n${agentLines}`;
152 },
153 consult,
154 };
155}

This file's history is long; its oldest lines are credited to the oldest commit read.