| 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 | } |