Skip to content
86 linesCodeBlameRaw
1/**
2 * "Write this up as an artifact" from chat (docs/ARTIFACTS_MODE.md section
3 * 4.4): where it goes (a space, Private, or shared with the conversation),
4 * the agents that can write it, and the message that asks one to. Nothing
5 * is written behind anyone's back: the person posts the ask in the thread,
6 * as themselves, and the agent answers it the usual way (a session if it
7 * needs one, then `create_artifact`). The kind is a doc until slides and
8 * the other kinds ship. Pure, so it is tested on its own;
9 * components/chat/write-up.tsx draws the dialog.
10 */
11import type { DocRole, MemberProfile } from "@g1t/contracts";
12
13/** The orchestrator every workspace has: the default writer, and @-mentioning it brings it in. */
14export const ORCHESTRATOR = "g1t";
15
16/** The longest title the ask carries. */
17const TITLE_MAX = 120;
18
19/** A thread's link, to cite and to share: the conversation's path (lib/chat.ts `channelPath`) with `?thread=`. */
20export function threadLink(origin: string, conversationPath: string, rootId: string): string {
21 return `${origin.replace(/\/+$/, "")}${conversationPath}?thread=${encodeURIComponent(rootId)}`;
22}
23
24export 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. */
27export 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. */
37export 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 */
44export 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
50export type WriteUpAgent = { handle: string; name: string };
51
52/** Who can write it: @g1t first (always there to ask), then the agents in this conversation. */
53export 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. */
66export 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`. */
75export 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. */
82export 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}