| 1 | /** |
| 2 | * "Write this up as an artifact" from chat (docs.g1t.sh/guides/artifacts/, |
| 3 | * "Write a thread up"): where it goes (a space, Private, or shared with |
| 4 | * the conversation), the agents that can write it, and the message that |
| 5 | * asks one to. Nothing is written behind anyone's back: the person posts |
| 6 | * the ask in the thread, as themselves, and the agent answers it the usual |
| 7 | * way (a session if it needs one, then `create_artifact`). The kind is a |
| 8 | * doc until slides and the other kinds ship. Pure, so it is tested on its own; |
| 9 | * components/chat/write-up.tsx draws the dialog. |
| 10 | */ |
| 11 | import type { DocRole, MemberProfile } from "@g1t/contracts"; |
| 12 | |
| 13 | /** The orchestrator every workspace has: the default writer, and @-mentioning it brings it in. */ |
| 14 | export const ORCHESTRATOR = "g1t"; |
| 15 | |
| 16 | /** The longest title the ask carries. */ |
| 17 | const TITLE_MAX = 120; |
| 18 | |
| 19 | /** A thread's link, to cite and to share: the conversation's path (lib/chat.ts `channelPath`) with `?thread=`. */ |
| 20 | export function threadLink(origin: string, conversationPath: string, rootId: string): string { |
| 21 | return `${origin.replace(/\/+$/, "")}${conversationPath}?thread=${encodeURIComponent(rootId)}`; |
| 22 | } |
| 23 | |
| 24 | export type WritableSpace = { id: string; name: string; is_default: boolean }; |
| 25 | |
| 26 | /** The spaces a person can add to, not archived, the workspace's General space first. */ |
| 27 | export function writableSpaces( |
| 28 | spaces: readonly { id: string; name: string; viewer_role: DocRole; archived_at: string | null; is_default?: boolean }[], |
| 29 | ): WritableSpace[] { |
| 30 | return spaces |
| 31 | .filter((s) => (s.viewer_role === "edit" || s.viewer_role === "manage") && !s.archived_at) |
| 32 | .sort((a, b) => Number(!!b.is_default) - Number(!!a.is_default)) |
| 33 | .map((s) => ({ id: s.id, name: s.name, is_default: !!s.is_default })); |
| 34 | } |
| 35 | |
| 36 | /** Where the artifact goes: a space, the asker's Private, or Private shared with the conversation's people. */ |
| 37 | export type WriteUpWhere = { kind: "space"; id: string; name: string } | { kind: "private" } | { kind: "conversation" }; |
| 38 | |
| 39 | /** |
| 40 | * Where it goes unless the person says otherwise: shared with the |
| 41 | * conversation in a DM or a private channel; in a public channel, the |
| 42 | * General space when they can add to it, else their Private. |
| 43 | */ |
| 44 | export function defaultWhere(shared: boolean, spaces: readonly WritableSpace[]): WriteUpWhere { |
| 45 | if (shared) return { kind: "conversation" }; |
| 46 | const general = spaces.find((s) => s.is_default); |
| 47 | return general ? { kind: "space", id: general.id, name: general.name } : { kind: "private" }; |
| 48 | } |
| 49 | |
| 50 | export type WriteUpAgent = { handle: string; name: string }; |
| 51 | |
| 52 | /** Who can write it: @g1t first (always there to ask), then the agents in this conversation. */ |
| 53 | export function writeUpAgents(members: readonly Pick<MemberProfile, "kind" | "name" | "display_name">[]): WriteUpAgent[] { |
| 54 | const out: WriteUpAgent[] = [{ handle: ORCHESTRATOR, name: ORCHESTRATOR }]; |
| 55 | const seen = new Set([ORCHESTRATOR]); |
| 56 | for (const member of members) { |
| 57 | const handle = member.name.toLowerCase(); |
| 58 | if (member.kind !== "agent" || !handle || seen.has(handle)) continue; |
| 59 | seen.add(handle); |
| 60 | out.push({ handle, name: member.display_name.trim() || handle }); |
| 61 | } |
| 62 | return out; |
| 63 | } |
| 64 | |
| 65 | /** A title as the ask quotes it: one line, no double quotes, not too long. */ |
| 66 | export function cleanTitle(title: string | null | undefined): string { |
| 67 | const text = String(title ?? "") |
| 68 | .replace(/\s+/g, " ") |
| 69 | .replace(/"/g, "'") |
| 70 | .trim(); |
| 71 | return text.length > TITLE_MAX ? text.slice(0, TITLE_MAX).trimEnd() : text; |
| 72 | } |
| 73 | |
| 74 | /** Where, in the words the agent maps to `create_artifact`'s `where`. */ |
| 75 | export function whereWords(where: WriteUpWhere): string { |
| 76 | if (where.kind === "space") return `in the ${where.name.replace(/\s+/g, " ").trim()} space`; |
| 77 | if (where.kind === "private") return "privately (just for me)"; |
| 78 | return "shared with this conversation"; |
| 79 | } |
| 80 | |
| 81 | /** What the person posts in the thread to ask for the artifact. */ |
| 82 | export function writeUpMessage(input: { agent: string; where: WriteUpWhere; title?: string | null; link: string }): string { |
| 83 | const title = cleanTitle(input.title); |
| 84 | const handle = input.agent.replace(/^@/, "").toLowerCase() || ORCHESTRATOR; |
| 85 | return `@${handle} write this thread up as an artifact (a doc) ${whereWords(input.where)}${title ? ` titled "${title}"` : ""}. Link this thread as the source: ${input.link}`; |
| 86 | } |