Skip to content
64 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.

Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread1/**
2 * "Write this up in Docs" from chat (docs/WORKSPACE.md, "Docs"): the
3 * spaces a person can pick, the agents that can write it, and the message
4 * that asks one to. Nothing is written behind anyone's back: the person
5 * posts the ask in the thread, as themselves, and the agent answers it the
6 * usual way (a session if it needs one, then `create_page`). Pure, so it
7 * is tested on its own; components/chat/write-up.tsx draws the dialog.
8 */
9import type { DocRole, MemberProfile } from "@g1t/contracts";
10
11/** The orchestrator every workspace has: the default writer, and @-mentioning it brings it in. */
12export const ORCHESTRATOR = "g1t";
13
14/** The longest title the ask carries. */
15const TITLE_MAX = 120;
16
17/** A thread's link, to cite and to share: the conversation's path (lib/chat.ts `channelPath`) with `?thread=`. */
18export function threadLink(origin: string, conversationPath: string, rootId: string): string {
19 return `${origin.replace(/\/+$/, "")}${conversationPath}?thread=${encodeURIComponent(rootId)}`;
20}
21
22export type WritableSpace = { id: string; name: string };
23
24/** The Docs spaces a person can write in, not archived, the workspace's default first. */
25export function writableSpaces(
26 spaces: readonly { id: string; name: string; viewer_role: DocRole; archived_at: string | null; is_default?: boolean }[],
27): WritableSpace[] {
28 return spaces
29 .filter((s) => (s.viewer_role === "edit" || s.viewer_role === "manage") && !s.archived_at)
30 .sort((a, b) => Number(!!b.is_default) - Number(!!a.is_default))
31 .map((s) => ({ id: s.id, name: s.name }));
32}
33
34export type WriteUpAgent = { handle: string; name: string };
35
36/** Who can write it: @g1t first (always there to ask), then the agents in this conversation. */
37export function writeUpAgents(members: readonly Pick<MemberProfile, "kind" | "name" | "display_name">[]): WriteUpAgent[] {
38 const out: WriteUpAgent[] = [{ handle: ORCHESTRATOR, name: ORCHESTRATOR }];
39 const seen = new Set([ORCHESTRATOR]);
40 for (const member of members) {
41 const handle = member.name.toLowerCase();
42 if (member.kind !== "agent" || !handle || seen.has(handle)) continue;
43 seen.add(handle);
44 out.push({ handle, name: member.display_name.trim() || handle });
45 }
46 return out;
47}
48
49/** A title as the ask quotes it: one line, no double quotes, not too long. */
50export function cleanTitle(title: string | null | undefined): string {
51 const text = String(title ?? "")
52 .replace(/\s+/g, " ")
53 .replace(/"/g, "'")
54 .trim();
55 return text.length > TITLE_MAX ? text.slice(0, TITLE_MAX).trimEnd() : text;
56}
57
58/** What the person posts in the thread to ask for the page. */
59export function writeUpMessage(input: { agent: string; space: string; title?: string | null; link: string }): string {
60 const title = cleanTitle(input.title);
61 const handle = input.agent.replace(/^@/, "").toLowerCase() || ORCHESTRATOR;
62 const space = input.space.replace(/\s+/g, " ").trim();
63 return `@${handle} write this thread up as a Docs page in ${space}${title ? ` titled "${title}"` : ""}: what was decided, why, and what's next. Link this thread as the source: ${input.link}`;
64}