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.
| Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet | 1 | /** |
| 2 | * Folios: what people call artifacts. Artifacts mode is one mode for docs, | |
| 3 | * slides, designs and dashboards, each private, shared with people and | |
| 4 | * agents, in a space or open to the workspace, and edited live together. | |
| 5 | * Kept by the docs service (`services/docs`). Plan and decisions: | |
| 6 | * docs/ARTIFACTS_MODE.md. | |
| 7 | * | |
| 8 | * Naming: code says "folio", people see "artifact" (UI text, URLs | |
| 9 | * `/<ws>/-/artifacts/...`, the `artifact` MCP tool, REST paths, the | |
| 10 | * `artifacts:*` scopes). The Cloudflare Artifacts git store and workflow | |
| 11 | * run artifacts are something else and keep their names. | |
| 12 | * | |
| 13 | * Wire shapes are snake_case end to end. Each kind's own model is in its | |
| 14 | * own file: folios-slides.ts, folios-design.ts, folios-dashboard.ts. | |
| 15 | * `crates/contracts/src/folios.rs` mirrors what the Rust API needs, and a | |
| 16 | * Rust test runs both validators over `folios.fixtures.json`. | |
| 17 | * | |
| 18 | * Access, in one place (section 2.1 of the plan): | |
| 19 | * | |
| 20 | * - Every folio has one owner, always a person, who has `manage`. | |
| 21 | * - It sits in a space, or in its owner's Private section (`space` null). | |
| 22 | * - A person's role is the highest of: owner; explicit grants on it or an | |
| 23 | * ancestor it inherits from; their space role when it inherits up to a | |
| 24 | * folio in a space; and the general access of its access root | |
| 25 | * (`workspace`: every member; `link`: members who opened the link). | |
| 26 | * - `inherit: false` makes a folio its own access root ("Only people | |
| 27 | * invited"). "Private" (the lock) is computed: only the owner can read it. | |
| 28 | * - An agent's role is never higher than its asker's, narrowed to what | |
| 29 | * every person in the audience can read. | |
| 30 | */ | |
| 31 | import type { MemberProfile } from "./chat"; | |
| 32 | import type { ServiceBinding } from "./clients"; | |
| 33 | import type { DatasetQuery, DatasetResult } from "./datasets"; | |
| 34 | import type { | |
| 35 | DocAgentAbilities, | |
| 36 | DocAgentMode, | |
| 37 | DocAudience, | |
| 38 | DocBlockOutline, | |
| 39 | DocDiffLine, | |
| 40 | DocEditTarget, | |
| 41 | DocRepoSpace, | |
| 42 | DocRole, | |
| 43 | DocSpace, | |
| 44 | DocSpaceKind, | |
| 45 | DocSuggestion, | |
| 46 | DocThread, | |
| 47 | DocThreadAction, | |
| 48 | } from "./docs"; | |
| 49 | import type { DashboardOp, DashboardPreview } from "./folios-dashboard"; | |
| 50 | import type { DesignOp, DesignPreview } from "./folios-design"; | |
| 51 | import type { SlidesOp, SlidesPreview } from "./folios-slides"; | |
| 52 | import type { User } from "./identity"; | |
| 53 | import type { Result } from "./result"; | |
| 54 | ||
| 55 | // ── Kinds ───────────────────────────────────────────────────────────── | |
| 56 | ||
| 57 | export type FolioKind = "doc" | "slides" | "design" | "dashboard"; | |
| 58 | export const FOLIO_KINDS: readonly FolioKind[] = ["doc", "slides", "design", "dashboard"]; | |
| 59 | ||
| 60 | /** The kinds as the Artifacts home's tiles name them. */ | |
| 61 | export const FOLIO_KIND_LABELS: Record<FolioKind, string> = { | |
| 62 | doc: "Docs", | |
| 63 | slides: "Slides", | |
| 64 | design: "Design", | |
| 65 | dashboard: "Dashboard", | |
| 66 | }; | |
| 67 | ||
| 68 | /** One of a kind, in a sentence: "a doc", "a deck"... */ | |
| 69 | export const FOLIO_KIND_NOUNS: Record<FolioKind, string> = { | |
| 70 | doc: "doc", | |
| 71 | slides: "deck", | |
| 72 | design: "design", | |
| 73 | dashboard: "dashboard", | |
| 74 | }; | |
| 75 | ||
| 76 | export function isFolioKind(value: unknown): value is FolioKind { | |
| 77 | return typeof value === "string" && (FOLIO_KINDS as readonly string[]).includes(value); | |
| 78 | } | |
| 79 | ||
| 80 | /** Only a doc holds other folios: a doc with children is the folder. */ | |
| 81 | export function folioCanHaveChildren(kind: FolioKind): boolean { | |
| 82 | return kind === "doc"; | |
| 83 | } | |
| 84 | ||
| 85 | // ── Roles and access ────────────────────────────────────────────────── | |
| 86 | ||
| 87 | /** What someone may do with a folio, weakest first. The same roles as a space's. */ | |
| 88 | export type FolioRole = DocRole; | |
| 89 | export const FOLIO_ROLES: readonly FolioRole[] = ["view", "comment", "edit", "manage"]; | |
| 90 | ||
| 91 | /** Who may open a folio besides its owner, grants and space. */ | |
| 92 | export type FolioGeneralAccess = "none" | "workspace" | "link"; | |
| 93 | export const FOLIO_GENERAL_ACCESS: readonly FolioGeneralAccess[] = ["none", "workspace", "link"]; | |
| 94 | ||
| 95 | export const FOLIO_GENERAL_ACCESS_LABELS: Record<FolioGeneralAccess, string> = { | |
| 96 | none: "Restricted", | |
| 97 | workspace: "Everyone in the workspace", | |
| 98 | link: "Anyone in the workspace with the link", | |
| 99 | }; | |
| 100 | ||
| 101 | /** General access never gives `manage`. */ | |
| 102 | export type FolioGeneralRole = Exclude<FolioRole, "manage">; | |
| 103 | export const FOLIO_GENERAL_ROLES: readonly FolioGeneralRole[] = ["view", "comment", "edit"]; | |
| 104 | ||
| 105 | /** Space kinds as Artifacts names them (the database keeps workspace / team / private). */ | |
| 106 | export const FOLIO_SPACE_KIND_LABELS: Record<DocSpaceKind, string> = { | |
| 107 | workspace: "Open", | |
| 108 | team: "Team", | |
| 109 | private: "Members only", | |
| 110 | }; | |
| 111 | ||
| 112 | /** Who a grant names: `user:<id>`, `agent:<id>` or `team:<slug>`. */ | |
| 113 | export type FolioPrincipal = string; | |
| 114 | ||
| 115 | const PRINCIPAL = /^(user|agent|team):[A-Za-z0-9_.-]{1,64}$/; | |
| 116 | ||
| 117 | export function isFolioPrincipal(value: unknown): value is FolioPrincipal { | |
| 118 | return typeof value === "string" && PRINCIPAL.test(value); | |
| 119 | } | |
| 120 | ||
| 121 | /** The deepest a folio tree goes. */ | |
| 122 | export const FOLIO_MAX_DEPTH = 10; | |
| 123 | /** The longest title, in characters. */ | |
| 124 | export const FOLIO_MAX_TITLE = 200; | |
| 125 | /** The most people a folio is shared with in one change. */ | |
| 126 | export const FOLIO_MAX_SHARE = 50; | |
| 127 | /** A subtree larger than this has its access rebuilt by a queue job, not inline. */ | |
| 128 | export const FOLIO_INLINE_REACL = 2_000; | |
| 129 | ||
| 130 | // ── Addresses ───────────────────────────────────────────────────────── | |
| 131 | ||
| 132 | const FOLIO_ID = /(fol_[0-9a-hjkmnp-tv-z]{26})$/; | |
| 133 | ||
| 134 | /** The words of a title as an address: `q4-roadmap`. At most 50 characters. */ | |
| 135 | export function folioTitleSlug(title: string): string { | |
| 136 | return title | |
| 137 | .normalize("NFKD") | |
| 138 | .replace(/[̀-ͯ]/g, "") | |
| 139 | .toLowerCase() | |
| 140 | .replace(/[^a-z0-9]+/g, "-") | |
| 141 | .replace(/^-+|-+$/g, "") | |
| 142 | .slice(0, 50) | |
| 143 | .replace(/-+$/g, ""); | |
| 144 | } | |
| 145 | ||
| 146 | /** A folio's last address segment, `<title-slug>-<id>`: only the id is read, so renames keep links working. */ | |
| 147 | export function folioSlug(title: string, id: string): string { | |
| 148 | const words = folioTitleSlug(title); | |
| 149 | return words ? `${words}-${id}` : id; | |
| 150 | } | |
| 151 | ||
| 152 | /** A folio's address. Flat: moving it between spaces and Private never breaks a link. */ | |
| 153 | export function folioPath(workspace: string, title: string, id: string): string { | |
| 154 | return `/${workspace}/-/artifacts/${folioSlug(title, id)}`; | |
| 155 | } | |
| 156 | ||
| 157 | /** The folio id at the end of an address segment, or null. */ | |
| 158 | export function folioIdFrom(segment: string | null | undefined): string | null { | |
| 159 | return FOLIO_ID.exec(String(segment ?? ""))?.[1] ?? null; | |
| 160 | } | |
| 161 | ||
| 162 | /** Every folio id a text links to, for backlinks. */ | |
| 163 | export function linkedFolioIds(text: string): string[] { | |
| 164 | return [...new Set(text.match(/fol_[0-9a-hjkmnp-tv-z]{26}/g) ?? [])]; | |
| 165 | } | |
| 166 | ||
| 167 | /** Segments the site's routes use under `-/artifacts/`: never a folio. */ | |
| 168 | export const FOLIO_ROUTE_SEGMENTS: readonly string[] = ["live", "api", "query", "threads", "upload", "export", "new", "templates", "trash", "stale", "spaces", "repo"]; | |
| 169 | ||
| 170 | // ── Folios ──────────────────────────────────────────────────────────── | |
| 171 | ||
| 172 | /** Enough to link to a folio. */ | |
| 173 | export type FolioRef = { | |
| 174 | id: string; | |
| 175 | kind: FolioKind; | |
| 176 | title: string; | |
| 177 | /** An emoji, or null for the kind's icon. */ | |
| 178 | icon: string | null; | |
| 179 | /** `<title-slug>-<id>`. */ | |
| 180 | slug: string; | |
| 181 | /** `/<ws>/-/artifacts/<slug>`. */ | |
| 182 | path: string; | |
| 183 | }; | |
| 184 | ||
| 185 | /** The space a folio sits in. */ | |
| 186 | export type FolioSpaceRef = { id: string; slug: string; name: string; kind: DocSpaceKind }; | |
| 187 | ||
| 188 | /** Where a folio's access comes from when it inherits. */ | |
| 189 | export type FolioInheritedFrom = { kind: "space" | "folio"; id: string; name: string }; | |
| 190 | ||
| 191 | /** What a card draws: written when the folio is saved, never data values. */ | |
| 192 | export type FolioPreview = { kind: "doc"; lines: string[] } | SlidesPreview | DesignPreview | DashboardPreview; | |
| 193 | ||
| 194 | /** A folio as lists and its page show it, for one viewer. */ | |
| 195 | export type Folio = FolioRef & { | |
| 196 | workspace_id: string; | |
| 197 | /** Null: its owner's Private section. */ | |
| 198 | space: FolioSpaceRef | null; | |
| 199 | parent_id: string | null; | |
| 200 | position: number; | |
| 201 | owner: MemberProfile; | |
| 202 | created_by: MemberProfile; | |
| 203 | created_at: string; | |
| 204 | /** Any change: rename, move, share. */ | |
| 205 | updated_at: string; | |
| 206 | /** Content changes: "Edited 45m ago". */ | |
| 207 | edited_by: MemberProfile | null; | |
| 208 | edited_at: string; | |
| 209 | trashed_at: string | null; | |
| 210 | viewer_role: FolioRole; | |
| 211 | favorite: boolean; | |
| 212 | /** Only its owner can read it (the lock). */ | |
| 213 | private: boolean; | |
| 214 | /** How many people, agents and teams it is shared with directly. */ | |
| 215 | shared_count: number; | |
| 216 | /** The access root's general access. */ | |
| 217 | general_access: FolioGeneralAccess; | |
| 218 | general_role: FolioGeneralRole | null; | |
| 219 | /** False: "Only people invited", its own access root. */ | |
| 220 | inherit: boolean; | |
| 221 | inherited_from: FolioInheritedFrom | null; | |
| 222 | /** How agents change it: its own, else its space's, else `suggest`. */ | |
| 223 | agent_mode: DocAgentMode; | |
| 224 | excerpt: string; | |
| 225 | preview: FolioPreview | null; | |
| 226 | /** Where it was written up from, such as a chat thread, when it was. */ | |
| 227 | source: { title: string; href: string } | null; | |
| 228 | /** Possibly out of date: code it cites changed. */ | |
| 229 | stale: boolean; | |
| 230 | has_children: boolean; | |
| 231 | }; | |
| 232 | ||
| 233 | /** A row of the sidebar's trees. */ | |
| 234 | export type FolioTreeNode = { | |
| 235 | id: string; | |
| 236 | kind: FolioKind; | |
| 237 | parent_id: string | null; | |
| 238 | position: number; | |
| 239 | title: string; | |
| 240 | icon: string | null; | |
| 241 | slug: string; | |
| 242 | /** It restricts access below where it sits (a lock on the row). */ | |
| 243 | restricted: boolean; | |
| 244 | stale?: boolean; | |
| 245 | }; | |
| 246 | ||
| 247 | export type FolioListTab = "all" | "yours" | "shared"; | |
| 248 | export const FOLIO_LIST_TABS: readonly FolioListTab[] = ["all", "yours", "shared"]; | |
| 249 | ||
| 250 | export type FolioListQuery = { | |
| 251 | tab: FolioListTab; | |
| 252 | kinds?: FolioKind[] | null; | |
| 253 | space_id?: string | null; | |
| 254 | /** A member key, `user:<id>`. */ | |
| 255 | owner?: string | null; | |
| 256 | /** `owner/name`. */ | |
| 257 | project?: string | null; | |
| 258 | /** Words or meaning; hybrid search when given. */ | |
| 259 | q?: string | null; | |
| 260 | cursor?: string | null; | |
| 261 | limit?: number | null; | |
| 262 | }; | |
| 263 | ||
| 264 | /** The most folios a page of a list holds. */ | |
| 265 | export const FOLIO_LIST_MAX = 100; | |
| 266 | ||
| 267 | export type FolioList = { items: Folio[]; next_cursor: string | null }; | |
| 268 | ||
| 269 | export type FoliosSidebarSpace = DocSpace & { joined: boolean; tree: FolioTreeNode[] }; | |
| 270 | ||
| 271 | export type FoliosSidebar = { | |
| 272 | favorites: FolioRef[]; | |
| 273 | /** Joined open spaces, the viewer's team spaces and Members-only spaces. */ | |
| 274 | spaces: FoliosSidebarSpace[]; | |
| 275 | /** The viewer's own folios in no space. */ | |
| 276 | private_tree: FolioTreeNode[]; | |
| 277 | /** The tops of what is shared with the viewer: the highest ancestor they can read. */ | |
| 278 | shared: FolioRef[]; | |
| 279 | /** Projects' docs: repositories' `docs/` folders, read-only. */ | |
| 280 | repos: DocRepoSpace[]; | |
| 281 | can_create_space: boolean; | |
| 282 | trash_count: number; | |
| 283 | stale_count: number; | |
| 284 | }; | |
| 285 | ||
| 286 | /** Content to start a folio with: Markdown for docs and slides, a spec for designs and dashboards. */ | |
| 287 | export type FolioContentInput = { markdown: string } | { spec: unknown }; | |
| 288 | ||
| 289 | export type NewFolio = { | |
| 290 | kind: FolioKind; | |
| 291 | title?: string | null; | |
| 292 | icon?: string | null; | |
| 293 | /** Null or absent: the creator's Private section. */ | |
| 294 | space_id?: string | null; | |
| 295 | /** A doc to sit under. */ | |
| 296 | parent_id?: string | null; | |
| 297 | template_id?: string | null; | |
| 298 | content?: FolioContentInput | null; | |
| 299 | source?: { title: string; href: string } | null; | |
| 300 | share_with?: { principal: FolioPrincipal; role: FolioRole }[] | null; | |
| 301 | }; | |
| 302 | ||
| 303 | export type FolioChange = { | |
| 304 | title?: string; | |
| 305 | icon?: string | null; | |
| 306 | cover?: string | null; | |
| 307 | projects?: string[]; | |
| 308 | }; | |
| 309 | ||
| 310 | /** Where to put a folio: a space (null: Private) and a parent doc, before a sibling or last. */ | |
| 311 | export type FolioMove = { space_id: string | null; parent_id: string | null; before_id?: string | null }; | |
| 312 | ||
| 313 | // ── Sharing ─────────────────────────────────────────────────────────── | |
| 314 | ||
| 315 | /** Where a row of "Who has access" comes from. */ | |
| 316 | export type FolioAccessSource = | |
| 317 | | { kind: "owner" } | |
| 318 | /** A grant on this folio: editable here. */ | |
| 319 | | { kind: "grant" } | |
| 320 | /** A grant on a parent it inherits from: change it there. */ | |
| 321 | | { kind: "folio"; id: string; title: string; path: string } | |
| 322 | /** The space it inherits from. */ | |
| 323 | | { kind: "space"; id: string; name: string }; | |
| 324 | ||
| 325 | export type FolioAccessRow = { | |
| 326 | principal: FolioPrincipal; | |
| 327 | /** A person or agent; a team is shown by name. */ | |
| 328 | profile: MemberProfile | { kind: "team"; id: string; name: string; display_name: string }; | |
| 329 | role: FolioRole; | |
| 330 | source: FolioAccessSource; | |
| 331 | }; | |
| 332 | ||
| 333 | /** The share dialog. */ | |
| 334 | export type FolioAccessList = { | |
| 335 | folio_id: string; | |
| 336 | owner: MemberProfile; | |
| 337 | rows: FolioAccessRow[]; | |
| 338 | general_access: FolioGeneralAccess; | |
| 339 | general_role: FolioGeneralRole | null; | |
| 340 | inherit: boolean; | |
| 341 | inherited_from: FolioInheritedFrom | null; | |
| 342 | /** The folio's own setting; null follows its space's. */ | |
| 343 | agent_mode: DocAgentMode | null; | |
| 344 | /** The viewer may change any of it. */ | |
| 345 | can_share: boolean; | |
| 346 | /** Public links are off until a workspace turns them on (later). */ | |
| 347 | public_link: "off"; | |
| 348 | }; | |
| 349 | ||
| 350 | /** One change from the share dialog. */ | |
| 351 | export type FolioAccessChange = | |
| 352 | /** Share with someone, or change their role; `notify` is an optional message. */ | |
| 353 | | { op: "grant"; principal: FolioPrincipal; role: FolioRole; notify?: string | null } | |
| 354 | | { op: "revoke"; principal: FolioPrincipal } | |
| 355 | /** `none` takes no role; `workspace` and `link` take one, never `manage`. */ | |
| 356 | | { op: "general"; access: FolioGeneralAccess; role: FolioGeneralRole | null } | |
| 357 | /** Follow the space or parent (true), or "Only people invited" (false). */ | |
| 358 | | { op: "inherit"; inherit: boolean } | |
| 359 | | { op: "agent_mode"; agent_mode: DocAgentMode | null }; | |
| 360 | ||
| 361 | // ── History, proposals, templates ───────────────────────────────────── | |
| 362 | ||
| 363 | export type FolioVersionKind = "created" | "edit" | "agent" | "suggestion" | "proposal" | "restore"; | |
| 364 | ||
| 365 | export type FolioVersion = { | |
| 366 | id: string; | |
| 367 | folio_id: string; | |
| 368 | created_at: string; | |
| 369 | authors: MemberProfile[]; | |
| 370 | kind: FolioVersionKind; | |
| 371 | note: string | null; | |
| 372 | }; | |
| 373 | ||
| 374 | export type FolioVersionDetail = FolioVersion & { | |
| 375 | /** The folio's text rendition then. */ | |
| 376 | text: string; | |
| 377 | /** Against the version before it. */ | |
| 378 | diff: DocDiffLine[]; | |
| 379 | }; | |
| 380 | ||
| 381 | export type FolioProposalStatus = "open" | "accepted" | "rejected" | "stale"; | |
| 382 | ||
| 383 | /** An agent's whole change to a slides deck, design or dashboard, which a person previews and applies or rejects. */ | |
| 384 | export type FolioProposal = { | |
| 385 | id: string; | |
| 386 | folio_id: string; | |
| 387 | author: MemberProfile; | |
| 388 | asked_by: MemberProfile | null; | |
| 389 | note: string | null; | |
| 390 | /** "Adds slides 4–6; rewrites the title slide". */ | |
| 391 | summary: string; | |
| 392 | status: FolioProposalStatus; | |
| 393 | created_at: string; | |
| 394 | decided_by: MemberProfile | null; | |
| 395 | decided_at: string | null; | |
| 396 | }; | |
| 397 | ||
| 398 | /** A doc's tracked change, as Docs has them, on a folio. */ | |
| 399 | export type FolioSuggestion = Omit<DocSuggestion, "page_id"> & { folio_id: string }; | |
| 400 | ||
| 401 | export type FolioTemplate = { | |
| 402 | id: string; | |
| 403 | kind: FolioKind; | |
| 404 | name: string; | |
| 405 | description: string; | |
| 406 | icon: string | null; | |
| 407 | builtin: boolean; | |
| 408 | /** Markdown (doc, slides) or a JSON spec (design, dashboard). */ | |
| 409 | body: string; | |
| 410 | created_by: MemberProfile | null; | |
| 411 | }; | |
| 412 | ||
| 413 | // ── Live ────────────────────────────────────────────────────────────── | |
| 414 | ||
| 415 | /** JSON text frames on a folio's socket, besides the Yjs protocol's binary ones. */ | |
| 416 | export type FoliosLiveEvent = | |
| 417 | | { type: "folio.updated"; folio: Folio } | |
| 418 | | { type: "folio.trashed"; folio_id: string } | |
| 419 | | { type: "suggestion.created" | "suggestion.updated"; suggestion: FolioSuggestion } | |
| 420 | | { type: "proposal.created" | "proposal.updated"; proposal: FolioProposal } | |
| 421 | | { type: "version.created"; version: FolioVersion } | |
| 422 | /** Whether it is possibly out of date changed: ask again. */ | |
| 423 | | { type: "folio.staleness" } | |
| 424 | /** Its sharing changed: ask for the access list again to refresh badges. */ | |
| 425 | | { type: "folio.access" } | |
| 426 | /** The viewer's role changed, or their access ended (`role` null; the socket then closes with 4403). */ | |
| 427 | | { type: "access"; role: FolioRole | null }; | |
| 428 | ||
| 429 | // ── Agents ──────────────────────────────────────────────────────────── | |
| 430 | ||
| 431 | /** Who will see what an agent says. More than 20 people reads as the workspace. */ | |
| 432 | export type FolioAudience = DocAudience; | |
| 433 | ||
| 434 | export type FolioAgentAbilities = DocAgentAbilities; | |
| 435 | ||
| 436 | /** Options on any agent edit. */ | |
| 437 | export type FolioEditOptions = { | |
| 438 | note?: string | null; | |
| 439 | /** Never edit directly: file a suggestion (doc) or a proposal (others). */ | |
| 440 | suggest_only?: boolean | null; | |
| 441 | /** The edit brings it up to date with the code it cites. */ | |
| 442 | marks_current?: boolean | null; | |
| 443 | }; | |
| 444 | ||
| 445 | /** An agent's (or a token's) change to a folio, in the kind's own terms. */ | |
| 446 | export type FolioAgentEdit = ( | |
| 447 | | { kind: "doc"; target: DocEditTarget; markdown: string } | |
| 448 | | { kind: "slides"; ops: SlidesOp[] } | |
| 449 | | { kind: "design"; ops: DesignOp[] } | |
| 450 | | { kind: "dashboard"; ops: DashboardOp[] } | |
| 451 | ) & | |
| 452 | FolioEditOptions; | |
| 453 | ||
| 454 | /** The most ops one edit carries. */ | |
| 455 | export const FOLIO_MAX_OPS = 200; | |
| 456 | ||
| 457 | export type FolioAgentEditResult = | |
| 458 | | { mode: "applied"; version_id: string | null; folio: FolioRef; summary: string } | |
| 459 | | { mode: "suggested"; suggestion: FolioSuggestion; folio: FolioRef } | |
| 460 | | { mode: "proposed"; proposal: FolioProposal; folio: FolioRef }; | |
| 461 | ||
| 462 | /** | |
| 463 | * A folio in the form an agent reads and writes: a doc's Markdown with its | |
| 464 | * block ids; a deck's Markdown with slide ids; a design's node spec; a | |
| 465 | * dashboard's spec (tiles and queries, never values). | |
| 466 | */ | |
| 467 | export type FolioAgentRead = { | |
| 468 | folio: FolioRef & { edited_at: string }; | |
| 469 | space: { id: string; slug: string; name: string; agent_mode: DocAgentMode } | null; | |
| 470 | /** Markdown for doc and slides; JSON text for design and dashboard. */ | |
| 471 | content: string; | |
| 472 | /** doc: top-level blocks, for `blocks` targets. */ | |
| 473 | blocks?: DocBlockOutline[]; | |
| 474 | can: FolioAgentAbilities; | |
| 475 | /** False: someone the agent is talking to can't read it, so don't quote it there. */ | |
| 476 | audience_can_read: boolean; | |
| 477 | }; | |
| 478 | ||
| 479 | /** One passage recalled for an agent: part of a folio, or of a repository's docs. */ | |
| 480 | export type FolioPassage = { | |
| 481 | folio: FolioRef | null; | |
| 482 | repo_file: { repo: string; path: string; href: string } | null; | |
| 483 | space_name: string; | |
| 484 | heading: string | null; | |
| 485 | text: string; | |
| 486 | score: number; | |
| 487 | updated_at: string; | |
| 488 | stale: boolean; | |
| 489 | }; | |
| 490 | ||
| 491 | export type FolioSearchHit = FolioRef & { | |
| 492 | space_name: string | null; | |
| 493 | snippet: string; | |
| 494 | edited_at: string; | |
| 495 | heading?: string | null; | |
| 496 | matched?: "words" | "meaning" | "both" | null; | |
| 497 | }; | |
| 498 | ||
| 499 | // ── Validators (pure; mirrored in Rust) ─────────────────────────────── | |
| 500 | ||
| 501 | function isObject(value: unknown): value is Record<string, unknown> { | |
| 502 | return typeof value === "object" && value !== null && !Array.isArray(value); | |
| 503 | } | |
| 504 | ||
| 505 | const isRole = (value: unknown): value is FolioRole => typeof value === "string" && (FOLIO_ROLES as readonly string[]).includes(value); | |
| 506 | ||
| 507 | /** What is wrong with a new folio, or null. Whether its space and parent exist and allow it is the service's to say. */ | |
| 508 | export function newFolioError(input: NewFolio): string | null { | |
| 509 | if (!isFolioKind(input.kind)) return `There is no kind of artifact called ${String(input.kind)}.`; | |
| 510 | const title = input.title ?? ""; | |
| 511 | if ([...title].length > FOLIO_MAX_TITLE) return `A title is at most ${FOLIO_MAX_TITLE} characters.`; | |
| 512 | const content = input.content ?? null; | |
| 513 | if (content !== null) { | |
| 514 | const wantsMarkdown = input.kind === "doc" || input.kind === "slides"; | |
| 515 | if (wantsMarkdown && typeof (content as { markdown?: unknown }).markdown !== "string") return `A ${FOLIO_KIND_NOUNS[input.kind]} starts from markdown.`; | |
| 516 | if (!wantsMarkdown && !isObject((content as { spec?: unknown }).spec)) return `A ${FOLIO_KIND_NOUNS[input.kind]} starts from a spec.`; | |
| 517 | if (input.template_id != null) return "Start from a template or from content, not both."; | |
| 518 | } | |
| 519 | const share = input.share_with ?? []; | |
| 520 | if (share.length > FOLIO_MAX_SHARE) return `Share with at most ${FOLIO_MAX_SHARE} at once.`; | |
| 521 | for (const row of share) { | |
| 522 | if (!isFolioPrincipal(row.principal)) return `${String(row.principal)} is not user:, agent: or team: and an id.`; | |
| 523 | if (!isRole(row.role)) return `There is no role called ${String(row.role)}.`; | |
| 524 | } | |
| 525 | return null; | |
| 526 | } | |
| 527 | ||
| 528 | /** What is wrong with a change from the share dialog, or null. */ | |
| 529 | export function folioAccessChangeError(change: FolioAccessChange): string | null { | |
| 530 | switch (change.op) { | |
| 531 | case "grant": | |
| 532 | if (!isFolioPrincipal(change.principal)) return `${String(change.principal)} is not user:, agent: or team: and an id.`; | |
| 533 | if (!isRole(change.role)) return `There is no role called ${String(change.role)}.`; | |
| 534 | if (change.notify != null && [...change.notify].length > 2_000) return "A message is at most 2000 characters."; | |
| 535 | return null; | |
| 536 | case "revoke": | |
| 537 | return isFolioPrincipal(change.principal) ? null : `${String(change.principal)} is not user:, agent: or team: and an id.`; | |
| 538 | case "general": | |
| 539 | if (!(FOLIO_GENERAL_ACCESS as readonly string[]).includes(change.access)) return `There is no general access called ${String(change.access)}.`; | |
| 540 | if (change.access === "none") return change.role === null ? null : "Restricted takes no role."; | |
| 541 | if (change.role === null) return `${FOLIO_GENERAL_ACCESS_LABELS[change.access]} needs a role.`; | |
| 542 | return (FOLIO_GENERAL_ROLES as readonly string[]).includes(change.role) ? null : "General access gives view, comment or edit, never full access."; | |
| 543 | case "inherit": | |
| 544 | return typeof change.inherit === "boolean" ? null : "inherit is true or false."; | |
| 545 | case "agent_mode": | |
| 546 | return change.agent_mode === null || change.agent_mode === "suggest" || change.agent_mode === "edit" ? null : "agent_mode is suggest, edit or null."; | |
| 547 | default: | |
| 548 | return `There is no access change called ${String((change as { op?: unknown }).op)}.`; | |
| 549 | } | |
| 550 | } | |
| 551 | ||
| 552 | /** | |
| 553 | * What is wrong with an agent edit's envelope, or null: its kind, and a | |
| 554 | * doc's target and Markdown or the other kinds' list of ops. Each op is | |
| 555 | * checked by its kind's validator (`slidesOpError`, `designOpError`, | |
| 556 | * `dashboardOpError`). | |
| 557 | */ | |
| 558 | export function folioAgentEditError(edit: unknown): string | null { | |
| 559 | if (!isObject(edit)) return "An edit is an object."; | |
| 560 | if (!isFolioKind(edit.kind)) return `There is no kind of artifact called ${String(edit.kind)}.`; | |
| 561 | if (edit.note != null && typeof edit.note !== "string") return "note is text."; | |
| 562 | if (edit.kind === "doc") { | |
| 563 | if (typeof edit.markdown !== "string") return "A doc edit has markdown."; | |
| 564 | if (edit.ops !== undefined) return "A doc edit has a target and markdown, not ops."; | |
| 565 | const target = edit.target; | |
| 566 | if (!isObject(target)) return "A doc edit has a target."; | |
| 567 | switch (target.kind) { | |
| 568 | case "append": | |
| 569 | case "document": | |
| 570 | return null; | |
| 571 | case "section": | |
| 572 | return typeof target.heading === "string" && target.heading.length > 0 ? null : "A section target names its heading."; | |
| 573 | case "blocks": | |
| 574 | return typeof target.from_block === "string" && typeof target.to_block === "string" ? null : "A blocks target names from_block and to_block."; | |
| 575 | default: | |
| 576 | return "A doc edit's target is append, document, section or blocks."; | |
| 577 | } | |
| 578 | } | |
| 579 | if (edit.markdown !== undefined || edit.target !== undefined) return `A ${FOLIO_KIND_NOUNS[edit.kind]} edit has ops, not a target and markdown.`; | |
| 580 | if (!Array.isArray(edit.ops) || edit.ops.length === 0) return `A ${FOLIO_KIND_NOUNS[edit.kind]} edit has a list of ops.`; | |
| 581 | if (edit.ops.length > FOLIO_MAX_OPS) return `An edit has at most ${FOLIO_MAX_OPS} ops.`; | |
| 582 | return edit.ops.every((op) => isObject(op) && typeof op.op === "string") ? null : "Each op is an object with an op."; | |
| 583 | } | |
| 584 | ||
| 585 | /** What is wrong with a list query, or null. */ | |
| 586 | export function folioListQueryError(query: FolioListQuery): string | null { | |
| 587 | if (!(FOLIO_LIST_TABS as readonly string[]).includes(query.tab)) return "tab is all, yours or shared."; | |
| 588 | for (const kind of query.kinds ?? []) if (!isFolioKind(kind)) return `There is no kind of artifact called ${String(kind)}.`; | |
| 589 | const limit = query.limit ?? null; | |
| 590 | if (limit !== null && (!Number.isInteger(limit) || limit < 1 || limit > FOLIO_LIST_MAX)) return `limit is between 1 and ${FOLIO_LIST_MAX}.`; | |
| 591 | return null; | |
| 592 | } | |
| 593 | ||
| 594 | // ── The docs service's folio RPC ────────────────────────────────────── | |
| 595 | ||
| 596 | /** Every method the docs service answers for folios at `/rpc/<method>` (plan section 7). */ | |
| 597 | export const FOLIO_RPC_METHODS = [ | |
| 598 | // Lists and navigation. | |
| 599 | "folio_list", | |
| 600 | "folio_sidebar", | |
| 601 | "folio", | |
| 602 | // Changing folios. | |
| 603 | "create_folio", | |
| 604 | "update_folio", | |
| 605 | "move_folio", | |
| 606 | "duplicate_folio", | |
| 607 | "trash_folio", | |
| 608 | "restore_folio", | |
| 609 | "delete_folio", | |
| 610 | "folio_trash", | |
| 611 | "favorite_folio", | |
| 612 | // A person's (or a token's) own read and edit in the agent form. | |
| 613 | "folio_content", | |
| 614 | "edit_folio", | |
| 615 | // Sharing and spaces. | |
| 616 | "folio_access", | |
| 617 | "set_folio_grant", | |
| 618 | "set_folio_general_access", | |
| 619 | "request_folio_access", | |
| 620 | "join_space", | |
| 621 | "leave_space", | |
| 622 | // Search, history, templates and export. | |
| 623 | "search_folios", | |
| 624 | "folio_versions", | |
| 625 | "folio_version", | |
| 626 | "restore_folio_version", | |
| 627 | "folio_templates", | |
| 628 | "save_folio_template", | |
| 629 | "delete_folio_template", | |
| 630 | "export_folio", | |
| 631 | // Suggestions, proposals and comments. | |
| 632 | "folio_suggestions", | |
| 633 | "decide_folio_suggestion", | |
| 634 | "folio_proposals", | |
| 635 | "decide_folio_proposal", | |
| 636 | "folio_thread", | |
| 637 | "folio_threads", | |
| 638 | // Dashboards. | |
| 639 | "query_tile", | |
| 640 | "query_dataset", | |
| 641 | "query_dataset_for_agent", | |
| 642 | // Agents. | |
| 643 | "folios_for_agent", | |
| 644 | "read_folio_for_agent", | |
| 645 | "create_folio_as_agent", | |
| 646 | "edit_folio_as_agent", | |
| 647 | "share_folio_as_agent", | |
| 648 | "recall_folios_for_agent", | |
| 649 | "stale_folios_for_agent", | |
| 650 | "mark_folio_current", | |
| 651 | "reindex_folios", | |
| 652 | ] as const; | |
| 653 | ||
| 654 | export type FolioRpcMethod = (typeof FOLIO_RPC_METHODS)[number]; | |
| 655 | ||
| 656 | export type FoliosApi = { | |
| 657 | // ── The site, and the API acting for a person ─────────────────────── | |
| 658 | list(workspace: string, viewer: User, query: FolioListQuery): Promise<Result<FolioList>>; | |
| 659 | sidebar(workspace: string, viewer: User): Promise<Result<FoliosSidebar>>; | |
| 660 | /** A folio and the viewer's role in it; records the visit (which is what makes a link folio readable). Not found when they can't read it. */ | |
| 661 | folio(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 662 | create(workspace: string, viewer: User, input: NewFolio): Promise<Result<Folio>>; | |
| 663 | update(workspace: string, viewer: User, folioId: string, change: FolioChange): Promise<Result<Folio>>; | |
| 664 | /** Edit role where it is and where it goes. Refuses moving under itself, under a non-doc, or deeper than `FOLIO_MAX_DEPTH`. */ | |
| 665 | move(workspace: string, viewer: User, folioId: string, move: FolioMove): Promise<Result<Folio>>; | |
| 666 | duplicate(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 667 | /** To the trash, with everything under it. */ | |
| 668 | trash(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 669 | restore(workspace: string, viewer: User, folioId: string): Promise<Result<Folio>>; | |
| 670 | /** For good: only from the trash, manage role. */ | |
| 671 | delete(workspace: string, viewer: User, folioId: string): Promise<Result<boolean>>; | |
| 672 | trashed(workspace: string, viewer: User): Promise<Result<Folio[]>>; | |
| 673 | favorite(workspace: string, viewer: User, folioId: string, on: boolean): Promise<Result<boolean>>; | |
| 674 | /** The folio in its agent form, for a person or their token. */ | |
| 675 | content(workspace: string, viewer: User, folioId: string): Promise<Result<FolioAgentRead>>; | |
| 676 | /** Applies an edit in the kind's terms as the viewer: edit role, else a suggestion or proposal with comment role. */ | |
| 677 | edit(workspace: string, viewer: User, folioId: string, edit: FolioAgentEdit): Promise<Result<FolioAgentEditResult>>; | |
| 678 | ||
| 679 | access(workspace: string, viewer: User, folioId: string): Promise<Result<FolioAccessList>>; | |
| 680 | /** One change from the share dialog: grants go to `set_folio_grant`, the rest to `set_folio_general_access`. */ | |
| 681 | changeAccess(workspace: string, viewer: User, folioId: string, change: FolioAccessChange): Promise<Result<FolioAccessList>>; | |
| 682 | /** Asks the owner and managers for access; they get an inbox item. */ | |
| 683 | requestAccess(workspace: string, viewer: User, folioId: string, message?: string | null): Promise<Result<boolean>>; | |
| 684 | joinSpace(workspace: string, viewer: User, spaceId: string): Promise<Result<boolean>>; | |
| 685 | leaveSpace(workspace: string, viewer: User, spaceId: string): Promise<Result<boolean>>; | |
| 686 | ||
| 687 | search(workspace: string, viewer: User, query: { q: string; kinds?: FolioKind[] | null; space_id?: string | null; project?: string | null; owner?: string | null; mode?: "words" | "hybrid" | null; limit?: number | null }): Promise<Result<FolioSearchHit[]>>; | |
| 688 | versions(workspace: string, viewer: User, folioId: string): Promise<Result<FolioVersion[]>>; | |
| 689 | version(workspace: string, viewer: User, folioId: string, versionId: string): Promise<Result<FolioVersionDetail>>; | |
| 690 | restoreVersion(workspace: string, viewer: User, folioId: string, versionId: string): Promise<Result<FolioVersion>>; | |
| 691 | templates(workspace: string, viewer: User, kind?: FolioKind | null): Promise<Result<FolioTemplate[]>>; | |
| 692 | saveTemplate(workspace: string, viewer: User, input: { folio_id: string; name: string; description?: string | null }): Promise<Result<FolioTemplate>>; | |
| 693 | deleteTemplate(workspace: string, viewer: User, templateId: string): Promise<Result<boolean>>; | |
| 694 | export(workspace: string, viewer: User, folioId: string, format?: "markdown" | "json" | null): Promise<Result<{ filename: string; content_type: string; body: string }>>; | |
| 695 | ||
| 696 | suggestions(workspace: string, viewer: User, folioId: string): Promise<Result<FolioSuggestion[]>>; | |
| 697 | decideSuggestion(workspace: string, viewer: User, suggestionId: string, decision: "accept" | "reject"): Promise<Result<FolioSuggestion>>; | |
| 698 | proposals(workspace: string, viewer: User, folioId: string): Promise<Result<FolioProposal[]>>; | |
| 699 | decideProposal(workspace: string, viewer: User, proposalId: string, decision: "accept" | "reject"): Promise<Result<FolioProposal>>; | |
| 700 | thread(workspace: string, viewer: User, folioId: string, action: DocThreadAction): Promise<Result<unknown>>; | |
| 701 | threads(workspace: string, viewer: User, folioId: string): Promise<Result<DocThread[]>>; | |
| 702 | ||
| 703 | /** A dashboard tile's numbers for the viewer: the tile's query, read from the room. */ | |
| 704 | queryTile(workspace: string, viewer: User, folioId: string, tileId: string): Promise<Result<DatasetResult>>; | |
| 705 | /** A query as the viewer, for the API. */ | |
| 706 | queryDataset(workspace: string, viewer: User, query: DatasetQuery): Promise<Result<DatasetResult>>; | |
| 707 | ||
| 708 | // ── Agents (services/agents) ──────────────────────────────────────── | |
| 709 | // | |
| 710 | // Each takes the agent and the person it acts for (`viewer`). The agent | |
| 711 | // never reads or changes more than that person can, narrowed to what | |
| 712 | // everyone in `audience` can read. A folio it may not read is not found. | |
| 713 | ||
| 714 | foliosForAgent(workspace: string, agentId: string, viewer: User, query: FolioListQuery, audience?: FolioAudience | null): Promise<Result<FolioList>>; | |
| 715 | readForAgent(workspace: string, agentId: string, viewer: User, folioId: string, audience?: FolioAudience | null): Promise<Result<FolioAgentRead>>; | |
| 716 | /** | |
| 717 | * A new folio: the viewer owns it, the agent made it and gets `edit` on | |
| 718 | * it. `where` is a space the viewer can edit, `private`, or | |
| 719 | * `conversation` (Private plus `view` for these people). | |
| 720 | */ | |
| 721 | createAsAgent( | |
| 722 | workspace: string, | |
| 723 | agentId: string, | |
| 724 | viewer: User, | |
| 725 | input: { kind: FolioKind; title: string; content?: FolioContentInput | null; template_id?: string | null; where: { space_id: string } | "private" | { conversation: string[] }; parent_id?: string | null; source?: { title: string; href: string } | null }, | |
| 726 | ): Promise<Result<FolioRef>>; | |
| 727 | editAsAgent(workspace: string, agentId: string, viewer: User, folioId: string, edit: FolioAgentEdit): Promise<Result<FolioAgentEditResult>>; | |
| 728 | /** `view` or `comment` for people already in the conversation, when the viewer has `manage`. Never general access, `edit` or `manage`. */ | |
| 729 | shareAsAgent(workspace: string, agentId: string, viewer: User, folioId: string, input: { user_ids: string[]; role: "view" | "comment" }, audience: FolioAudience): Promise<Result<FolioAccessList>>; | |
| 730 | recallForAgent(workspace: string, agentId: string, viewer: User, input: { query: string; limit?: number | null; spaces?: string[] | null; kinds?: FolioKind[] | null }, audience?: FolioAudience | null): Promise<Result<FolioPassage[]>>; | |
| 731 | staleForAgent(workspace: string, agentId: string, viewer: User, options?: { repo?: string | null; since?: string | null }, audience?: FolioAudience | null): Promise<Result<Folio[]>>; | |
| 732 | /** As the asker narrowed to repositories every audience member can read; spend only when the audience is the asker alone. */ | |
| 733 | queryDatasetForAgent(workspace: string, agentId: string, viewer: User, input: { query: DatasetQuery } | { folio_id: string; tile_id: string }, audience?: FolioAudience | null): Promise<Result<DatasetResult>>; | |
| 734 | markCurrent(workspace: string, viewer: User, folioId: string): Promise<Result<boolean>>; | |
| 735 | /** Indexes the workspace's folios again in the background. Workspace owners. */ | |
| 736 | reindex(workspace: string, viewer: User): Promise<Result<boolean>>; | |
| 737 | }; | |
| 738 | ||
| 739 | export function foliosClient(service: ServiceBinding): FoliosApi { | |
| 740 | const call = async <T>(method: FolioRpcMethod, args: object): Promise<T> => { | |
| 741 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 742 | method: "POST", | |
| 743 | headers: { "content-type": "application/json" }, | |
| 744 | body: JSON.stringify(args), | |
| 745 | }); | |
| 746 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 747 | return (await response.json()) as T; | |
| 748 | }; | |
| 749 | return { | |
| 750 | list: (workspace, viewer, query) => call("folio_list", { workspace, viewer, query }), | |
| 751 | sidebar: (workspace, viewer) => call("folio_sidebar", { workspace, viewer }), | |
| 752 | folio: (workspace, viewer, folioId) => call("folio", { workspace, viewer, folio_id: folioId }), | |
| 753 | create: (workspace, viewer, input) => call("create_folio", { workspace, viewer, input }), | |
| 754 | update: (workspace, viewer, folioId, change) => call("update_folio", { workspace, viewer, folio_id: folioId, change }), | |
| 755 | move: (workspace, viewer, folioId, move) => call("move_folio", { workspace, viewer, folio_id: folioId, move }), | |
| 756 | duplicate: (workspace, viewer, folioId) => call("duplicate_folio", { workspace, viewer, folio_id: folioId }), | |
| 757 | trash: (workspace, viewer, folioId) => call("trash_folio", { workspace, viewer, folio_id: folioId }), | |
| 758 | restore: (workspace, viewer, folioId) => call("restore_folio", { workspace, viewer, folio_id: folioId }), | |
| 759 | delete: (workspace, viewer, folioId) => call("delete_folio", { workspace, viewer, folio_id: folioId }), | |
| 760 | trashed: (workspace, viewer) => call("folio_trash", { workspace, viewer }), | |
| 761 | favorite: (workspace, viewer, folioId, on) => call("favorite_folio", { workspace, viewer, folio_id: folioId, on }), | |
| 762 | content: (workspace, viewer, folioId) => call("folio_content", { workspace, viewer, folio_id: folioId }), | |
| 763 | edit: (workspace, viewer, folioId, edit) => call("edit_folio", { workspace, viewer, folio_id: folioId, edit }), | |
| 764 | access: (workspace, viewer, folioId) => call("folio_access", { workspace, viewer, folio_id: folioId }), | |
| 765 | changeAccess: (workspace, viewer, folioId, change) => | |
| 766 | call(change.op === "grant" || change.op === "revoke" ? "set_folio_grant" : "set_folio_general_access", { workspace, viewer, folio_id: folioId, change }), | |
| 767 | requestAccess: (workspace, viewer, folioId, message) => call("request_folio_access", { workspace, viewer, folio_id: folioId, message: message ?? null }), | |
| 768 | joinSpace: (workspace, viewer, spaceId) => call("join_space", { workspace, viewer, space_id: spaceId }), | |
| 769 | leaveSpace: (workspace, viewer, spaceId) => call("leave_space", { workspace, viewer, space_id: spaceId }), | |
| 770 | search: (workspace, viewer, query) => call("search_folios", { workspace, viewer, query }), | |
| 771 | versions: (workspace, viewer, folioId) => call("folio_versions", { workspace, viewer, folio_id: folioId }), | |
| 772 | version: (workspace, viewer, folioId, versionId) => call("folio_version", { workspace, viewer, folio_id: folioId, version_id: versionId }), | |
| 773 | restoreVersion: (workspace, viewer, folioId, versionId) => call("restore_folio_version", { workspace, viewer, folio_id: folioId, version_id: versionId }), | |
| 774 | templates: (workspace, viewer, kind) => call("folio_templates", { workspace, viewer, kind: kind ?? null }), | |
| 775 | saveTemplate: (workspace, viewer, input) => call("save_folio_template", { workspace, viewer, input }), | |
| 776 | deleteTemplate: (workspace, viewer, templateId) => call("delete_folio_template", { workspace, viewer, template_id: templateId }), | |
| 777 | export: (workspace, viewer, folioId, format) => call("export_folio", { workspace, viewer, folio_id: folioId, format: format ?? null }), | |
| 778 | suggestions: (workspace, viewer, folioId) => call("folio_suggestions", { workspace, viewer, folio_id: folioId }), | |
| 779 | decideSuggestion: (workspace, viewer, suggestionId, decision) => call("decide_folio_suggestion", { workspace, viewer, suggestion_id: suggestionId, decision }), | |
| 780 | proposals: (workspace, viewer, folioId) => call("folio_proposals", { workspace, viewer, folio_id: folioId }), | |
| 781 | decideProposal: (workspace, viewer, proposalId, decision) => call("decide_folio_proposal", { workspace, viewer, proposal_id: proposalId, decision }), | |
| 782 | thread: (workspace, viewer, folioId, action) => call("folio_thread", { workspace, viewer, folio_id: folioId, action }), | |
| 783 | threads: (workspace, viewer, folioId) => call("folio_threads", { workspace, viewer, folio_id: folioId }), | |
| 784 | queryTile: (workspace, viewer, folioId, tileId) => call("query_tile", { workspace, viewer, folio_id: folioId, tile_id: tileId }), | |
| 785 | queryDataset: (workspace, viewer, query) => call("query_dataset", { workspace, viewer, query }), | |
| 786 | foliosForAgent: (workspace, agentId, viewer, query, audience) => call("folios_for_agent", { workspace, agent_id: agentId, viewer, query, audience: audience ?? null }), | |
| 787 | readForAgent: (workspace, agentId, viewer, folioId, audience) => call("read_folio_for_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, audience: audience ?? null }), | |
| 788 | createAsAgent: (workspace, agentId, viewer, input) => call("create_folio_as_agent", { workspace, agent_id: agentId, viewer, input }), | |
| 789 | editAsAgent: (workspace, agentId, viewer, folioId, edit) => call("edit_folio_as_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, edit }), | |
| 790 | shareAsAgent: (workspace, agentId, viewer, folioId, input, audience) => | |
| 791 | call("share_folio_as_agent", { workspace, agent_id: agentId, viewer, folio_id: folioId, ...input, audience }), | |
| 792 | recallForAgent: (workspace, agentId, viewer, input, audience) => | |
| 793 | call("recall_folios_for_agent", { workspace, agent_id: agentId, viewer, ...input, audience: audience ?? null }), | |
| 794 | staleForAgent: (workspace, agentId, viewer, options, audience) => | |
| 795 | call("stale_folios_for_agent", { workspace, agent_id: agentId, viewer, repo: options?.repo ?? null, since: options?.since ?? null, audience: audience ?? null }), | |
| 796 | queryDatasetForAgent: (workspace, agentId, viewer, input, audience) => | |
| 797 | call("query_dataset_for_agent", { workspace, agent_id: agentId, viewer, ...input, audience: audience ?? null }), | |
| 798 | markCurrent: (workspace, viewer, folioId) => call("mark_folio_current", { workspace, viewer, folio_id: folioId }), | |
| 799 | reindex: (workspace, viewer) => call("reindex_folios", { workspace, viewer }), | |
| 800 | }; | |
| 801 | } |