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 thread | 1 | /** |
| 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 | */ | |
| 9 | import type { DocRole, MemberProfile } from "@g1t/contracts"; | |
| 10 | ||
| 11 | /** The orchestrator every workspace has: the default writer, and @-mentioning it brings it in. */ | |
| 12 | export const ORCHESTRATOR = "g1t"; | |
| 13 | ||
| 14 | /** The longest title the ask carries. */ | |
| 15 | const TITLE_MAX = 120; | |
| 16 | ||
| 17 | /** A thread's link, to cite and to share: the conversation's path (lib/chat.ts `channelPath`) with `?thread=`. */ | |
| 18 | export function threadLink(origin: string, conversationPath: string, rootId: string): string { | |
| 19 | return `${origin.replace(/\/+$/, "")}${conversationPath}?thread=${encodeURIComponent(rootId)}`; | |
| 20 | } | |
| 21 | ||
| 22 | export type WritableSpace = { id: string; name: string }; | |
| 23 | ||
| 24 | /** The Docs spaces a person can write in, not archived, the workspace's default first. */ | |
| 25 | export 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 | ||
| 34 | export type WriteUpAgent = { handle: string; name: string }; | |
| 35 | ||
| 36 | /** Who can write it: @g1t first (always there to ask), then the agents in this conversation. */ | |
| 37 | export 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. */ | |
| 50 | export 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. */ | |
| 59 | export 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 | } |