diff --git a/README.md b/README.md index f6f85ec..14ae110 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,16 @@ Lets you peek at a thread's status and repository context without leaving the si Install: `bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/thread-hover-cards --yes` +### Timeline Comments + +Keeps comments and comment threads attached to exact text in bb timelines. + +![Timeline Comments in bb](plugins/timeline-comments/docs/screenshot.png) + +[Source](plugins/timeline-comments) · [README](plugins/timeline-comments/README.md) + +Install: `bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/timeline-comments --yes` + ### Thread Organizer Files new threads into the right existing work section while preserving native titles and every manual override. diff --git a/package-lock.json b/package-lock.json index 8da825e..2a16263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4009,6 +4009,10 @@ "resolved": "plugins/thread-organizer", "link": true }, + "node_modules/bb-plugin-timeline-comments": { + "resolved": "plugins/timeline-comments", + "link": true + }, "node_modules/bb-plugin-ui-patterns": { "resolved": "plugins/ui-patterns", "link": true @@ -5059,6 +5063,22 @@ "node": "20 || >=22" } }, + "node_modules/lucide": { + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.474.0.tgz", + "integrity": "sha512-2rfORNh9fzhCUKu0ciE48ogR1Zg7hleABLO5MBF06vOb4esTtE8FbfQJiC+wveiNFoeojWKz6zXheUToRR5uIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.474.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", + "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -5240,6 +5260,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.20", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", @@ -6702,6 +6769,35 @@ "bbPluginSdk": "^0.4.0" } }, + "plugins/timeline-comments": { + "name": "bb-plugin-timeline-comments", + "version": "0.1.0", + "license": "UNLICENSED", + "devDependencies": { + "@bb/plugin-sdk": "file:../../tooling/vendor/bb-plugin-sdk-0.4.0.tgz", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "better-sqlite3": "^12.10.0", + "cron-parser": "^5.5.0", + "esbuild": "^0.28.0", + "hono": "^4.11.9", + "jsdom": "^29.0.1", + "lucide": "^0.474.0", + "lucide-react": "^0.474.0", + "playwright": "^1.55.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.7.0", + "vitest": "^4.1.8", + "zod": "^4.3.6" + }, + "engines": { + "bb": ">=0.0.34", + "bbPluginSdk": "^0.4.0" + } + }, "plugins/ui-patterns": { "name": "bb-plugin-ui-patterns", "version": "0.1.0", @@ -7288,15 +7384,6 @@ "url": "https://opencollective.com/parcel" } }, - "plugins/ui-patterns/node_modules/lucide-react": { - "version": "0.474.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.474.0.tgz", - "integrity": "sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "plugins/ui-patterns/node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", diff --git a/plugins/timeline-comments/README.md b/plugins/timeline-comments/README.md new file mode 100644 index 0000000..fe774e2 --- /dev/null +++ b/plugins/timeline-comments/README.md @@ -0,0 +1,41 @@ +# Timeline Comments + +Timeline Comments keeps review notes attached to the exact part of a bb conversation they refer to. + +## Screenshots + +Select message text to add a comment without leaving the timeline. + +![Comment action in the floating text-selection menu](docs/selection-action.png) + +Comments stay attached through an underline and a compact nearest-gutter thread. + +![An anchored comment thread with a reply](docs/screenshot.png) + +## Use + +- Adds **Comment** to the floating menu when you select user or agent message text. +- Keeps open comment threads visible through a quiet underline and the nearest gutter marker. +- Provides replies, inline editing, deletion, resolve/reopen controls, and a thread-scoped Comments panel. +- Adds every open comment to the current thread's draft without submitting it. + +Comments are stored in plugin-owned SQLite on the bb server. Missing or ambiguous source text remains manageable as **Unanchored** and is never attached to a guess. + +## Install + +```bash +bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/timeline-comments --yes +``` + +Timeline Comments requires bb 0.0.34 or newer and the 0.4 plugin SDK. + +## Develop + +From the repository root: + +```bash +npm ci +npm run check --workspace=bb-plugin-timeline-comments +npm run test:browser --workspace=bb-plugin-timeline-comments +bb plugin install "path:$PWD/plugins/timeline-comments" --yes +``` diff --git a/plugins/timeline-comments/anchors.test.ts b/plugins/timeline-comments/anchors.test.ts new file mode 100644 index 0000000..b335ec4 --- /dev/null +++ b/plugins/timeline-comments/anchors.test.ts @@ -0,0 +1,130 @@ +// @vitest-environment jsdom +import { describe, expect, it } from "vitest"; +import { + chooseNearestGutter, + layoutGutterMarkers, + restoreSelector, +} from "./anchors.js"; + +const selector = ( + overrides: Partial[1]> = {}, +) => ({ + version: 1 as const, + coordinateSpace: "rendered-text-utf16" as const, + start: 6, + end: 15, + exact: "nested 😀", + prefix: "alpha ", + suffix: " omega", + ...overrides, +}); + +describe("restoreSelector", () => { + it("restores UTF-16 offsets across nested rendered nodes", () => { + const root = document.createElement("div"); + root.append("alpha "); + const strong = document.createElement("strong"); + strong.append("nested "); + root.append(strong, "😀 omega"); + const restored = restoreSelector(root, selector()); + expect(restored?.strategy).toBe("offset"); + expect(restored?.range.toString()).toBe("nested 😀"); + }); + + it("uses surrounding context only when it resolves one exact copy", () => { + const root = document.createElement("div"); + root.textContent = "wrong target here; right target done"; + expect( + restoreSelector( + root, + selector({ + start: 0, + end: 6, + exact: "target", + prefix: "right ", + suffix: " done", + }), + )?.range.toString(), + ).toBe("target"); + }); + + it("returns unanchored for ambiguous context instead of guessing", () => { + const root = document.createElement("div"); + root.textContent = "same target same target"; + expect( + restoreSelector( + root, + selector({ + start: 99, + end: 105, + exact: "target", + prefix: "same ", + suffix: "", + }), + ), + ).toBeNull(); + }); + + it("accepts a unique exact phrase when old context no longer matches", () => { + const root = document.createElement("div"); + root.textContent = "updated unique phrase ending"; + expect( + restoreSelector( + root, + selector({ + start: 99, + end: 105, + exact: "unique", + prefix: "old ", + suffix: " context", + }), + )?.range.toString(), + ).toBe("unique"); + }); +}); + +describe("gutter layout", () => { + const rail = { left: 100, right: 900, width: 800 }; + + it("uses the closest fragment edge and breaks ties toward the right", () => { + expect(chooseNearestGutter([{ left: 130, right: 400 }], rail)).toBe("left"); + expect(chooseNearestGutter([{ left: 400, right: 600 }], rail)).toBe( + "right", + ); + expect(chooseNearestGutter([{ left: 110, right: 890 }], rail)).toBe( + "right", + ); + }); + + it("keeps narrow timelines on the right gutter", () => { + expect( + chooseNearestGutter([{ left: 102, right: 200 }], { ...rail, width: 480 }), + ).toBe("right"); + }); + + it("de-overlaps markers and groups visual overflow", () => { + const markers = Array.from({ length: 5 }, (_, index) => ({ + id: String(index), + desiredY: 20, + })); + const placements = layoutGutterMarkers(markers, 0, 80, 24, 4); + expect(placements).toHaveLength(1); + expect(placements.flatMap(({ ids }) => ids)).toHaveLength(5); + expect(placements.at(-1)!.y + 24).toBeLessThanOrEqual(80); + }); + + it("keeps separated markers distinct while clustering local collisions", () => { + const placements = layoutGutterMarkers( + [ + { id: "a", desiredY: 20 }, + { id: "b", desiredY: 40 }, + { id: "c", desiredY: 90 }, + ], + 0, + 140, + 24, + 4, + ); + expect(placements.map(({ ids }) => ids)).toEqual([["a", "b"], ["c"]]); + }); +}); diff --git a/plugins/timeline-comments/anchors.ts b/plugins/timeline-comments/anchors.ts new file mode 100644 index 0000000..a9c7e55 --- /dev/null +++ b/plugins/timeline-comments/anchors.ts @@ -0,0 +1,203 @@ +export interface StoredSelector { + version: 1; + coordinateSpace: "rendered-text-utf16"; + start: number; + end: number; + exact: string; + prefix: string; + suffix: string; +} + +export interface RestoredAnchor { + range: Range; + strategy: "offset" | "context"; +} + +interface TextSegment { + node: Text; + start: number; + end: number; +} + +function indexText(root: Element): { text: string; segments: TextSegment[] } { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + const segments: TextSegment[] = []; + let text = ""; + for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) { + const value = node.nodeValue ?? ""; + segments.push({ + node: node as Text, + start: text.length, + end: text.length + value.length, + }); + text += value; + } + return { text, segments }; +} + +function boundaryAt( + segments: TextSegment[], + offset: number, + preferNext: boolean, +): { node: Text; offset: number } | null { + for (const [index, segment] of segments.entries()) { + if ( + offset < segment.end || + (offset === segment.end && (!preferNext || index === segments.length - 1)) + ) { + return { node: segment.node, offset: offset - segment.start }; + } + if (preferNext && offset === segment.end) continue; + } + return null; +} + +function rangeForOffsets( + segments: TextSegment[], + start: number, + end: number, +): Range | null { + const startBoundary = boundaryAt(segments, start, true); + const endBoundary = boundaryAt(segments, end, false); + if (startBoundary === null || endBoundary === null) return null; + const range = document.createRange(); + range.setStart(startBoundary.node, startBoundary.offset); + range.setEnd(endBoundary.node, endBoundary.offset); + return range; +} + +function contextMatches( + text: string, + at: number, + selector: StoredSelector, +): boolean { + const prefix = text.slice(Math.max(0, at - selector.prefix.length), at); + const suffixStart = at + selector.exact.length; + const suffix = text.slice(suffixStart, suffixStart + selector.suffix.length); + return prefix === selector.prefix && suffix === selector.suffix; +} + +/** Restore only an exact, unambiguous rendered-text range. Never guesses. */ +export function restoreSelector( + root: Element, + selector: StoredSelector, +): RestoredAnchor | null { + const { text, segments } = indexText(root); + if ( + text.slice(selector.start, selector.end) === selector.exact && + contextMatches(text, selector.start, selector) + ) { + const range = rangeForOffsets(segments, selector.start, selector.end); + return range === null ? null : { range, strategy: "offset" }; + } + + const occurrences: number[] = []; + const candidates: number[] = []; + let at = text.indexOf(selector.exact); + while (at !== -1) { + occurrences.push(at); + if (contextMatches(text, at, selector)) candidates.push(at); + at = text.indexOf(selector.exact, at + 1); + } + const start = + candidates.length === 1 + ? candidates[0] + : occurrences.length === 1 + ? occurrences[0] + : undefined; + if (start === undefined) return null; + const range = rangeForOffsets(segments, start, start + selector.exact.length); + return range === null ? null : { range, strategy: "context" }; +} + +export type GutterSide = "left" | "right"; + +export function chooseNearestGutter( + fragments: readonly Pick[], + rail: Pick, +): GutterSide { + if (rail.width < 520 || fragments.length === 0) return "right"; + const leftDistance = Math.min( + ...fragments.map((rect) => Math.abs(rect.left - rail.left)), + ); + const rightDistance = Math.min( + ...fragments.map((rect) => Math.abs(rail.right - rect.right)), + ); + return leftDistance < rightDistance ? "left" : "right"; +} + +export interface MarkerCandidate { + id: string; + desiredY: number; +} + +export interface MarkerPlacement { + ids: string[]; + y: number; +} + +/** + * De-overlap one gutter. Locally colliding candidates cluster immediately; + * any remaining rail overflow collapses into the smallest possible group. + */ +export function layoutGutterMarkers( + candidates: readonly MarkerCandidate[], + top: number, + bottom: number, + markerSize = 24, + gap = 4, +): MarkerPlacement[] { + if (candidates.length === 0 || bottom <= top) return []; + const sorted = [...candidates].sort( + (a, b) => a.desiredY - b.desiredY || a.id.localeCompare(b.id), + ); + const capacity = Math.max( + 1, + Math.floor((bottom - top + gap) / (markerSize + gap)), + ); + const proximityGroups: MarkerCandidate[][] = []; + for (const candidate of sorted) { + const previous = proximityGroups.at(-1); + const previousDesiredY = previous?.at(-1)?.desiredY; + if ( + previous !== undefined && + previousDesiredY !== undefined && + candidate.desiredY - previousDesiredY < markerSize + gap + ) { + previous.push(candidate); + } else { + proximityGroups.push([candidate]); + } + } + + const groups = + proximityGroups.length <= capacity + ? proximityGroups + : [ + ...proximityGroups.slice(0, capacity - 1), + proximityGroups.slice(capacity - 1).flat(), + ]; + + const placements = groups.map((group) => ({ + ids: group.map(({ id }) => id), + y: Math.min( + bottom - markerSize, + Math.max( + top, + group.reduce((sum, item) => sum + item.desiredY, 0) / group.length - + markerSize / 2, + ), + ), + })); + for (let index = 1; index < placements.length; index += 1) { + placements[index]!.y = Math.max( + placements[index]!.y, + placements[index - 1]!.y + markerSize + gap, + ); + } + const overflow = placements.at(-1)!.y + markerSize - bottom; + if (overflow > 0) { + for (const placement of placements) placement.y -= overflow; + } + return placements; +} diff --git a/plugins/timeline-comments/app.css b/plugins/timeline-comments/app.css new file mode 100644 index 0000000..568ea94 --- /dev/null +++ b/plugins/timeline-comments/app.css @@ -0,0 +1,675 @@ +.bb-comments-overlay { + position: fixed; + inset: 0; + z-index: 52; + pointer-events: none; +} + +.bb-comments-marker { + position: fixed; + display: grid; + width: 32px; + height: 32px; + place-items: center; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + box-shadow: none; + color: var(--muted-foreground); + font: + 600 10px/1 ui-sans-serif, + system-ui, + sans-serif; + pointer-events: auto; + cursor: pointer; +} + +.bb-comments-marker svg { + width: 20px; + height: 20px; +} +.bb-comments-marker-cluster { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; +} +[data-bb-comment-gutter="left"].bb-comments-marker-cluster { + flex-direction: row-reverse; +} +.bb-comments-marker-cluster svg { + width: 20px; + height: 20px; +} +.bb-comments-marker-count { + min-width: 6px; + font-size: 9px; + line-height: 1; +} +.bb-comments-marker:hover, +.bb-comments-marker:focus-visible { + background: var(--surface-recessed); + color: var(--foreground); + outline: none; +} + +.bb-comments-composer-action-wrap { + display: inline-flex; + align-items: center; + gap: 4px; +} +.bb-comments-composer-action-error { + color: var(--destructive-text, var(--muted-foreground)); + font-size: 10px; + line-height: 1; + white-space: nowrap; +} +.bb-comments-composer-action { + display: grid; + width: 28px; + height: 28px; + place-items: center; + padding: 0; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted-foreground); + cursor: pointer; +} +.bb-comments-composer-action:hover { + background: var(--surface-recessed); + color: var(--foreground); +} +.bb-comments-composer-action:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 1px; +} + +.bb-comments-composer, +.bb-comments-popover { + position: fixed; + z-index: 54; + border: 1px solid var(--border); + border-radius: 11px; + background: var(--popover); + box-shadow: 0 8px 24px + color-mix(in oklab, var(--foreground) 12%, transparent); + color: var(--popover-foreground); + outline: none; + font: + 13px/1.45 ui-sans-serif, + system-ui, + sans-serif; +} + +.bb-comments-composer { + width: 288px; + overflow: hidden; + padding: 0; +} +.bb-comments-textarea, +.bb-comments-reply-input, +.bb-comments-edit-input, +.bb-comments-panel textarea { + width: 100%; + box-sizing: border-box; + resize: vertical; + border: 1px solid var(--border); + border-radius: 7px; + background: var(--background); + color: var(--foreground); + font: inherit; + outline: none; +} +.bb-comments-textarea { + min-height: 64px; + padding: 10px 12px; + border: 0; + background: transparent; + resize: none; +} +.bb-comments-textarea:focus, +.bb-comments-reply-input:focus, +.bb-comments-edit-input:focus, +.bb-comments-panel textarea:focus { + box-shadow: 0 0 0 1px var(--ring); +} +.bb-comments-edit-input:focus, +.bb-comments-panel-edit textarea:focus { + box-shadow: none; +} +.bb-comments-composer-footer, +.bb-comments-thread-header, +.bb-comments-comment-actions, +.bb-comments-header-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; +} +.bb-comments-composer-footer { + min-height: 38px; + padding: 6px 8px 6px 12px; + border-top: 1px solid var(--border-seam, var(--border)); + background: color-mix( + in oklab, + var(--surface-recessed) 42%, + var(--popover) + ); +} +.bb-comments-composer > .bb-comments-error { + padding-right: 12px; + padding-left: 12px; +} +.bb-comments-hint { + color: var(--muted-foreground); + font-size: 10px; +} +.bb-comments-primary, +.bb-comments-quiet, +.bb-comments-icon-button, +.bb-comments-icon-control, +.bb-comments-cluster-row, +.bb-comments-panel button { + font: inherit; + cursor: pointer; +} +.bb-comments-primary { + min-height: 28px; + padding: 0 9px; + border: 0; + border-radius: 6px; + background: var(--primary); + color: var(--primary-foreground); + font-size: 11px; + font-weight: 600; +} +.bb-comments-primary:disabled { + cursor: default; + opacity: 0.45; +} +.bb-comments-icon-control { + display: grid; + width: 24px; + height: 24px; + flex: 0 0 24px; + place-items: center; + padding: 0; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--muted-foreground); +} +.bb-comments-icon-control:hover, +.bb-comments-icon-control[aria-pressed="true"] { + background: var(--surface-recessed); + color: var(--foreground); +} +.bb-comments-icon-control:focus-visible { + outline: 1px solid var(--ring); + outline-offset: 1px; +} +.bb-comments-icon-control:disabled { + cursor: default; + opacity: 0.4; +} +.bb-comments-icon-control svg { + width: 14px; + height: 14px; +} +.bb-comments-error, +.bb-comments-panel-error { + padding: 6px 0; + color: var(--destructive-text, var(--foreground)); + font-size: 11px; +} + +.bb-comments-thread { + display: flex; + width: 384px; + max-width: calc(100vw - 32px); + max-height: min(82vh, calc(100vh - 72px)); + flex-direction: column; + overflow: hidden; + border-radius: 12px; +} +.bb-comments-thread-header { + min-height: 34px; + box-sizing: border-box; + flex: 0 0 auto; + padding: 8px 10px 8px 12px; + border-bottom: 1px solid var(--border-seam, var(--border)); + background: color-mix( + in oklab, + var(--surface-recessed) 58%, + var(--popover) + ); +} +.bb-comments-thread-source { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + margin-right: 4px; + overflow: hidden; + color: var(--foreground); + font-size: 12px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} +.bb-comments-thread-source svg { + width: 13px; + height: 13px; + color: var(--muted-foreground); +} +.bb-comments-quiet, +.bb-comments-icon-button { + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted-foreground); + font-size: 11px; +} +.bb-comments-quiet { + min-height: 28px; + padding: 0 6px; + white-space: nowrap; +} +.bb-comments-quiet:hover, +.bb-comments-icon-button:hover { + background: var(--surface-recessed); + color: var(--foreground); +} +.bb-comments-destructive:hover, +.bb-comments-destructive:focus-visible { + color: var(--destructive-text, var(--foreground)); +} +.bb-comments-icon-button { + width: 24px; + height: 24px; + font-size: 18px; +} +.bb-comments-thread-comments { + min-height: 0; + overflow-y: auto; +} +.bb-comments-comment { + position: relative; + padding: 14px 14px 10px; + border-bottom: 1px solid + color-mix(in oklab, var(--border-seam, var(--border)) 55%, transparent); +} +.bb-comments-comment[data-editing="true"] { background: transparent; } +.bb-comments-comment-meta { + color: var(--muted-foreground); + font-size: 10px; +} +.bb-comments-message-header { + display: flex; + min-height: 20px; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0; +} +.bb-comments-message-header > div:first-child { + display: flex; + min-width: 0; + align-items: center; + gap: 6px; +} +.bb-comments-message-header strong { + color: var(--foreground); + font-size: 10px; + font-weight: 600; +} +.bb-comments-message-header time { + color: var(--muted-foreground); + font-size: 10px; + font-weight: 300; +} +.bb-comments-comment-body { + margin: 6px 0 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.55; +} +.bb-comments-comment-actions { + gap: 2px; + justify-content: flex-end; +} +.bb-comments-reply { + display: block; + padding: 10px 12px; + border-top: 1px solid var(--border-seam, var(--border)); + background: color-mix( + in oklab, + var(--surface-recessed) 42%, + var(--popover) + ); +} +.bb-comments-thread[data-editing="true"] .bb-comments-reply { + display: none; +} +.bb-comments-reply .bb-comments-error { + grid-column: 1 / -1; + padding: 0; +} +.bb-comments-reply-input { + field-sizing: content; + min-height: 24px; + max-height: 128px; + padding: 2px 0; + border: 0; + background: transparent; + line-height: 1.5; + resize: none; +} +.bb-comments-edit-input { + field-sizing: content; + min-height: 24px; + max-height: 128px; + padding: 2px 0; + border: 0; + background: transparent; + line-height: 1.5; + resize: none; +} +.bb-comments-inline-composer { + position: relative; + display: block; + box-sizing: border-box; + min-height: 38px; + margin-top: 8px; + padding: 4px 8px; + border: 1px solid color-mix(in oklab, var(--border) 55%, transparent); + border-radius: 12px; + background: color-mix(in oklab, var(--surface-recessed) 44%, var(--popover)); + transition: border-color 120ms ease; +} +.bb-comments-reply .bb-comments-inline-composer { + margin-top: 0; +} +.bb-comments-inline-composer:focus-within { + border-color: var(--border); +} +.bb-comments-inline-composer + :is(.bb-comments-reply-input, .bb-comments-edit-input) { + padding-right: 41px; +} +.bb-comments-inline-composer[data-multiline="true"] { + padding-bottom: 32px; +} +.bb-comments-inline-composer[data-multiline="true"] + :is(.bb-comments-reply-input, .bb-comments-edit-input) { + padding-right: 0; +} +.bb-comments-submit-shortcut { + position: absolute; + right: 8px; + bottom: 6px; + display: flex; + min-width: 36px; + height: 24px; + align-items: center; + justify-content: center; + gap: 1px; + padding: 0 5px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted-foreground); + font: inherit; + font-size: 10px; + cursor: pointer; +} +.bb-comments-submit-shortcut svg { + width: 13px; + height: 13px; +} +.bb-comments-submit-shortcut:hover:not(:disabled) { + background: var(--surface-recessed); + color: var(--foreground); +} +.bb-comments-submit-shortcut:disabled { + cursor: default; + opacity: 0.3; +} +.bb-comments-actions-menu { + flex: 0 0 auto; +} +.bb-comments-actions-popover { + position: fixed; + z-index: 56; + display: grid; + min-width: 132px; + max-width: calc(100vw - 16px); + max-height: calc(100vh - 16px); + overflow: auto; + padding: 4px; + border: 1px solid var(--border); + border-radius: 7px; + background: var(--popover); + box-shadow: 0 8px 24px + color-mix(in oklab, var(--foreground) 14%, transparent); +} +.bb-comments-actions-popover button { + display: flex; + min-height: 28px; + align-items: center; + gap: 7px; + padding: 0 8px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--foreground); + font-size: 11px; + text-align: left; +} +.bb-comments-actions-popover button svg { + width: 13px; + height: 13px; + color: var(--muted-foreground); +} +.bb-comments-actions-popover button:hover { + background: var(--surface-recessed); +} +.bb-comments-loading { + padding: 14px; + color: var(--muted-foreground); + text-align: center; +} +.bb-comments-cluster { + padding: 4px; +} +.bb-comments-cluster-row { + display: block; + width: 100%; + padding: 7px 8px; + overflow: hidden; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--foreground); + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} +.bb-comments-cluster-row:hover { + background: var(--surface-recessed); +} + +.bb-comments-panel { + display: flex; + height: 100%; + min-height: 0; + flex-direction: column; + background: var(--background); + color: var(--foreground); + font: + 13px/1.45 ui-sans-serif, + system-ui, + sans-serif; +} +.bb-comments-filters { + display: flex; + gap: 3px; + padding: 6px 10px; + border-bottom: 1px solid var(--border-seam, var(--border)); + background: color-mix( + in oklab, + var(--surface-recessed) 45%, + transparent + ); +} +.bb-comments-filters button { + min-height: 26px; + padding: 0 8px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted-foreground); + font-size: 11px; +} +.bb-comments-filters button[aria-pressed="true"] { + background: var(--surface-recessed); + color: var(--foreground); +} +.bb-comments-panel-error { + padding: 8px 12px; + border-bottom: 1px solid var(--border); +} +.bb-comments-panel-list { + min-height: 0; + flex: 1; + overflow-y: auto; +} +.bb-comments-panel-row { + border-bottom: 1px solid + color-mix(in oklab, var(--border-seam, var(--border)) 68%, transparent); +} +.bb-comments-panel-row[data-active="true"] { + background: color-mix( + in oklab, + var(--surface-recessed) 72%, + transparent + ); +} +.bb-comments-row-summary { + display: grid; + width: 100%; + gap: 2px; + padding: 9px 12px; + border: 0; + background: transparent; + color: var(--foreground); + text-align: left; +} +.bb-comments-row-summary:hover { + background: var(--surface-recessed); +} +.bb-comments-row-source { + overflow: hidden; + color: var(--muted-foreground); + font-size: 10px; + text-overflow: ellipsis; + white-space: nowrap; +} +.bb-comments-row-body { + font-size: 12px; + line-height: 1.45; +} +.bb-comments-row-meta { + color: var(--muted-foreground); + font-size: 10px; +} +.bb-comments-panel-thread { + margin: 0 8px 8px; + overflow: visible; + border: 1px solid var(--border-seam, var(--border)); + border-radius: 12px; + background: var(--popover); +} +.bb-comments-panel-thread-header { + display: flex; + min-height: 34px; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 5px 7px 5px 10px; + border-bottom: 1px solid var(--border-seam, var(--border)); + background: color-mix( + in oklab, + var(--surface-recessed) 55%, + var(--popover) + ); +} +.bb-comments-panel-thread-header > span { + min-width: 0; + overflow: hidden; + color: var(--foreground); + font-size: 12px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} +.bb-comments-panel-comment { + position: relative; + padding: 14px 14px 10px; + border-bottom: 1px solid + color-mix(in oklab, var(--border-seam, var(--border)) 55%, transparent); +} +.bb-comments-panel-comment[data-editing="true"] { background: transparent; } +.bb-comments-panel-comment p { + margin: 6px 0 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + line-height: 1.55; +} +.bb-comments-panel-edit { + display: grid; + margin-top: 8px; +} +.bb-comments-panel-edit textarea { + field-sizing: content; + min-height: 34px; + max-height: 180px; + padding: 4px 6px; + border: 0; + background: transparent; + resize: none; +} +.bb-comments-panel-reply { + display: block; + padding: 10px 12px; + background: color-mix( + in oklab, + var(--surface-recessed) 42%, + var(--popover) + ); +} +.bb-comments-panel-reply textarea { + min-height: 28px; + max-height: 112px; + padding: 4px 6px; + border: 0; + background: transparent; + resize: none; +} +.bb-comments-load-more { + display: block; + margin: 10px auto; + padding: 5px 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--background); + color: var(--muted-foreground); + font-size: 11px; +} +.bb-comments-empty { + padding: 28px 16px; + color: var(--muted-foreground); + text-align: center; +} diff --git a/plugins/timeline-comments/app.test.tsx b/plugins/timeline-comments/app.test.tsx new file mode 100644 index 0000000..f2ab145 --- /dev/null +++ b/plugins/timeline-comments/app.test.tsx @@ -0,0 +1,475 @@ +// @vitest-environment jsdom +import { act, cleanup, fireEvent } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { PluginThreadPanelProps } from "@bb/plugin-sdk/app"; +import { + loadPluginApp, + mountPluginContentScripts, + renderSlot, +} from "./test/plugin-sdk-app-harness.js"; +import type { timelineCommentsRpcContract } from "./server.js"; + +afterEach(cleanup); + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +const thread = { + id: "comment_thread_1", + bbThreadId: "thr_1", + messageId: "msg_1", + messageRole: "assistant" as const, + selector: { + version: 1 as const, + coordinateSpace: "rendered-text-utf16" as const, + start: 0, + end: 6, + exact: "source", + prefix: "", + suffix: "", + }, + version: 1, + createdAt: 1, + updatedAt: 1, + resolvedAt: null, + rootComment: { + id: "comment_1", + threadId: "comment_thread_1", + parentId: null, + body: "Make the API explicit.", + version: 1, + createdAt: 1, + updatedAt: 1, + }, + replyCount: 0, +}; + +describe("timeline comments app", () => { + it("registers a selection action, composer action, Comments panel, and one content script", async () => { + const app = await loadPluginApp(() => import("./app.js")); + expect(app.messageActions).toMatchObject([ + { + id: "comment-selection", + title: "Comment", + placements: ["selection-menu"], + }, + ]); + expect(app.threadPanelActions).toMatchObject([ + { + id: "comments", + title: "Comments", + icon: "MessageSquare", + layout: "flush", + }, + ]); + expect(app.composerCustomizations).toMatchObject([ + { + id: "timeline-comments", + scopes: ["thread"], + actions: [{ id: "add-comments" }], + }, + ]); + expect(app.contentScripts.map(({ id }) => id)).toEqual([ + "timeline-comment-anchors", + ]); + }); + + it("adds open comments to the draft from the thread composer action", async () => { + const app = await loadPluginApp(() => import("./app.js")); + const action = renderSlot( + app.composerCustomizations[0]!.actions![0]!, + {}, + { + context: { threadId: "thr_1" }, + composer: { + text: "Keep this draft", + scope: { kind: "thread", threadId: "thr_1" }, + }, + rpc: { + getThreadHandoffSummary: () => ({ + threadCount: 1, + commentCount: 1, + codePointSize: 100, + }), + }, + }, + ); + + fireEvent.click( + action.getByRole("button", { name: "Add comments to chat" }), + ); + await vi.waitFor(() => + expect(action.inspection.composer.mentions).toHaveLength(1), + ); + expect(action.inspection.composer.mentions[0]).toEqual({ + provider: "thread-comments", + id: "thr_1", + label: "1 comment from 1 open thread", + }); + expect(action.inspection.composer.text).toContain("Keep this draft"); + expect(action.inspection.navigateCalls).toEqual([]); + }); + + it("shows a compact error and recovers when adding comments is retried", async () => { + const getThreadHandoffSummary = vi + .fn() + .mockRejectedValueOnce(new Error("Summary unavailable")) + .mockResolvedValueOnce({ + threadCount: 1, + commentCount: 2, + codePointSize: 100, + }); + const app = await loadPluginApp(() => import("./app.js")); + const action = renderSlot( + app.composerCustomizations[0]!.actions![0]!, + {}, + { + context: { threadId: "thr_1" }, + composer: { + text: "", + scope: { kind: "thread", threadId: "thr_1" }, + }, + rpc: { getThreadHandoffSummary }, + }, + ); + + fireEvent.click( + action.getByRole("button", { name: "Add comments to chat" }), + ); + expect((await action.findByRole("alert")).textContent).toBe( + "Couldn’t add comments", + ); + expect(action.inspection.composer.mentions).toHaveLength(0); + + fireEvent.click( + action.getByRole("button", { name: "Retry adding comments to chat" }), + ); + await vi.waitFor(() => + expect(action.inspection.composer.mentions).toHaveLength(1), + ); + expect(action.queryByRole("alert")).toBeNull(); + expect(getThreadHandoffSummary).toHaveBeenCalledTimes(2); + }); + + it("does not add a handoff from a composer scope that changed in flight", async () => { + const staleSummary = deferred<{ + threadCount: number; + commentCount: number; + codePointSize: number; + }>(); + const getThreadHandoffSummary = vi + .fn() + .mockReturnValueOnce(staleSummary.promise) + .mockResolvedValueOnce({ + threadCount: 1, + commentCount: 2, + codePointSize: 100, + }); + const app = await loadPluginApp(() => import("./app.js")); + const action = renderSlot( + app.composerCustomizations[0]!.actions![0]!, + {}, + { + context: { threadId: "thr_1" }, + composer: { + text: "", + scope: { kind: "thread", threadId: "thr_1" }, + }, + rpc: { getThreadHandoffSummary }, + }, + ); + + fireEvent.click( + action.getByRole("button", { name: "Add comments to chat" }), + ); + await vi.waitFor(() => + expect(getThreadHandoffSummary).toHaveBeenCalledWith({ + bbThreadId: "thr_1", + }), + ); + await action.behavior.setComposerScope({ + kind: "thread", + threadId: "thr_2", + }); + await act(async () => { + staleSummary.resolve({ + threadCount: 1, + commentCount: 1, + codePointSize: 100, + }); + await staleSummary.promise; + }); + expect( + ( + action.getByRole("button", { + name: "Add comments to chat", + }) as HTMLButtonElement + ).disabled, + ).toBe(false); + expect(action.inspection.composer.mentions).toHaveLength(0); + expect(action.queryByRole("alert")).toBeNull(); + + fireEvent.click( + action.getByRole("button", { name: "Add comments to chat" }), + ); + await vi.waitFor(() => + expect(action.inspection.composer.mentions).toHaveLength(1), + ); + expect(action.inspection.composer.mentions[0]?.id).toBe("thr_2"); + expect(getThreadHandoffSummary).toHaveBeenLastCalledWith({ + bbThreadId: "thr_2", + }); + }); + + it("removes every content-script node and tolerates repeated disposal", async () => { + const app = await loadPluginApp(() => import("./app.js")); + const scripts = await mountPluginContentScripts(app, { + pluginId: "timeline-comments", + }); + expect( + document.querySelectorAll("[data-bb-timeline-comments-owned]"), + ).toHaveLength(3); + await scripts.lifecycle.dispose(); + await scripts.lifecycle.dispose(); + expect( + document.querySelectorAll("[data-bb-timeline-comments-owned]"), + ).toHaveLength(0); + }); + + it("shows the comment list without a duplicate title or chat action", async () => { + const app = await loadPluginApp(() => import("./app.js")); + const panel = renderSlot< + PluginThreadPanelProps, + typeof timelineCommentsRpcContract + >( + app.threadPanelActions[0]!, + { + threadId: "thr_1", + params: null, + revealMessage: vi.fn(async () => "revealed" as const), + }, + { + context: { threadId: "thr_1" }, + rpc: { + listCommentThreads: () => ({ threads: [thread], nextCursor: null }), + getThreadHandoffSummary: () => ({ + threadCount: 1, + commentCount: 1, + codePointSize: 100, + }), + getCommentThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + createThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + reply: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + updateComment: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + deleteComment: () => ({ deletedThreadId: null, thread: null }), + setThreadResolved: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + listOpenAnchors: () => ({ anchors: [], nextCursor: null }), + }, + }, + ); + expect(panel.queryByRole("heading", { name: "Comments" })).toBeNull(); + expect(panel.queryByRole("button", { name: "Add to chat" })).toBeNull(); + expect(panel.queryByText("1 open comment")).toBeNull(); + expect(await panel.findByRole("button", { name: /source/i })).not.toBeNull(); + }); + + it("uses panel rows only to reveal the anchored thread popover", async () => { + const app = await loadPluginApp(() => import("./app.js")); + const revealMessage = vi.fn(async () => "revealed" as const); + const panel = renderSlot< + PluginThreadPanelProps, + typeof timelineCommentsRpcContract + >( + app.threadPanelActions[0]!, + { threadId: "thr_1", params: null, revealMessage }, + { + context: { threadId: "thr_1" }, + rpc: { + listCommentThreads: () => ({ threads: [thread], nextCursor: null }), + getThreadHandoffSummary: () => ({ + threadCount: 1, + commentCount: 1, + codePointSize: 100, + }), + getCommentThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + createThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + reply: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + updateComment: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + deleteComment: () => ({ deletedThreadId: null, thread: null }), + setThreadResolved: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + listOpenAnchors: () => ({ anchors: [], nextCursor: null }), + }, + }, + ); + const row = await panel.findByRole("button", { name: /source/i }); + fireEvent.click(row); + await vi.waitFor(() => + expect(revealMessage).toHaveBeenCalledWith("msg_1"), + ); + expect(row.closest("article")?.dataset.active).toBe("true"); + expect( + panel.queryByRole("button", { name: "Comment actions" }), + ).toBeNull(); + expect(panel.inspection.navigateCalls).toEqual([]); + }); + + it("keeps the newest filter response when an older load resolves later", async () => { + const firstPage = deferred<{ + threads: typeof thread[]; + nextCursor: string | null; + }>(); + const stalePage = deferred<{ + threads: typeof thread[]; + nextCursor: string | null; + }>(); + const newestPage = deferred<{ + threads: typeof thread[]; + nextCursor: string | null; + }>(); + const listCommentThreads = vi + .fn() + .mockReturnValueOnce(firstPage.promise) + .mockReturnValueOnce(stalePage.promise) + .mockReturnValueOnce(newestPage.promise); + const app = await loadPluginApp(() => import("./app.js")); + const panel = renderSlot< + PluginThreadPanelProps, + typeof timelineCommentsRpcContract + >( + app.threadPanelActions[0]!, + { + threadId: "thr_1", + params: null, + revealMessage: vi.fn(async () => "revealed" as const), + }, + { + context: { threadId: "thr_1" }, + rpc: { + listCommentThreads, + getThreadHandoffSummary: () => ({ + threadCount: 1, + commentCount: 1, + codePointSize: 100, + }), + getCommentThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + createThread: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + reply: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + updateComment: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + deleteComment: () => ({ deletedThreadId: null, thread: null }), + setThreadResolved: () => ({ + thread, + comments: [thread.rootComment], + nextCursor: null, + }), + listOpenAnchors: () => ({ anchors: [], nextCursor: null }), + }, + }, + ); + + firstPage.resolve({ threads: [thread], nextCursor: null }); + expect(await panel.findByText("Make the API explicit.")).not.toBeNull(); + + fireEvent.click(panel.getByRole("button", { name: "Resolved" })); + await vi.waitFor(() => expect(listCommentThreads).toHaveBeenCalledTimes(2)); + expect(panel.queryByText("Make the API explicit.")).toBeNull(); + + fireEvent.click(panel.getByRole("button", { name: "All" })); + await vi.waitFor(() => expect(listCommentThreads).toHaveBeenCalledTimes(3)); + const newestThread = { + ...thread, + id: "comment_thread_newest", + rootComment: { + ...thread.rootComment, + id: "comment_newest", + threadId: "comment_thread_newest", + body: "Newest response", + }, + }; + newestPage.resolve({ threads: [newestThread], nextCursor: "newest-cursor" }); + expect(await panel.findByText("Newest response")).not.toBeNull(); + + stalePage.resolve({ + threads: [ + { + ...thread, + id: "comment_thread_stale", + rootComment: { + ...thread.rootComment, + id: "comment_stale", + threadId: "comment_thread_stale", + body: "Stale response", + }, + }, + ], + nextCursor: "stale-cursor", + }); + await vi.waitFor(() => + expect(panel.queryByText("Stale response")).toBeNull(), + ); + expect(panel.getByText("Newest response")).not.toBeNull(); + expect(panel.getByRole("button", { name: "Load more" })).not.toBeNull(); + }); +}); diff --git a/plugins/timeline-comments/app.tsx b/plugins/timeline-comments/app.tsx new file mode 100644 index 0000000..039d2a6 --- /dev/null +++ b/plugins/timeline-comments/app.tsx @@ -0,0 +1,344 @@ +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, + useSyncExternalStore, +} from "react"; +import { MessageSquareText } from "lucide-react"; +import { + definePluginApp, + useComposer, + useRealtime, + useRealtimeConnectionState, + useRpc, + type PluginThreadPanelProps, +} from "@bb/plugin-sdk/app"; +import type { + TimelineCommentThreadSummary, + timelineCommentsRpcContract, +} from "./server.js"; +import { + beginTimelineComment, + focusTimelineComment, + getTimelineCommentAnchorHealth, + subscribeTimelineCommentAnchorHealth, +} from "./bridge.js"; +import { mountTimelineCommentsController } from "./controller.js"; +import "./app.css"; + +type Filter = "open" | "resolved" | "all"; + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Something went wrong"; +} + +function excerpt(value: string, length: number): string { + return value.length > length ? `${value.slice(0, length - 1)}…` : value; +} + +function AddCommentsAction() { + const rpc = useRpc(); + const composer = useComposer(); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const requestGeneration = useRef(0); + const threadId = + composer.scope.kind === "thread" ? composer.scope.threadId : null; + const currentThreadId = useRef(threadId); + + useLayoutEffect(() => { + currentThreadId.current = threadId; + requestGeneration.current += 1; + setBusy(false); + setError(null); + return () => { + currentThreadId.current = null; + requestGeneration.current += 1; + }; + }, [threadId]); + + if (threadId === null) return null; + + const addComments = async () => { + const generation = ++requestGeneration.current; + const isCurrentRequest = () => + generation === requestGeneration.current && + currentThreadId.current === threadId; + setBusy(true); + setError(null); + try { + const summary = await rpc.call("getThreadHandoffSummary", { + bbThreadId: threadId, + }); + if (!isCurrentRequest()) return; + if (summary.threadCount === 0) return; + composer.insertMention({ + provider: "thread-comments", + id: threadId, + label: `${summary.commentCount} ${summary.commentCount === 1 ? "comment" : "comments"} from ${summary.threadCount} open ${summary.threadCount === 1 ? "thread" : "threads"}`, + }); + composer.focus(); + } catch (caught) { + if (isCurrentRequest()) setError(errorMessage(caught)); + } finally { + if (isCurrentRequest()) setBusy(false); + } + }; + + return ( + + {error !== null ? ( + + Couldn’t add comments + + ) : null} + + + ); +} + +function CommentPanel({ threadId, revealMessage }: PluginThreadPanelProps) { + const rpc = useRpc(); + const connection = useRealtimeConnectionState(); + const anchorHealth = useSyncExternalStore( + subscribeTimelineCommentAnchorHealth, + getTimelineCommentAnchorHealth, + getTimelineCommentAnchorHealth, + ); + const previousConnection = useRef(connection); + const revealRequest = useRef(0); + const loadGeneration = useRef(0); + const [filter, setFilter] = useState("open"); + const loadScope = useRef({ filter, threadId }); + const [threads, setThreads] = useState([]); + const [nextCursor, setNextCursor] = useState(null); + const [activeId, setActiveId] = useState(null); + const [unanchored, setUnanchored] = useState>(new Set()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useLayoutEffect(() => { + loadScope.current = { filter, threadId }; + loadGeneration.current += 1; + return () => { + loadGeneration.current += 1; + }; + }, [filter, threadId]); + + const loadThreads = useCallback( + async (append = false) => { + if ( + loadScope.current.threadId !== threadId || + loadScope.current.filter !== filter + ) + return; + const generation = ++loadGeneration.current; + const cursor = append ? nextCursor : null; + const isCurrentLoad = () => + generation === loadGeneration.current && + loadScope.current.threadId === threadId && + loadScope.current.filter === filter; + setLoading(true); + setError(null); + if (!append) { + setThreads([]); + setNextCursor(null); + } + try { + const page = await rpc.call("listCommentThreads", { + bbThreadId: threadId, + filter, + ...(cursor !== null ? { cursor } : {}), + }); + if (!isCurrentLoad()) return; + setThreads((current) => + append ? [...current, ...page.threads] : page.threads, + ); + setNextCursor(page.nextCursor); + } catch (caught) { + if (!isCurrentLoad()) return; + setError(errorMessage(caught)); + } finally { + if (isCurrentLoad()) setLoading(false); + } + }, + [filter, nextCursor, rpc, threadId], + ); + + const reconcile = useCallback(async () => { + await loadThreads(false); + }, [loadThreads]); + + useEffect(() => { + setActiveId(null); + void loadThreads(false); + }, [filter, threadId]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect( + () => () => { + revealRequest.current += 1; + }, + [threadId], + ); + + useRealtime("comments-changed", (payload) => { + if ( + typeof payload === "object" && + payload !== null && + (payload as { bbThreadId?: unknown }).bbThreadId === threadId + ) { + void reconcile(); + } + }); + + useEffect(() => { + const previous = previousConnection.current; + previousConnection.current = connection; + if (connection === "connected" && previous === "reconnecting") + void reconcile(); + }, [connection, reconcile]); + + const activate = async (item: TimelineCommentThreadSummary) => { + const request = ++revealRequest.current; + setActiveId(item.id); + setError(null); + try { + const revealed = await revealMessage(item.messageId); + if (request !== revealRequest.current) return; + const anchored = + revealed === "revealed" && (await focusTimelineComment(item.id)); + if (request !== revealRequest.current) return; + setUnanchored((current) => { + const next = new Set(current); + if (anchored) next.delete(item.id); + else next.add(item.id); + return next; + }); + } catch (caught) { + if (request === revealRequest.current) setError(errorMessage(caught)); + } + }; + + return ( +
+
+ {(["open", "resolved", "all"] as const).map((value) => ( + + ))} +
+ + {error !== null ? ( +
+ {error} +
+ ) : null} +
+ {!loading && threads.length === 0 ? ( +
+ No {filter === "all" ? "" : `${filter} `}comments. +
+ ) : null} + {threads.map((item) => { + return ( +
+ +
+ ); + })} + {loading ?
Loading…
: null} + {!loading && nextCursor !== null ? ( + + ) : null} +
+
+ ); +} + +export default definePluginApp((app) => { + app.experimental_contentScripts.register({ + id: "timeline-comment-anchors", + mount: mountTimelineCommentsController, + }); + app.composer.customize({ + id: "timeline-comments", + scopes: ["thread"], + actions: [{ id: "add-comments", component: AddCommentsAction }], + }); + app.slots.threadPanelAction({ + id: "comments", + title: "Comments", + icon: "MessageSquare", + component: CommentPanel, + layout: "flush", + }); + app.slots.experimental_messageAction({ + id: "comment-selection", + title: "Comment", + icon: "MessageSquare", + placements: ["selection-menu"], + run(context) { + beginTimelineComment(context); + }, + }); +}); diff --git a/plugins/timeline-comments/bb-sdk-0.0.34.d.ts b/plugins/timeline-comments/bb-sdk-0.0.34.d.ts new file mode 100644 index 0000000..02cfa27 --- /dev/null +++ b/plugins/timeline-comments/bb-sdk-0.0.34.d.ts @@ -0,0 +1,56 @@ +import type { + BbNavigate, + PluginRealtimeConnectionState, + PluginRpcClient, +} from "@bb/plugin-sdk/app"; + +interface TimelineCommentsRenderedTextSelection { + version: 1; + coordinateSpace: "rendered-text-utf16"; + start: number; + end: number; + exact: string; + prefix: string; + suffix: string; + rects: readonly { + x: number; + y: number; + width: number; + height: number; + }[]; +} + +declare module "@bb/plugin-sdk/app" { + interface PluginThreadPanelProps { + revealMessage(messageId: string): Promise<"revealed" | "missing">; + } + + interface PluginMessageActionContext { + selection?: TimelineCommentsRenderedTextSelection; + } + + interface PluginMessageActionRegistration { + placements?: readonly ("action-bar" | "selection-menu")[]; + } + + interface PluginContentScriptContext { + readonly rpc: PluginRpcClient; + readonly realtime: { + subscribe( + channel: string, + handler: (payload: unknown) => void, + ): () => void | Promise; + getConnectionState(): PluginRealtimeConnectionState; + subscribeConnectionState( + handler: (state: PluginRealtimeConnectionState) => void, + ): () => void | Promise; + }; + readonly navigate: Pick; + } +} + +declare module "@bb/plugin-sdk" { + interface PluginThreadPanelProps { + revealMessage(messageId: string): Promise<"revealed" | "missing">; + } +} diff --git a/plugins/timeline-comments/bridge.ts b/plugins/timeline-comments/bridge.ts new file mode 100644 index 0000000..4f963ff --- /dev/null +++ b/plugins/timeline-comments/bridge.ts @@ -0,0 +1,57 @@ +import type { PluginMessageActionContext } from "@bb/plugin-sdk/app"; + +export interface TimelineCommentsControllerBridge { + beginComment(context: PluginMessageActionContext): void; + focusThread(commentThreadId: string): Promise; +} + +let activeController: TimelineCommentsControllerBridge | null = null; +export type TimelineCommentAnchorHealth = + | "anchored" + | "unanchored" + | "not-mounted"; +let anchorHealthSnapshot: ReadonlyMap = + new Map(); +const anchorHealthListeners = new Set<() => void>(); + +export function publishTimelineCommentAnchorHealth( + health: ReadonlyMap, +): void { + anchorHealthSnapshot = new Map(health); + for (const listener of anchorHealthListeners) listener(); +} + +export function getTimelineCommentAnchorHealth(): ReadonlyMap< + string, + TimelineCommentAnchorHealth +> { + return anchorHealthSnapshot; +} + +export function subscribeTimelineCommentAnchorHealth( + listener: () => void, +): () => void { + anchorHealthListeners.add(listener); + return () => anchorHealthListeners.delete(listener); +} + +export function installTimelineCommentsController( + controller: TimelineCommentsControllerBridge, +): () => void { + activeController = controller; + return () => { + if (activeController === controller) activeController = null; + }; +} + +export function beginTimelineComment( + context: PluginMessageActionContext, +): void { + activeController?.beginComment(context); +} + +export async function focusTimelineComment( + commentThreadId: string, +): Promise { + return (await activeController?.focusThread(commentThreadId)) ?? false; +} diff --git a/plugins/timeline-comments/browser-check/harness.html b/plugins/timeline-comments/browser-check/harness.html new file mode 100644 index 0000000..ac499df --- /dev/null +++ b/plugins/timeline-comments/browser-check/harness.html @@ -0,0 +1,91 @@ + + + + + + Timeline comments browser check + + + + +
+
+
+
+ First exact browser anchor + Second exact browser anchor + Third exact browser anchor + Fourth exact browser anchor + Fifth exact browser anchor + Sixth exact browser anchor + Seventh exact browser anchor + Eighth exact browser anchor +
+
+
+
+ Running browser geometry check… + + + diff --git a/plugins/timeline-comments/browser-check/harness.ts b/plugins/timeline-comments/browser-check/harness.ts new file mode 100644 index 0000000..22fa4d7 --- /dev/null +++ b/plugins/timeline-comments/browser-check/harness.ts @@ -0,0 +1,510 @@ +import "../app.css"; +import { mountTimelineCommentsController } from "../controller.js"; +import type { PluginContentScriptContext } from "@bb/plugin-sdk/app"; + +const threadId = "thr_browser"; +const messageId = "msg_browser"; +const prose = document.querySelector( + "[data-bb-message-prose-root]", +)!; +const text = prose.textContent ?? ""; +const phrases = [ + "First exact browser anchor", + "Second exact browser anchor", + "Third exact browser anchor", + "Fourth exact browser anchor", + "Fifth exact browser anchor", + "Sixth exact browser anchor", + "Seventh exact browser anchor", + "Eighth exact browser anchor", +]; +const now = Date.now(); +const summaries = phrases.map((exact, index) => { + const start = text.indexOf(exact); + return { + id: `comment_thread_${index}`, + bbThreadId: threadId, + messageId, + messageRole: "assistant" as const, + selector: { + version: 1 as const, + coordinateSpace: "rendered-text-utf16" as const, + start, + end: start + exact.length, + exact, + prefix: text.slice(Math.max(0, start - 32), start), + suffix: text.slice(start + exact.length, start + exact.length + 32), + }, + version: 1, + createdAt: now + index, + updatedAt: now + index, + resolvedAt: null, + replyCount: 0, + }; +}); + +const context = { + pluginId: "timeline-comments", + generation: 1, + signal: new AbortController().signal, + rpc: { + async call(method: string) { + if (method === "listOpenAnchors") + return { anchors: summaries, nextCursor: null }; + if (method === "getCommentThread") { + const summary = summaries[0]!; + const rootComment = { + id: "comment_root", + threadId: summary.id, + parentId: null, + body: "Verify the real browser geometry before shipping.", + version: 1, + createdAt: now, + updatedAt: now, + }; + const replies = Array.from({ length: 12 }, (_, index) => ({ + id: `comment_reply_${index}`, + threadId: summary.id, + parentId: rootComment.id, + body: `Reply ${index + 1} keeps this thread long enough to scroll.`, + version: 1, + createdAt: now + index + 1, + updatedAt: now + index + 1, + })); + return { + thread: { ...summary, rootComment, replyCount: replies.length }, + comments: [rootComment, ...replies], + nextCursor: null, + }; + } + throw new Error(`Unexpected RPC ${method}`); + }, + }, + realtime: { + subscribe: () => () => {}, + getConnectionState: () => "connected" as const, + subscribeConnectionState: () => () => {}, + }, + navigate: {}, +} as unknown as PluginContentScriptContext; + +mountTimelineCommentsController(context); + +function wait(ms: number): Promise { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +function withinViewport(rect: DOMRect): boolean { + return ( + rect.left >= 0 && + rect.top >= 0 && + rect.right <= window.innerWidth && + rect.bottom <= window.innerHeight + ); +} + +void (async () => { + const result = document.querySelector("#result")!; + try { + await wait(300); + const overlay = document.querySelector( + ".bb-comments-overlay", + ); + if ( + overlay?.parentElement?.dataset.bbPluginDecoration !== "timeline-comments" + ) { + throw new Error("Overlay is outside the plugin CSS ownership boundary"); + } + const markers = [ + ...document.querySelectorAll(".bb-comments-marker"), + ]; + if (markers.length !== 1) + throw new Error(`Expected 1 local cluster, got ${markers.length}`); + const overflow = markers.find((marker) => marker.textContent === "8"); + if (overflow === undefined) + throw new Error("Expected one 8-thread collision marker"); + if (overflow.querySelector("svg") === null) + throw new Error("Collision marker omitted its comment icon"); + const clusterIconRect = overflow + .querySelector("svg")! + .getBoundingClientRect(); + const clusterCountRect = overflow + .querySelector(".bb-comments-marker-count")! + .getBoundingClientRect(); + if (clusterIconRect.width !== 20 || clusterIconRect.height !== 20) + throw new Error("Cluster icon does not match the single-marker icon size"); + if ( + overflow.dataset.bbCommentGutter === "left" && + clusterCountRect.right > clusterIconRect.left + ) + throw new Error("Left-gutter cluster count is not gutter-side"); + const markerRect = overflow.getBoundingClientRect(); + const proseRect = prose.getBoundingClientRect(); + if (Math.abs(proseRect.left - markerRect.right - 8) > 1) + throw new Error("Gutter marker is not 8px from the message text"); + const tops = markers.map((marker) => marker.getBoundingClientRect().top); + if (new Set(tops).size !== markers.length) + throw new Error("Markers overlap vertically"); + + overflow.click(); + await wait(30); + const cluster = document.querySelector(".bb-comments-cluster"); + if (cluster === null || cluster.querySelectorAll("button").length !== 8) { + throw new Error("Overflow marker did not expose all grouped threads"); + } + if (!withinViewport(cluster.getBoundingClientRect())) + throw new Error("Cluster escaped the viewport"); + if (document.activeElement !== cluster.querySelector("button")) + throw new Error("Cluster did not focus its first thread"); + + cluster.querySelector("button")!.click(); + await wait(80); + const popover = document.querySelector(".bb-comments-thread"); + if (popover === null) + throw new Error("Thread marker did not open its popover"); + if ( + popover.parentElement?.dataset.bbPluginDecoration !== "timeline-comments" + ) + throw new Error( + "Thread popover is outside the plugin CSS ownership boundary", + ); + if (!withinViewport(popover.getBoundingClientRect())) + throw new Error("Thread popover escaped the viewport"); + if (document.activeElement !== popover) + throw new Error("Thread popover did not receive focus"); + const reply = popover.querySelector( + ".bb-comments-reply-input", + ); + const replyButton = popover.querySelector( + 'button[aria-label="Reply"]', + ); + if (reply === null || replyButton?.disabled !== true) + throw new Error("Blank reply was not disabled"); + const emptyReplyHeight = reply.getBoundingClientRect().height; + const replyComposer = reply.closest( + ".bb-comments-inline-composer", + )!; + reply.value = "First line\nSecond line\nThird line"; + reply.dispatchEvent(new InputEvent("input", { bubbles: true })); + if (replyComposer.getAnimations().length === 0) + throw new Error("Multiline reply expansion did not animate"); + await wait(130); + const replyNearTerminalHeight = replyComposer.getBoundingClientRect().height; + await wait(70); + const replyTerminalHeight = replyComposer.getBoundingClientRect().height; + if ( + Math.abs(replyTerminalHeight - replyNearTerminalHeight) > 2 || + replyComposer.getAnimations().length !== 0 + ) { + throw new Error("Multiline reply snapped after its height animation"); + } + if (reply.getBoundingClientRect().height <= emptyReplyHeight) + throw new Error("Multiline reply input did not grow with its content"); + if (replyComposer.dataset.multiline !== "true") + throw new Error("Multiline reply did not switch composer layout"); + reply.value = "Ready"; + reply.dispatchEvent(new InputEvent("input", { bubbles: true })); + await wait(180); + if (replyButton.disabled) + throw new Error("Valid reply did not enable submission"); + if (replyComposer.dataset.multiline !== "true") + throw new Error("Expanded reply layout did not stay latched like Moss"); + reply.value = ""; + reply.dispatchEvent(new InputEvent("input", { bubbles: true })); + if (replyComposer.getAnimations().length === 0) + throw new Error("Reply collapse did not animate"); + await wait(180); + if (replyComposer.dataset.multiline !== "false") + throw new Error("Cleared reply did not restore inline layout"); + reply.value = "Ready"; + reply.dispatchEvent(new InputEvent("input", { bubbles: true })); + if (CSS.highlights.get("bb-timeline-comments")?.size !== 8) { + throw new Error("Custom Highlight registry did not retain every anchor"); + } + if (CSS.highlights.get("bb-timeline-comments-active")?.size !== 1) { + throw new Error("Open thread did not strengthen exactly one highlight"); + } + + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + await wait(0); + const actionsMenu = document.querySelector( + ".bb-comments-actions-popover", + ); + if (actionsMenu === null) + throw new Error("Comment actions menu did not open"); + if ( + popover.contains(actionsMenu) || + actionsMenu.parentElement?.dataset.bbPluginDecoration !== + "timeline-comments" + ) { + throw new Error("Comment actions menu was not rendered in its portal"); + } + if ( + getComputedStyle( + popover.querySelector(".bb-comments-thread-comments")!, + ).overflowY !== "auto" + ) { + throw new Error("Opening comment actions disabled comment scrolling"); + } + const actionsRect = actionsMenu.getBoundingClientRect(); + const deleteButton = [...actionsMenu.querySelectorAll("button")].find( + (button) => button.textContent === "Delete", + ); + const deleteRect = deleteButton?.getBoundingClientRect(); + const visibleAtDelete = + deleteRect === undefined + ? null + : document.elementFromPoint( + deleteRect.left + deleteRect.width / 2, + deleteRect.top + deleteRect.height / 2, + ); + if ( + deleteButton === undefined || + deleteRect === undefined || + !withinViewport(actionsRect) || + !deleteButton.contains(visibleAtDelete) + ) + throw new Error("Comment actions menu is clipped by the thread chrome"); + + const menuItems = [ + ...actionsMenu.querySelectorAll('[role="menuitem"]'), + ]; + if (document.activeElement !== menuItems[0]) + throw new Error("Comment actions menu did not focus its first item"); + menuItems[0]!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }), + ); + if (document.activeElement !== menuItems[1]) + throw new Error("ArrowDown did not move to the next comment action"); + menuItems[1]!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }), + ); + if (document.activeElement !== menuItems[0]) + throw new Error("ArrowDown did not wrap comment action focus"); + menuItems[0]!.dispatchEvent( + new KeyboardEvent("keydown", { key: "End", bubbles: true }), + ); + if (document.activeElement !== menuItems.at(-1)) + throw new Error("End did not focus the final comment action"); + menuItems.at(-1)!.dispatchEvent( + new KeyboardEvent("keydown", { key: "Home", bubbles: true }), + ); + if (document.activeElement !== menuItems[0]) + throw new Error("Home did not focus the first comment action"); + menuItems[0]!.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }), + ); + if (document.activeElement !== menuItems.at(-1)) + throw new Error("ArrowUp did not wrap comment action focus"); + menuItems.at(-1)!.dispatchEvent( + new KeyboardEvent("keydown", { key: "Tab", bubbles: true }), + ); + await wait(0); + if ( + document.querySelector(".bb-comments-actions-popover") !== null || + document.activeElement === document.body + ) { + throw new Error("Tab did not dismiss the menu and preserve focus"); + } + + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + document.activeElement?.dispatchEvent( + new KeyboardEvent("keydown", { + key: "Tab", + shiftKey: true, + bubbles: true, + }), + ); + await wait(0); + if ( + document.querySelector(".bb-comments-actions-popover") !== null || + document.activeElement === document.body + ) { + throw new Error("Shift+Tab did not dismiss the menu and preserve focus"); + } + + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + popover.focus({ preventScroll: true }); + await wait(0); + if (document.querySelector(".bb-comments-actions-popover") !== null) + throw new Error("Moving focus outside did not dismiss the actions menu"); + + const commentsScroller = popover.querySelector( + ".bb-comments-thread-comments", + )!; + commentsScroller.scrollTop = 0; + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + commentsScroller.scrollTop = commentsScroller.scrollHeight; + commentsScroller.dispatchEvent(new Event("scroll")); + await wait(30); + if (document.querySelector(".bb-comments-actions-popover") !== null) + throw new Error("Scrolling its trigger out of view did not dismiss menu"); + commentsScroller.scrollTop = 0; + commentsScroller.dispatchEvent(new Event("scroll")); + await wait(30); + + const clippedTrigger = popover.querySelector( + `[data-bb-comment-id="comment_root"] ` + + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + )!; + const scrollerRect = commentsScroller.getBoundingClientRect(); + const triggerRect = clippedTrigger.getBoundingClientRect(); + commentsScroller.scrollTop += + triggerRect.top - + scrollerRect.top + + triggerRect.height / 2; + commentsScroller.dispatchEvent(new Event("scroll")); + await wait(30); + const clippedRect = clippedTrigger.getBoundingClientRect(); + if ( + clippedRect.top >= scrollerRect.top || + clippedRect.bottom <= scrollerRect.top + ) { + throw new Error("Browser fixture did not partially clip action trigger"); + } + const originalAddEventListener = document.addEventListener; + let pointerdownListenerAdds = 0; + document.addEventListener = (( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ) => { + if (type === "pointerdown") pointerdownListenerAdds += 1; + originalAddEventListener.call(document, type, listener, options); + }) as typeof document.addEventListener; + clippedTrigger.focus({ preventScroll: true }); + try { + clippedTrigger.click(); + } finally { + document.addEventListener = originalAddEventListener; + } + if ( + document.querySelector(".bb-comments-actions-popover") !== null || + clippedTrigger.getAttribute("aria-expanded") !== "false" || + pointerdownListenerAdds !== 0 + ) { + throw new Error( + "Partially clipped trigger left a menu or dismissal listener open", + ); + } + commentsScroller.scrollTop = 0; + commentsScroller.dispatchEvent(new Event("scroll")); + await wait(30); + + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + document.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), + ); + if ( + document.querySelector(".bb-comments-actions-popover") !== null || + document.querySelector(".bb-comments-thread") === null + ) { + throw new Error("Escape did not dismiss only the comment actions menu"); + } + popover + .querySelector( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.click(); + const editButton = [ + ...document.querySelectorAll( + ".bb-comments-actions-popover button", + ), + ].find((button) => button.textContent === "Edit"); + editButton?.click(); + if (document.querySelector(".bb-comments-actions-popover") !== null) + throw new Error("Comment action did not dismiss its portal menu"); + const editInput = popover.querySelector( + ".bb-comments-edit-input", + ); + if ( + editInput === null || + editInput.value !== "Verify the real browser geometry before shipping." + ) { + throw new Error("Comment edit did not replace the message body"); + } + const saveEdit = popover.querySelector( + 'button[aria-label="Save comment"]', + ); + if (saveEdit?.disabled !== false) + throw new Error("Unchanged comment cannot exit editing like Moss"); + const editComposer = editInput.closest( + ".bb-comments-inline-composer", + ); + const editRect = editInput.getBoundingClientRect(); + const saveRect = saveEdit.getBoundingClientRect(); + if ( + editComposer?.dataset.multiline !== "true" || + saveRect.top < editRect.bottom + ) { + throw new Error("Multiline edit did not move actions below the text"); + } + editInput.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), + ); + if ( + document.querySelector(".bb-comments-thread") === null || + document.querySelector(".bb-comments-edit-input") !== null + ) { + throw new Error("Escape did not cancel editing in place"); + } + const restoredCommentAction = popover.querySelector( + `[data-bb-comment-id="comment_root"] ` + + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ); + if (document.activeElement !== restoredCommentAction) + throw new Error("Cancelling edit did not restore comment action focus"); + + document.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), + ); + if (document.querySelector(".bb-comments-thread") !== null) { + throw new Error("Escape did not dismiss the thread popover"); + } + if (!document.activeElement?.classList.contains("bb-comments-marker")) { + throw new Error("Popover dismissal did not restore marker focus"); + } + document + .querySelector(".bb-comments-marker") + ?.click(); + document + .querySelector(".bb-comments-cluster button") + ?.click(); + await wait(80); + if (document.querySelector(".bb-comments-thread") === null) + throw new Error("Thread popover did not reopen"); + if ( + [...document.querySelectorAll("button")].some((button) => + button.textContent?.includes("Send to agent"), + ) + ) + throw new Error("Removed agent handoff action is still visible"); + + document.body.dataset.testStatus = "passed"; + result.value = + "Passed: real Chrome laid out 8 highlights, one local collision cluster, an 8-thread chooser, and a bounded thread popover."; + } catch (error) { + document.body.dataset.testStatus = "failed"; + result.value = error instanceof Error ? error.message : String(error); + } +})(); diff --git a/plugins/timeline-comments/browser-check/run.mjs b/plugins/timeline-comments/browser-check/run.mjs new file mode 100644 index 0000000..7d736d1 --- /dev/null +++ b/plugins/timeline-comments/browser-check/run.mjs @@ -0,0 +1,113 @@ +import { copyFileSync, mkdtempSync, readFileSync } from "node:fs"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { build } from "esbuild"; +import { chromium } from "playwright"; + +const output = mkdtempSync(join(tmpdir(), "bb-timeline-comments-browser-")); +await build({ + entryPoints: [new URL("./harness.ts", import.meta.url).pathname], + bundle: true, + format: "esm", + outfile: join(output, "harness.js"), + external: ["@bb/plugin-sdk/app"], + loader: { ".css": "css" }, +}); +copyFileSync( + new URL("./harness.html", import.meta.url), + join(output, "harness.html"), +); + +const screenshot = + process.env.BB_TIMELINE_COMMENTS_SCREENSHOT ?? join(output, "screenshot.png"); +const server = createServer((request, response) => { + const file = (request.url ?? "/").replace(/^\//u, "") || "harness.html"; + if (!["harness.html", "harness.js", "harness.css"].includes(file)) { + response.writeHead(404).end(); + return; + } + response.setHeader( + "content-type", + file.endsWith(".html") + ? "text/html" + : file.endsWith(".js") + ? "text/javascript" + : "text/css", + ); + response.end(readFileSync(join(output, file))); +}); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +let browser; +try { + const address = server.address(); + if (address === null || typeof address === "string") { + throw new Error("Browser check server did not bind a TCP port"); + } + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 900, height: 600 } }); + await page.goto(`http://127.0.0.1:${address.port}/harness.html`); + await page.waitForFunction( + () => document.body.dataset.testStatus !== "running", + undefined, + { timeout: 5_000 }, + ); + const status = await page.locator("body").getAttribute("data-test-status"); + const result = await page.locator("#result").textContent(); + if (status !== "passed") throw new Error(result ?? "Unknown browser failure"); + await page.setViewportSize({ width: 480, height: 600 }); + await page.waitForTimeout(100); + const narrow = await page.evaluate(() => { + const markers = [...document.querySelectorAll(".bb-comments-marker")]; + const popover = document.querySelector(".bb-comments-thread"); + const popoverRect = popover?.getBoundingClientRect(); + const cluster = markers.find( + (marker) => marker.querySelector(".bb-comments-marker-count") !== null, + ); + const clusterIconRect = cluster + ?.querySelector("svg") + ?.getBoundingClientRect(); + const clusterCountRect = cluster + ?.querySelector(".bb-comments-marker-count") + ?.getBoundingClientRect(); + return { + usesRightRail: markers.every( + (marker) => marker.dataset.bbCommentGutter === "right", + ), + countIsGutterSide: + clusterIconRect !== undefined && + clusterCountRect !== undefined && + clusterCountRect.left >= clusterIconRect.right, + popoverBounded: + popoverRect !== undefined && + popoverRect.left >= 0 && + popoverRect.right <= window.innerWidth, + }; + }); + if ( + !narrow.usesRightRail || + !narrow.countIsGutterSide || + !narrow.popoverBounded + ) { + throw new Error("Narrow viewport did not retain a bounded right gutter"); + } + await page.setViewportSize({ width: 900, height: 600 }); + await page.waitForTimeout(100); + await page + .locator( + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + .first() + .click(); + await page + .locator(".bb-comments-actions-popover button", { hasText: "Edit" }) + .click(); + await page.waitForTimeout(50); + await page.screenshot({ path: screenshot }); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} +console.log( + `Timeline comments browser check passed. Screenshot: ${screenshot}`, +); diff --git a/plugins/timeline-comments/comment-body.ts b/plugins/timeline-comments/comment-body.ts new file mode 100644 index 0000000..8ecd4ba --- /dev/null +++ b/plugins/timeline-comments/comment-body.ts @@ -0,0 +1,8 @@ +export const COMMENT_BODY_CODE_POINT_LIMIT = 10_000; + +export function commentBodyError(value: string): string | null { + if (value.trim() === "") return "Comment body is required"; + if (Array.from(value).length > COMMENT_BODY_CODE_POINT_LIMIT) + return "Comment body must be at most 10,000 Unicode code points"; + return null; +} diff --git a/plugins/timeline-comments/controller.test.ts b/plugins/timeline-comments/controller.test.ts new file mode 100644 index 0000000..07c92b8 --- /dev/null +++ b/plugins/timeline-comments/controller.test.ts @@ -0,0 +1,107 @@ +// @vitest-environment jsdom +import { describe, expect, it, vi } from "vitest"; +import type { PluginContentScriptContext } from "@bb/plugin-sdk/app"; +import { + subscribeTimelineCommentAnchorHealth, +} from "./bridge.js"; +import { mountTimelineCommentsController } from "./controller.js"; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((resolvePromise) => { + resolve = resolvePromise; + }); + return { promise, resolve }; +} + +describe("timeline comments controller teardown", () => { + it("does not restore or republish anchors after a deferred load resolves", async () => { + document.body.innerHTML = ` +
+
+
+
source
+
+
+
+ `; + const page = deferred<{ + anchors: Array<{ + id: string; + bbThreadId: string; + messageId: string; + messageRole: "assistant"; + selector: { + version: 1; + coordinateSpace: "rendered-text-utf16"; + start: number; + end: number; + exact: string; + prefix: string; + suffix: string; + }; + version: number; + createdAt: number; + updatedAt: number; + resolvedAt: null; + replyCount: number; + }>; + nextCursor: null; + }>(); + const call = vi.fn(() => page.promise); + const dispose = mountTimelineCommentsController({ + pluginId: "timeline-comments", + generation: 1, + signal: new AbortController().signal, + rpc: { call }, + realtime: { + subscribe: () => () => {}, + getConnectionState: () => "connected", + subscribeConnectionState: () => () => {}, + }, + navigate: {}, + } as unknown as PluginContentScriptContext); + await vi.waitFor(() => expect(call).toHaveBeenCalledWith( + "listOpenAnchors", + { threadIds: ["thr_1"] }, + )); + + const healthChanged = vi.fn(); + const unsubscribe = subscribeTimelineCommentAnchorHealth(healthChanged); + dispose(); + healthChanged.mockClear(); + page.resolve({ + anchors: [ + { + id: "comment_thread_1", + bbThreadId: "thr_1", + messageId: "msg_1", + messageRole: "assistant", + selector: { + version: 1, + coordinateSpace: "rendered-text-utf16", + start: 0, + end: 6, + exact: "source", + prefix: "", + suffix: "", + }, + version: 1, + createdAt: 1, + updatedAt: 1, + resolvedAt: null, + replyCount: 0, + }, + ], + nextCursor: null, + }); + await page.promise; + await Promise.resolve(); + + expect(healthChanged).not.toHaveBeenCalled(); + expect( + document.querySelector("[data-bb-plugin-decoration='timeline-comments']"), + ).toBeNull(); + unsubscribe(); + }); +}); diff --git a/plugins/timeline-comments/controller.ts b/plugins/timeline-comments/controller.ts new file mode 100644 index 0000000..a274d91 --- /dev/null +++ b/plugins/timeline-comments/controller.ts @@ -0,0 +1,1418 @@ +import type { + PluginContentScriptContext, + PluginMessageActionContext, + PluginRpcClient, +} from "@bb/plugin-sdk/app"; +import { + CheckCheck, + Command, + CornerDownLeft, + EllipsisVertical, + Pencil, + StickyNote, + Trash2, + createElement as createLucideElement, + type IconNode, +} from "lucide"; +import type { + TimelineComment, + TimelineCommentThreadDetail, + TimelineCommentThreadSummary, + timelineCommentsRpcContract, +} from "./server.js"; +import { + chooseNearestGutter, + layoutGutterMarkers, + restoreSelector, +} from "./anchors.js"; +import { commentBodyError } from "./comment-body.js"; +import { + installTimelineCommentsController, + publishTimelineCommentAnchorHealth, + type TimelineCommentAnchorHealth, +} from "./bridge.js"; + +type Rpc = PluginRpcClient; + +interface RestoredThread { + anchor: TimelineCommentThreadSummary; + range: Range; + marker: HTMLButtonElement | null; + side: "left" | "right"; + desiredY: number; + window: HTMLElement; + prose: HTMLElement; +} + +const OWNED = "data-bb-timeline-comments-owned"; +const NORMAL_HIGHLIGHT = "bb-timeline-comments"; +const ACTIVE_HIGHLIGHT = "bb-timeline-comments-active"; +const DRAFT_TTL = 24 * 60 * 60 * 1_000; +const PLUGIN_DECORATION = "data-bb-plugin-decoration"; +const MARKER_SIZE = 32; +const MARKER_TEXT_GAP = 8; + +function readDraft(key: string): string | null { + const saved = sessionStorage.getItem(key); + if (saved === null) return null; + try { + const parsed = JSON.parse(saved) as { + body?: unknown; + expiresAt?: unknown; + }; + if ( + typeof parsed.body === "string" && + typeof parsed.expiresAt === "number" && + parsed.expiresAt > Date.now() + ) { + return parsed.body; + } + } catch { + // Invalid or expired drafts are discarded below. + } + sessionStorage.removeItem(key); + return null; +} + +function writeDraft(key: string, body: string): void { + if (body.trim() === "") { + sessionStorage.removeItem(key); + return; + } + sessionStorage.setItem( + key, + JSON.stringify({ body, expiresAt: Date.now() + DRAFT_TTL }), + ); +} + +function element( + tag: K, + className?: string, + text?: string, +): HTMLElementTagNameMap[K] { + const node = document.createElement(tag); + node.setAttribute(OWNED, ""); + if (className !== undefined) node.className = className; + if (text !== undefined) node.textContent = text; + return node; +} + +/** BB scopes plugin CSS to descendants of this ownership boundary. */ +function decorateRoot(node: T): T { + node.setAttribute(PLUGIN_DECORATION, "timeline-comments"); + return node; +} + +function escapeSelector(value: string): string { + return ( + globalThis.CSS?.escape?.(value) ?? value.replace(/[^a-zA-Z0-9_-]/gu, "\\$&") + ); +} + +function sourceExcerpt(text: string): string { + return text.length > 120 ? `${text.slice(0, 117)}…` : text; +} + +function formatTime(value: number): string { + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(value); +} + +function relativeTime(value: number): string { + const elapsed = Math.max(0, Date.now() - value); + if (elapsed < 60_000) return "just now"; + if (elapsed < 3_600_000) { + const minutes = Math.floor(elapsed / 60_000); + return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; + } + if (elapsed < 86_400_000) { + const hours = Math.floor(elapsed / 3_600_000); + return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; + } + if (elapsed < 604_800_000) { + const days = Math.floor(elapsed / 86_400_000); + return `${days} ${days === 1 ? "day" : "days"} ago`; + } + return new Intl.DateTimeFormat(undefined, { + month: "short", + day: "numeric", + }).format(value); +} + +function icon(node: IconNode): SVGElement { + const svg = createLucideElement(node); + svg.setAttribute(OWNED, ""); + svg.setAttribute("width", "16"); + svg.setAttribute("height", "16"); + svg.setAttribute("stroke-width", "1.5"); + svg.setAttribute("aria-hidden", "true"); + return svg; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Something went wrong"; +} + +const inlineComposerAnimations = new WeakMap(); +const inlineComposerNaturalHeights = new WeakMap(); + +function syncInlineComposerLayout( + textarea: HTMLTextAreaElement, + composer: HTMLElement, + animate = true, +): void { + const styles = getComputedStyle(textarea); + const lineHeight = Number.parseFloat(styles.lineHeight) || 20; + const verticalPadding = + (Number.parseFloat(styles.paddingTop) || 0) + + (Number.parseFloat(styles.paddingBottom) || 0); + const currentMultiline = composer.dataset.multiline === "true"; + const hasText = textarea.value.trim() !== ""; + const requestsMultiline = + textarea.value.includes("\n") || + textarea.scrollHeight - verticalPadding > lineHeight + 1; + const nextMultiline = + hasText && (currentMultiline || requestsMultiline); + const running = inlineComposerAnimations.get(composer); + const startHeight = + running === undefined + ? (inlineComposerNaturalHeights.get(composer) ?? + composer.getBoundingClientRect().height) + : composer.getBoundingClientRect().height; + running?.cancel(); + inlineComposerAnimations.delete(composer); + composer.style.removeProperty("overflow"); + composer.dataset.multiline = nextMultiline ? "true" : "false"; + const endHeight = composer.getBoundingClientRect().height; + inlineComposerNaturalHeights.set(composer, endHeight); + if ( + !animate || + Math.abs(endHeight - startHeight) < 0.5 || + typeof composer.animate !== "function" || + matchMedia("(prefers-reduced-motion: reduce)").matches + ) { + return; + } + composer.style.overflow = "hidden"; + const animation = composer.animate( + [{ height: `${startHeight}px` }, { height: `${endHeight}px` }], + { + duration: 150, + easing: "cubic-bezier(0.22, 1, 0.36, 1)", + }, + ); + inlineComposerAnimations.set(composer, animation); + const finish = () => { + if (inlineComposerAnimations.get(composer) !== animation) return; + inlineComposerAnimations.delete(composer); + composer.style.removeProperty("overflow"); + }; + animation.addEventListener("finish", finish, { once: true }); + animation.addEventListener("cancel", finish, { once: true }); +} + +function isRelevantMutation(record: MutationRecord): boolean { + const selector = + "[data-bb-thread-window], [data-bb-conversation-message-id], [data-bb-message-prose-root]"; + return [...record.addedNodes, ...record.removedNodes].some( + (node) => + node instanceof Element && + (node.matches(selector) || node.querySelector(selector) !== null), + ); +} + +class TimelineCommentsController { + readonly #rpc: Rpc; + readonly #navigate: PluginContentScriptContext["navigate"]; + readonly #portal = decorateRoot(element("div", "bb-comments-portal")); + readonly #overlay = element("div", "bb-comments-overlay"); + readonly #highlightStyle = element("style"); + readonly #anchors = new Map(); + readonly #restored = new Map(); + readonly #disposers: Array<() => void> = []; + readonly #observer: MutationObserver; + readonly #resizeObserver: ResizeObserver | null; + #refreshNonce = 0; + #refreshing: Promise | null = null; + #frame: number | null = null; + #popover: HTMLElement | null = null; + #composer: HTMLElement | null = null; + #activeIds = new Set(); + #provisionalRange: Range | null = null; + #openThreadId: string | null = null; + #destroyed = false; + #sawConnected = false; + #focusNonce = 0; + #outsideComposer: ((event: PointerEvent) => void) | null = null; + #outsidePopover: ((event: PointerEvent) => void) | null = null; + #popoverKeydown: ((event: KeyboardEvent) => void) | null = null; + #popoverInvoker: HTMLElement | null = null; + #actionsMenu: HTMLElement | null = null; + #actionsTrigger: HTMLButtonElement | null = null; + #outsideActionsMenu: ((event: PointerEvent) => void) | null = null; + + constructor(context: PluginContentScriptContext) { + this.#rpc = context.rpc as Rpc; + this.#navigate = context.navigate; + this.#overlay.setAttribute("aria-live", "polite"); + this.#highlightStyle.textContent = ` + ::highlight(${NORMAL_HIGHLIGHT}) { + text-decoration: underline; + text-decoration-color: color-mix(in oklab, var(--foreground) 62%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 3px; + } + ::highlight(${ACTIVE_HIGHLIGHT}) { + background: color-mix(in oklab, var(--primary) 12%, transparent); + text-decoration: underline; + text-decoration-color: var(--foreground); + text-decoration-thickness: 2px; + text-underline-offset: 3px; + } + `; + this.#portal.append(this.#overlay); + document.body.append(this.#highlightStyle, this.#portal); + + this.#observer = new MutationObserver((records) => { + if (records.some(isRelevantMutation)) this.scheduleRefresh(); + }); + this.#observer.observe(document.body, { childList: true, subtree: true }); + this.#resizeObserver = + typeof ResizeObserver === "undefined" + ? null + : new ResizeObserver(() => this.scheduleLayout()); + + const onViewportChange = () => this.scheduleLayout(); + document.addEventListener("scroll", onViewportChange, true); + window.addEventListener("resize", onViewportChange); + this.#disposers.push(() => + document.removeEventListener("scroll", onViewportChange, true), + ); + this.#disposers.push(() => + window.removeEventListener("resize", onViewportChange), + ); + this.#disposers.push( + context.realtime.subscribe("comments-changed", () => + this.scheduleRefresh(), + ), + ); + this.#disposers.push( + context.realtime.subscribeConnectionState((state) => { + if (state === "connected") { + if (this.#sawConnected) this.scheduleRefresh(); + this.#sawConnected = true; + } + }), + ); + this.#sawConnected = context.realtime.getConnectionState() === "connected"; + this.#disposers.push(installTimelineCommentsController(this)); + this.scheduleRefresh(); + } + + beginComment(context: PluginMessageActionContext): void { + if (context.selection === undefined || context.selectedText === undefined) + return; + this.closeComposer(); + const root = this.findProse(context.threadId, context.message.id); + const restored = + root === null ? null : restoreSelector(root, context.selection); + this.#provisionalRange = restored?.range ?? null; + this.rebuildHighlights(); + + const key = `bb.timeline-comments.draft:${context.threadId}:${context.message.id}:${context.selection.start}:${context.selection.end}`; + const shell = element("form", "bb-comments-composer"); + shell.setAttribute("role", "dialog"); + shell.setAttribute("aria-label", "Add comment"); + const textarea = element( + "textarea", + "bb-comments-textarea", + ) as HTMLTextAreaElement; + textarea.placeholder = "Add a comment…"; + textarea.maxLength = 20_000; + textarea.value = readDraft(key) ?? ""; + const footer = element("div", "bb-comments-composer-footer"); + footer.append(element("span", "bb-comments-hint", "⌘/Ctrl Enter")); + const submit = element( + "button", + "bb-comments-primary", + "Comment", + ) as HTMLButtonElement; + submit.type = "submit"; + footer.append(submit); + const error = element("div", "bb-comments-error"); + error.setAttribute("role", "status"); + shell.append(textarea, error, footer); + + const firstRect = context.selection.rects[0]; + const x = firstRect?.x ?? window.innerWidth / 2; + const y = context.selection.rects.at(-1)?.y ?? window.innerHeight / 2; + shell.style.left = `${Math.max(8, Math.min(window.innerWidth - 328, x))}px`; + shell.style.top = `${Math.max(8, Math.min(window.innerHeight - 180, y + 22))}px`; + const validate = () => { + const message = commentBodyError(textarea.value); + submit.disabled = message !== null; + error.textContent = + message !== null && textarea.value.trim() !== "" ? message : ""; + return message; + }; + const persist = () => writeDraft(key, textarea.value); + textarea.addEventListener("input", () => { + persist(); + validate(); + }); + textarea.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + persist(); + this.closeComposer(); + } + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + shell.requestSubmit(); + } + }); + shell.addEventListener("submit", (event) => { + event.preventDefault(); + if (validate() !== null) return; + submit.disabled = true; + error.textContent = ""; + void this.#rpc + .call("createThread", { + bbThreadId: context.threadId, + message: context.message, + selector: { + ...context.selection!, + rects: context.selection!.rects.map((rect) => ({ ...rect })), + }, + body: textarea.value, + }) + .then((detail) => { + sessionStorage.removeItem(key); + this.closeComposer(); + this.#openThreadId = detail.thread.id; + return this.refresh(); + }) + .then(() => this.openThread(detailId(this.#openThreadId))) + .catch((caught) => { + submit.disabled = false; + error.textContent = errorMessage(caught); + }); + }); + validate(); + this.#composer = shell; + this.#portal.append(shell); + this.#outsideComposer = (event) => { + if (event.target instanceof Node && shell.contains(event.target)) return; + persist(); + this.closeComposer(); + }; + document.addEventListener("pointerdown", this.#outsideComposer, true); + requestAnimationFrame(() => textarea.focus()); + } + + async focusThread(commentThreadId: string): Promise { + const request = ++this.#focusNonce; + this.#openThreadId = commentThreadId; + await this.refresh(); + if (request !== this.#focusNonce || this.#destroyed) return false; + const restored = this.#restored.get(commentThreadId); + if (restored === undefined) return false; + const scrollRoot = restored.window.querySelector( + "[data-bb-thread-scroll-root]", + ); + if (scrollRoot === null) return false; + const rangeRect = restored.range.getBoundingClientRect(); + const scrollRect = scrollRoot.getBoundingClientRect(); + scrollRoot.scrollBy({ + top: + rangeRect.top + + rangeRect.height / 2 - + (scrollRect.top + scrollRect.height / 2), + behavior: "smooth", + }); + this.setActive([commentThreadId]); + await new Promise((resolve) => + requestAnimationFrame(() => resolve()), + ); + if (request !== this.#focusNonce || this.#destroyed) return false; + this.scheduleLayout(); + restored.marker?.focus({ preventScroll: true }); + await this.openThread(commentThreadId); + return true; + } + + scheduleRefresh(): void { + this.#refreshNonce += 1; + queueMicrotask(() => { + void this.refresh().catch((error: unknown) => { + if (!this.#destroyed) + console.error("timeline-comments refresh failed", error); + }); + }); + } + + async refresh(): Promise { + if (this.#destroyed) return; + if (this.#refreshing !== null) { + await this.#refreshing; + if (!this.#destroyed) return this.refresh(); + return; + } + const nonce = this.#refreshNonce; + this.#refreshing = this.loadAnchors(); + try { + await this.#refreshing; + } finally { + this.#refreshing = null; + } + if (nonce !== this.#refreshNonce && !this.#destroyed) await this.refresh(); + } + + private async loadAnchors(): Promise { + const threadIds = [ + ...document.querySelectorAll("[data-bb-thread-window]"), + ] + .map((node) => node.dataset.bbThreadWindow) + .filter((id): id is string => typeof id === "string" && id !== "") + .filter((id, index, all) => all.indexOf(id) === index) + .slice(0, 20); + this.#anchors.clear(); + if (threadIds.length > 0) { + let cursor: string | undefined; + do { + const page = await this.#rpc.call("listOpenAnchors", { + threadIds, + ...(cursor !== undefined ? { cursor } : {}), + }); + if (this.#destroyed) return; + for (const anchor of page.anchors) + this.#anchors.set(anchor.id, anchor as TimelineCommentThreadSummary); + cursor = page.nextCursor ?? undefined; + } while (cursor !== undefined && !this.#destroyed); + } + if (this.#destroyed) return; + this.restoreAll(); + } + + private findWindow(threadId: string): HTMLElement | null { + return document.querySelector( + `[data-bb-thread-window="${escapeSelector(threadId)}"]`, + ); + } + + private findProse(threadId: string, messageId: string): HTMLElement | null { + const windowNode = this.findWindow(threadId); + const row = windowNode?.querySelector( + `[data-bb-conversation-message-id="${escapeSelector(messageId)}"]`, + ); + return ( + row?.querySelector("[data-bb-message-prose-root]") ?? null + ); + } + + private restoreAll(): void { + this.#restored.clear(); + this.#overlay.replaceChildren(); + this.#resizeObserver?.disconnect(); + const health = new Map(); + for (const anchor of this.#anchors.values()) { + const windowNode = this.findWindow(anchor.bbThreadId); + const prose = this.findProse(anchor.bbThreadId, anchor.messageId); + if (windowNode === null || prose === null) { + health.set(anchor.id, "not-mounted"); + continue; + } + const restored = restoreSelector(prose, anchor.selector); + if (restored === null) { + health.set(anchor.id, "unanchored"); + continue; + } + health.set(anchor.id, "anchored"); + this.#resizeObserver?.observe(windowNode); + this.#resizeObserver?.observe(prose); + const fragments = [...restored.range.getClientRects()].filter( + (rect) => rect.width > 0 || rect.height > 0, + ); + const fallback = restored.range.getBoundingClientRect(); + const rects = fragments.length > 0 ? fragments : [fallback]; + const proseRect = prose.getBoundingClientRect(); + const side = chooseNearestGutter(rects, { + left: proseRect.left, + right: proseRect.right, + width: windowNode.getBoundingClientRect().width, + }); + const desiredY = + rects.reduce((sum, rect) => sum + rect.top + rect.height / 2, 0) / + rects.length; + this.#restored.set(anchor.id, { + anchor, + range: restored.range, + marker: null, + side, + desiredY, + window: windowNode, + prose, + }); + } + publishTimelineCommentAnchorHealth(health); + this.rebuildHighlights(); + this.layoutMarkers(); + } + + private rebuildHighlights(): void { + const registry = globalThis.CSS?.highlights; + const HighlightClass = globalThis.Highlight; + if (registry === undefined || HighlightClass === undefined) return; + const normal = new HighlightClass(); + const active = new HighlightClass(); + for (const [id, restored] of this.#restored) { + const paintRange = + typeof globalThis.StaticRange === "undefined" + ? restored.range + : new StaticRange({ + startContainer: restored.range.startContainer, + startOffset: restored.range.startOffset, + endContainer: restored.range.endContainer, + endOffset: restored.range.endOffset, + }); + normal.add(paintRange); + if (this.#activeIds.has(id)) active.add(paintRange); + } + if (this.#provisionalRange !== null) { + const range = this.#provisionalRange; + active.add( + typeof globalThis.StaticRange === "undefined" + ? range + : new StaticRange({ + startContainer: range.startContainer, + startOffset: range.startOffset, + endContainer: range.endContainer, + endOffset: range.endOffset, + }), + ); + } + registry.set(NORMAL_HIGHLIGHT, normal); + registry.set(ACTIVE_HIGHLIGHT, active); + } + + private scheduleLayout(): void { + if (this.#frame !== null) return; + this.#frame = requestAnimationFrame(() => { + this.#frame = null; + this.layoutMarkers(); + this.positionPopover(); + this.positionActionsMenu(); + }); + } + + private layoutMarkers(): void { + this.#overlay.replaceChildren(); + for (const restored of this.#restored.values()) { + const rects = [...restored.range.getClientRects()]; + const bounding = restored.range.getBoundingClientRect(); + const fragments = rects.length > 0 ? rects : [bounding]; + restored.desiredY = + fragments.reduce((sum, rect) => sum + rect.top + rect.height / 2, 0) / + fragments.length; + const proseRect = restored.prose.getBoundingClientRect(); + restored.side = chooseNearestGutter(fragments, { + left: proseRect.left, + right: proseRect.right, + width: restored.window.getBoundingClientRect().width, + }); + restored.marker = null; + } + const groups = new Map(); + for (const restored of this.#restored.values()) { + const key = `${restored.anchor.bbThreadId}:${restored.side}`; + const list = groups.get(key) ?? []; + list.push(restored); + groups.set(key, list); + } + for (const list of groups.values()) { + const windowRect = list[0]!.window.getBoundingClientRect(); + const top = Math.max(8, windowRect.top); + const bottom = Math.min(window.innerHeight - 8, windowRect.bottom); + for (const placement of layoutGutterMarkers( + list.map(({ anchor, desiredY }) => ({ id: anchor.id, desiredY })), + top, + bottom, + )) { + const threads = placement.ids + .map((id) => this.#restored.get(id)!) + .filter(Boolean); + const marker = element( + "button", + "bb-comments-marker", + ) as HTMLButtonElement; + marker.type = "button"; + marker.dataset.bbCommentGutter = threads[0]!.side; + marker.style.top = `${placement.y}px`; + const proseRects = threads.map(({ prose }) => + prose.getBoundingClientRect(), + ); + const gutterX = + threads[0]!.side === "left" + ? Math.min(...proseRects.map(({ left }) => left)) - + MARKER_SIZE - + MARKER_TEXT_GAP + : Math.max(...proseRects.map(({ right }) => right)) + + MARKER_TEXT_GAP; + marker.style.left = `${Math.max( + 8, + Math.min(window.innerWidth - MARKER_SIZE - 8, gutterX), + )}px`; + marker.setAttribute( + "aria-label", + threads.length === 1 + ? `Open comment thread${threads[0]!.anchor.replyCount > 0 ? ` with ${threads[0]!.anchor.replyCount} ${threads[0]!.anchor.replyCount === 1 ? "reply" : "replies"}` : ""}` + : `Open ${threads.length} comment threads`, + ); + marker.append(icon(StickyNote)); + if (threads.length > 1) { + marker.classList.add("bb-comments-marker-cluster"); + marker.append( + element( + "span", + "bb-comments-marker-count", + String(threads.length), + ), + ); + } + marker.addEventListener("mouseenter", () => + this.setActive(placement.ids), + ); + marker.addEventListener("mouseleave", () => this.setActive([])); + marker.addEventListener("focus", () => this.setActive(placement.ids)); + marker.addEventListener("blur", () => this.setActive([])); + marker.addEventListener("click", () => { + if (threads.length === 1) void this.openThread(threads[0]!.anchor.id); + else this.openCluster(marker, threads); + }); + this.#overlay.append(marker); + for (const thread of threads) thread.marker = marker; + } + } + } + + private setActive(ids: string[]): void { + this.#activeIds = new Set(ids); + this.rebuildHighlights(); + } + + private openCluster( + marker: HTMLButtonElement, + threads: RestoredThread[], + ): void { + this.closePopover(); + const menu = element("div", "bb-comments-popover bb-comments-cluster"); + menu.setAttribute("role", "dialog"); + menu.setAttribute("aria-label", "Comment threads"); + let first: HTMLButtonElement | null = null; + for (const thread of threads) { + const button = element( + "button", + "bb-comments-cluster-row", + ) as HTMLButtonElement; + button.type = "button"; + button.textContent = sourceExcerpt(thread.anchor.selector.exact); + button.addEventListener( + "click", + () => void this.openThread(thread.anchor.id), + ); + first ??= button; + menu.append(button); + } + this.#popover = menu; + this.#portal.append(menu); + this.installPopoverDismissal(marker); + this.positionNear(marker, menu); + first?.focus({ preventScroll: true }); + } + + private async openThread(commentThreadId: string): Promise { + const anchor = + this.#anchors.get(commentThreadId) ?? + this.#restored.get(commentThreadId)?.anchor; + if (anchor === undefined) return; + this.#openThreadId = commentThreadId; + this.closePopover(false); + this.setActive([commentThreadId]); + const popover = element( + "section", + "bb-comments-popover bb-comments-thread", + ); + popover.setAttribute("role", "dialog"); + popover.setAttribute("aria-label", "Comment thread"); + popover.tabIndex = -1; + popover.append(element("div", "bb-comments-loading", "Loading…")); + this.#popover = popover; + this.#portal.append(popover); + this.installPopoverDismissal( + this.#restored.get(commentThreadId)?.marker ?? null, + ); + this.positionPopover(); + popover.focus({ preventScroll: true }); + try { + const detail = await this.loadThread(anchor.bbThreadId, commentThreadId); + if (this.#popover !== popover) return; + this.renderThreadPopover(popover, detail); + this.positionPopover(); + } catch (caught) { + popover.replaceChildren( + element("div", "bb-comments-error", errorMessage(caught)), + ); + } + } + + private async loadThread( + bbThreadId: string, + commentThreadId: string, + ): Promise { + let cursor: string | undefined; + let detail: TimelineCommentThreadDetail | null = null; + do { + const page = (await this.#rpc.call("getCommentThread", { + bbThreadId, + commentThreadId, + ...(cursor !== undefined ? { cursor } : {}), + })) as TimelineCommentThreadDetail; + detail = + detail === null + ? page + : { ...page, comments: [...detail.comments, ...page.comments] }; + cursor = page.nextCursor ?? undefined; + } while (cursor !== undefined); + return detail!; + } + + private renderThreadPopover( + popover: HTMLElement, + detail: TimelineCommentThreadDetail, + ): void { + this.closeActionsMenu(); + delete popover.dataset.editing; + popover.replaceChildren(); + const header = element("header", "bb-comments-thread-header"); + const source = element("div", "bb-comments-thread-source"); + source.append(icon(StickyNote), document.createTextNode("Comment")); + const headerActions = element("div", "bb-comments-header-actions"); + const resolve = element( + "button", + "bb-comments-icon-control", + ) as HTMLButtonElement; + resolve.type = "button"; + resolve.setAttribute( + "aria-label", + detail.thread.resolvedAt === null ? "Resolve thread" : "Reopen thread", + ); + resolve.title = + detail.thread.resolvedAt === null ? "Resolve thread" : "Reopen thread"; + resolve.setAttribute( + "aria-pressed", + String(detail.thread.resolvedAt !== null), + ); + resolve.append(icon(CheckCheck)); + resolve.addEventListener("click", () => { + resolve.disabled = true; + void this.#rpc + .call("setThreadResolved", { + bbThreadId: detail.thread.bbThreadId, + commentThreadId: detail.thread.id, + expectedVersion: detail.thread.version, + resolved: detail.thread.resolvedAt === null, + }) + .then(() => { + this.closePopover(); + this.scheduleRefresh(); + }) + .catch((caught) => { + resolve.disabled = false; + this.handlePopoverMutationError(popover, detail, caught); + }); + }); + const removeThread = element( + "button", + "bb-comments-icon-control bb-comments-destructive", + ) as HTMLButtonElement; + removeThread.type = "button"; + removeThread.setAttribute("aria-label", "Delete thread"); + removeThread.title = "Delete thread"; + removeThread.append(icon(Trash2)); + removeThread.addEventListener("click", () => { + if (!window.confirm("Delete this comment thread?")) return; + const root = detail.comments.find(({ parentId }) => parentId === null); + if (root === undefined) return; + removeThread.disabled = true; + void this.#rpc + .call("deleteComment", { + bbThreadId: detail.thread.bbThreadId, + commentId: root.id, + expectedVersion: root.version, + expectedThreadVersion: detail.thread.version, + }) + .then(() => { + this.closePopover(); + this.scheduleRefresh(); + }) + .catch((caught) => { + removeThread.disabled = false; + this.handlePopoverMutationError(popover, detail, caught); + }); + }); + headerActions.append(resolve, removeThread); + header.append(source, headerActions); + popover.append(header); + + const comments = element("div", "bb-comments-thread-comments"); + for (const comment of detail.comments) + comments.append(this.renderComment(detail, comment, popover)); + popover.append(comments); + + if (detail.thread.resolvedAt === null) { + const reply = element("form", "bb-comments-reply"); + const replyComposer = element("div", "bb-comments-inline-composer"); + const draftKey = `bb.timeline-comments.reply:${detail.thread.id}`; + const textarea = element( + "textarea", + "bb-comments-reply-input", + ) as HTMLTextAreaElement; + textarea.placeholder = "Reply..."; + textarea.maxLength = 20_000; + textarea.value = readDraft(draftKey) ?? ""; + const send = element( + "button", + "bb-comments-submit-shortcut", + ) as HTMLButtonElement; + send.type = "submit"; + send.setAttribute("aria-label", "Reply"); + send.title = "Reply · ⌘/Ctrl Enter"; + send.append(icon(Command), icon(CornerDownLeft)); + const error = element("div", "bb-comments-error"); + error.setAttribute("role", "status"); + const validate = () => { + const message = commentBodyError(textarea.value); + send.disabled = message !== null; + error.textContent = + message !== null && textarea.value.trim() !== "" ? message : ""; + return message; + }; + textarea.addEventListener("input", () => { + writeDraft(draftKey, textarea.value); + syncInlineComposerLayout(textarea, replyComposer); + validate(); + }); + textarea.addEventListener("keydown", (event) => { + if ( + event.key === "Enter" && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + reply.requestSubmit(); + } + }); + replyComposer.append(textarea, send); + reply.append(replyComposer, error); + reply.addEventListener("submit", (event) => { + event.preventDefault(); + if (validate() !== null) return; + send.disabled = true; + void this.#rpc + .call("reply", { + bbThreadId: detail.thread.bbThreadId, + commentThreadId: detail.thread.id, + body: textarea.value, + }) + .then(() => { + sessionStorage.removeItem(draftKey); + return this.openThread(detail.thread.id); + }) + .catch((caught) => { + send.disabled = false; + this.handlePopoverMutationError(popover, detail, caught); + }); + }); + validate(); + popover.append(reply); + syncInlineComposerLayout(textarea, replyComposer, false); + } + } + + private renderComment( + detail: TimelineCommentThreadDetail, + comment: TimelineComment, + popover: HTMLElement, + ): HTMLElement { + const row = element("article", "bb-comments-comment"); + row.dataset.bbCommentId = comment.id; + const buildHeader = (action: HTMLElement): HTMLElement => { + const header = element("header", "bb-comments-message-header"); + const byline = element("div"); + byline.append(element("strong", undefined, "Me")); + const timestamp = element("time", undefined, relativeTime(comment.createdAt)); + timestamp.dateTime = new Date(comment.createdAt).toISOString(); + timestamp.title = formatTime(comment.createdAt); + byline.append(timestamp); + header.append(byline, action); + return header; + }; + const body = element("p", "bb-comments-comment-body", comment.body); + const actions = element("div", "bb-comments-actions-menu"); + const actionsTrigger = element( + "button", + "bb-comments-icon-control", + ) as HTMLButtonElement; + actionsTrigger.type = "button"; + actionsTrigger.setAttribute("aria-label", "Comment actions"); + actionsTrigger.setAttribute("aria-haspopup", "menu"); + actionsTrigger.setAttribute("aria-expanded", "false"); + actionsTrigger.title = "Comment actions"; + actionsTrigger.append(icon(EllipsisVertical)); + const actionsMenu = element("div", "bb-comments-actions-popover"); + actionsMenu.setAttribute("role", "menu"); + const menuItems = () => + [...actionsMenu.querySelectorAll('[role="menuitem"]')] + .filter((item) => !item.disabled); + actionsMenu.addEventListener("keydown", (event) => { + const items = menuItems(); + const current = items.indexOf(document.activeElement as HTMLButtonElement); + let next: HTMLButtonElement | undefined; + if (event.key === "ArrowDown") + next = items[(current + 1 + items.length) % items.length]; + if (event.key === "ArrowUp") + next = items[(current - 1 + items.length) % items.length]; + if (event.key === "Home") next = items[0]; + if (event.key === "End") next = items.at(-1); + if (next !== undefined) { + event.preventDefault(); + next.focus({ preventScroll: true }); + return; + } + if (event.key !== "Tab") return; + event.preventDefault(); + this.focusAdjacentToActionsTrigger(actionsTrigger, event.shiftKey); + this.closeActionsMenu(); + }); + actionsMenu.addEventListener("focusout", () => { + queueMicrotask(() => { + if ( + this.#actionsMenu === actionsMenu && + !actionsMenu.contains(document.activeElement) + ) { + this.closeActionsMenu(); + } + }); + }); + actionsTrigger.addEventListener("click", () => { + if (this.#actionsTrigger === actionsTrigger) { + this.closeActionsMenu(); + return; + } + this.openActionsMenu(actionsTrigger, actionsMenu); + }); + const edit = element("button") as HTMLButtonElement; + edit.type = "button"; + edit.tabIndex = -1; + edit.setAttribute("role", "menuitem"); + edit.append(icon(Pencil), document.createTextNode("Edit")); + edit.addEventListener("click", () => { + const draftKey = `bb.timeline-comments.edit:${comment.id}`; + popover.dataset.editing = "true"; + row.dataset.editing = "true"; + this.closeActionsMenu(); + const textarea = element( + "textarea", + "bb-comments-edit-input", + ) as HTMLTextAreaElement; + textarea.setAttribute("aria-label", "Edit comment"); + textarea.maxLength = 20_000; + textarea.value = readDraft(draftKey) ?? comment.body; + const editComposer = element("div", "bb-comments-inline-composer"); + const save = element( + "button", + "bb-comments-submit-shortcut", + ) as HTMLButtonElement; + save.type = "button"; + save.append(icon(Command), icon(CornerDownLeft)); + save.setAttribute("aria-label", "Save comment"); + save.title = "Save comment · ⌘/Ctrl Enter"; + const error = element("div", "bb-comments-error"); + error.setAttribute("role", "status"); + const validate = () => { + const message = commentBodyError(textarea.value); + save.disabled = message !== null; + error.textContent = + message !== null && textarea.value.trim() !== "" ? message : ""; + return message; + }; + const cancelEdit = () => { + sessionStorage.removeItem(draftKey); + this.renderThreadPopover(popover, detail); + popover + .querySelector( + `[data-bb-comment-id="${escapeSelector(comment.id)}"] ` + + '.bb-comments-actions-menu > button[aria-label="Comment actions"]', + ) + ?.focus({ preventScroll: true }); + }; + textarea.addEventListener("input", () => { + if (textarea.value === comment.body) sessionStorage.removeItem(draftKey); + else writeDraft(draftKey, textarea.value); + syncInlineComposerLayout(textarea, editComposer); + validate(); + }); + textarea.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + cancelEdit(); + } + if ( + event.key === "Enter" && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + save.click(); + } + }); + save.addEventListener("click", () => { + if (validate() !== null) return; + const body = textarea.value.trim(); + if (body === comment.body) { + cancelEdit(); + return; + } + save.disabled = true; + void this.#rpc + .call("updateComment", { + bbThreadId: detail.thread.bbThreadId, + commentId: comment.id, + expectedVersion: comment.version, + body, + }) + .then(() => { + sessionStorage.removeItem(draftKey); + return this.openThread(detail.thread.id); + }) + .catch((caught) => { + save.disabled = false; + this.handlePopoverMutationError(popover, detail, caught); + }); + }); + editComposer.append(textarea, save); + row.replaceChildren(buildHeader(actions), editComposer, error); + syncInlineComposerLayout(textarea, editComposer, false); + validate(); + textarea.focus(); + }); + const remove = element( + "button", + "bb-comments-destructive", + ) as HTMLButtonElement; + remove.type = "button"; + remove.tabIndex = -1; + remove.setAttribute("role", "menuitem"); + remove.append(icon(Trash2), document.createTextNode("Delete")); + remove.addEventListener("click", () => { + if ( + !window.confirm( + comment.parentId === null + ? "Delete this comment thread?" + : "Delete this reply?", + ) + ) + return; + remove.disabled = true; + void this.#rpc + .call("deleteComment", { + bbThreadId: detail.thread.bbThreadId, + commentId: comment.id, + expectedVersion: comment.version, + expectedThreadVersion: detail.thread.version, + }) + .then((result) => { + if (result.deletedThreadId !== null) this.closePopover(); + else void this.openThread(detail.thread.id); + this.scheduleRefresh(); + }) + .catch((caught) => { + remove.disabled = false; + this.handlePopoverMutationError(popover, detail, caught); + }); + }); + actionsMenu.append(edit, remove); + actions.append(actionsTrigger); + row.append(buildHeader(actions), body); + return row; + } + + private focusAdjacentToActionsTrigger( + trigger: HTMLButtonElement, + backwards: boolean, + ): void { + const popover = trigger.closest(".bb-comments-thread"); + if (popover === null) { + trigger.focus({ preventScroll: true }); + return; + } + const focusable = [ + ...popover.querySelectorAll( + 'button:not(:disabled), textarea:not(:disabled), input:not(:disabled), [href], [tabindex]:not([tabindex="-1"])', + ), + ].filter((node) => node.getClientRects().length > 0); + const current = focusable.indexOf(trigger); + const adjacent = focusable[current + (backwards ? -1 : 1)] ?? trigger; + adjacent.focus({ preventScroll: true }); + } + + private openActionsMenu( + trigger: HTMLButtonElement, + menu: HTMLElement, + ): void { + this.closeActionsMenu(); + this.#actionsMenu = menu; + this.#actionsTrigger = trigger; + trigger.setAttribute("aria-expanded", "true"); + this.#portal.append(menu); + this.positionActionsMenu(); + if (this.#actionsMenu !== menu) return; + + this.#outsideActionsMenu = (event) => { + if ( + event.target instanceof Node && + (menu.contains(event.target) || trigger.contains(event.target)) + ) { + return; + } + this.closeActionsMenu(); + }; + document.addEventListener("pointerdown", this.#outsideActionsMenu, true); + menu.querySelector("button")?.focus({ + preventScroll: true, + }); + } + + private positionActionsMenu(): void { + const menu = this.#actionsMenu; + const trigger = this.#actionsTrigger; + if (menu === null || trigger === null) return; + if (!trigger.isConnected) { + this.closeActionsMenu(); + return; + } + const triggerRect = trigger.getBoundingClientRect(); + const scrollViewport = trigger.closest( + ".bb-comments-thread-comments", + ); + if (scrollViewport !== null) { + const viewportRect = scrollViewport.getBoundingClientRect(); + if ( + triggerRect.top < viewportRect.top || + triggerRect.bottom > viewportRect.bottom + ) { + this.closeActionsMenu(); + return; + } + } + const menuRect = menu.getBoundingClientRect(); + const gap = 4; + const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8); + const left = Math.max( + 8, + Math.min( + maxLeft, + triggerRect.right - menuRect.width, + ), + ); + const below = triggerRect.bottom + gap; + const candidateTop = + below + menuRect.height <= window.innerHeight - 8 + ? below + : triggerRect.top - menuRect.height - gap; + const maxTop = Math.max(8, window.innerHeight - menuRect.height - 8); + const top = Math.max(8, Math.min(maxTop, candidateTop)); + menu.style.left = `${left}px`; + menu.style.top = `${top}px`; + } + + private closeActionsMenu(restoreFocus = false): void { + const trigger = this.#actionsTrigger; + if (this.#outsideActionsMenu !== null) { + document.removeEventListener( + "pointerdown", + this.#outsideActionsMenu, + true, + ); + this.#outsideActionsMenu = null; + } + this.#actionsMenu?.remove(); + this.#actionsMenu = null; + this.#actionsTrigger = null; + trigger?.setAttribute("aria-expanded", "false"); + if (restoreFocus && trigger?.isConnected === true) + trigger.focus({ preventScroll: true }); + } + + private showPopoverError(popover: HTMLElement, error: unknown): void { + const existing = popover.querySelector(".bb-comments-error"); + const node = existing ?? element("div", "bb-comments-error"); + node.textContent = errorMessage(error); + if (existing === null) popover.append(node); + } + + private handlePopoverMutationError( + popover: HTMLElement, + detail: TimelineCommentThreadDetail, + error: unknown, + ): void { + this.showPopoverError(popover, error); + if (!/changed/iu.test(errorMessage(error))) return; + void this.loadThread(detail.thread.bbThreadId, detail.thread.id).then( + (fresh) => { + if (this.#popover !== popover) return; + this.renderThreadPopover(popover, fresh); + this.showPopoverError(popover, error); + }, + ); + } + + private installPopoverDismissal(invoker: HTMLElement | null): void { + this.removePopoverDismissal(); + this.#popoverInvoker = invoker; + this.#outsidePopover = (event) => { + if ( + event.target instanceof Node && + (this.#popover?.contains(event.target) === true || + this.#popoverInvoker?.contains(event.target) === true || + this.#actionsMenu?.contains(event.target) === true) + ) { + return; + } + this.closePopover(); + }; + this.#popoverKeydown = (event) => { + if (event.key !== "Escape") return; + if (this.#actionsMenu !== null) { + event.preventDefault(); + this.closeActionsMenu(true); + return; + } + if ( + event.target instanceof Element && + event.target.closest(".bb-comments-edit-input") !== null + ) { + return; + } + event.preventDefault(); + this.closePopover(); + }; + document.addEventListener("pointerdown", this.#outsidePopover, true); + document.addEventListener("keydown", this.#popoverKeydown, true); + } + + private removePopoverDismissal(): void { + if (this.#outsidePopover !== null) { + document.removeEventListener("pointerdown", this.#outsidePopover, true); + this.#outsidePopover = null; + } + if (this.#popoverKeydown !== null) { + document.removeEventListener("keydown", this.#popoverKeydown, true); + this.#popoverKeydown = null; + } + } + + private positionPopover(): void { + if (this.#popover === null || this.#openThreadId === null) return; + const marker = this.#restored.get(this.#openThreadId)?.marker; + if (marker !== null && marker !== undefined) + this.positionNear(marker, this.#popover); + } + + private positionNear(anchor: HTMLElement, popover: HTMLElement): void { + const rect = anchor.getBoundingClientRect(); + const width = Math.min(340, window.innerWidth - 16); + popover.style.width = `${width}px`; + const leftOption = rect.left - width - 8; + const rightOption = rect.right + 8; + const fitsLeft = leftOption >= 8; + const fitsRight = rightOption + width <= window.innerWidth - 8; + const gutter = anchor.dataset.bbCommentGutter; + const left = + gutter === "left" && fitsLeft + ? leftOption + : gutter === "right" && fitsRight + ? rightOption + : fitsRight + ? rightOption + : leftOption; + popover.style.left = `${Math.max(8, Math.min(window.innerWidth - width - 8, left))}px`; + const height = Math.min( + popover.getBoundingClientRect().height || 300, + window.innerHeight - 16, + ); + popover.style.top = `${Math.max(8, Math.min(window.innerHeight - height - 8, rect.top - 8))}px`; + } + + private closeComposer(): void { + if (this.#outsideComposer !== null) { + document.removeEventListener("pointerdown", this.#outsideComposer, true); + this.#outsideComposer = null; + } + this.#composer?.remove(); + this.#composer = null; + this.#provisionalRange = null; + this.rebuildHighlights(); + } + + private closePopover(clearOpen = true): void { + const invoker = this.#popoverInvoker; + const currentMarker = + this.#openThreadId === null + ? null + : (this.#restored.get(this.#openThreadId)?.marker ?? null); + const focusTarget = + invoker?.isConnected === true ? invoker : currentMarker; + this.closeActionsMenu(); + this.removePopoverDismissal(); + this.#popoverInvoker = null; + this.#popover?.remove(); + this.#popover = null; + if (clearOpen) this.#openThreadId = null; + this.setActive([]); + if (clearOpen && focusTarget?.isConnected === true) { + focusTarget.focus({ preventScroll: true }); + } + } + + destroy(): void { + if (this.#destroyed) return; + this.#destroyed = true; + this.#focusNonce += 1; + this.#observer.disconnect(); + this.#resizeObserver?.disconnect(); + if (this.#frame !== null) cancelAnimationFrame(this.#frame); + for (const dispose of this.#disposers.splice(0).reverse()) dispose(); + this.closeComposer(); + this.closePopover(); + this.#portal.remove(); + this.#highlightStyle.remove(); + publishTimelineCommentAnchorHealth(new Map()); + globalThis.CSS?.highlights?.delete(NORMAL_HIGHLIGHT); + globalThis.CSS?.highlights?.delete(ACTIVE_HIGHLIGHT); + } +} + +function detailId(id: string | null): string { + if (id === null) throw new Error("Comment thread was not created"); + return id; +} + +export function mountTimelineCommentsController( + context: PluginContentScriptContext, +): () => void { + const controller = new TimelineCommentsController(context); + return () => controller.destroy(); +} diff --git a/plugins/timeline-comments/docs/screenshot.png b/plugins/timeline-comments/docs/screenshot.png new file mode 100644 index 0000000..aa7637f Binary files /dev/null and b/plugins/timeline-comments/docs/screenshot.png differ diff --git a/plugins/timeline-comments/docs/selection-action.png b/plugins/timeline-comments/docs/selection-action.png new file mode 100644 index 0000000..4d191e0 Binary files /dev/null and b/plugins/timeline-comments/docs/selection-action.png differ diff --git a/plugins/timeline-comments/docs/send-to-agent.png b/plugins/timeline-comments/docs/send-to-agent.png new file mode 100644 index 0000000..5878b5a Binary files /dev/null and b/plugins/timeline-comments/docs/send-to-agent.png differ diff --git a/plugins/timeline-comments/global.d.ts b/plugins/timeline-comments/global.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/plugins/timeline-comments/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/plugins/timeline-comments/package.json b/plugins/timeline-comments/package.json new file mode 100644 index 0000000..247fff5 --- /dev/null +++ b/plugins/timeline-comments/package.json @@ -0,0 +1,53 @@ +{ + "name": "bb-plugin-timeline-comments", + "version": "0.1.0", + "description": "Attach durable comment threads to selected text in bb timelines.", + "type": "module", + "license": "UNLICENSED", + "files": [ + "dist", + "docs", + "README.md" + ], + "scripts": { + "build": "node ../../tooling/build-plugin.mjs", + "check": "npm run typecheck && npm run build && npm test", + "test": "vitest run", + "test:browser": "node ./browser-check/run.mjs", + "typecheck": "tsc --noEmit" + }, + "engines": { + "bb": ">=0.0.34", + "bbPluginSdk": "^0.4.0" + }, + "bb": { + "name": "Timeline Comments", + "description": "Attach durable comment threads to selected text in bb timelines.", + "branding": { + "icon": "MessageSquare" + }, + "server": "./server.ts", + "app": "./app.tsx", + "skills": [] + }, + "devDependencies": { + "@bb/plugin-sdk": "file:../../tooling/vendor/bb-plugin-sdk-0.4.0.tgz", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "better-sqlite3": "^12.10.0", + "cron-parser": "^5.5.0", + "esbuild": "^0.28.0", + "hono": "^4.11.9", + "jsdom": "^29.0.1", + "lucide": "^0.474.0", + "lucide-react": "^0.474.0", + "playwright": "^1.55.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "typescript": "^5.7.0", + "vitest": "^4.1.8", + "zod": "^4.3.6" + } +} diff --git a/plugins/timeline-comments/server.test.ts b/plugins/timeline-comments/server.test.ts new file mode 100644 index 0000000..9e884df --- /dev/null +++ b/plugins/timeline-comments/server.test.ts @@ -0,0 +1,726 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createFakePluginHost, + makeThreadResponse, + type FakePluginHost, +} from "@bb/plugin-sdk/testing"; +import { z } from "zod"; +import plugin, { + commentBodySchema, + commentThreadDetailSchema, + renderedTextSelectorSchema, +} from "./server.js"; + +afterEach(() => vi.restoreAllMocks()); + +const threadPageSchema = z.object({ + threads: z.array(commentThreadDetailSchema.shape.thread), + nextCursor: z.string().nullable(), +}); + +async function loadPlugin(): Promise { + const host = createFakePluginHost({ pluginId: "timeline-comments" }); + await plugin(host.bb); + return host; +} + +function createInput( + body: string, + overrides: { bbThreadId?: string; messageId?: string; exact?: string } = {}, +) { + const bbThreadId = overrides.bbThreadId ?? "thr_1"; + const exact = overrides.exact ?? "stable"; + return { + bbThreadId, + message: { + id: overrides.messageId ?? "msg_1", + threadId: bbThreadId, + role: "assistant" as const, + text: exact, + sourceSeqEnd: 4, + }, + selector: { + version: 1 as const, + coordinateSpace: "rendered-text-utf16" as const, + start: 0, + end: exact.length, + exact, + prefix: "", + suffix: "", + rects: [{ x: 10, y: 20, width: 40, height: 18 }], + }, + body, + }; +} + +async function createComment( + host: FakePluginHost, + body: string, + overrides?: Parameters[1], +) { + return commentThreadDetailSchema.parse( + await host.harness.callRpc("createThread", createInput(body, overrides)), + ); +} + +describe("timeline comments backend", () => { + it("enforces foreign keys and creates a scoped root thread before publishing", async () => { + const host = await loadPlugin(); + const db = host.bb.storage.database(); + expect(db.pragma("foreign_keys", { simple: true })).toBe(1); + + const created = await createComment(host, "Make the contract explicit."); + expect(created.thread).toMatchObject({ + bbThreadId: "thr_1", + messageId: "msg_1", + messageRole: "assistant", + replyCount: 0, + resolvedAt: null, + selector: { exact: "stable", start: 0, end: 6 }, + rootComment: { body: "Make the contract explicit.", version: 1 }, + }); + expect(created.comments).toHaveLength(1); + expect(host.harness.realtimeSignals).toEqual([ + { + channel: "comments-changed", + payload: { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + }, + }, + ]); + + await expect( + host.harness.callRpc("createThread", { + ...createInput("Wrong scope"), + message: { ...createInput("Wrong scope").message, threadId: "thr_2" }, + }), + ).rejects.toThrow("does not belong"); + }); + + it("validates required bodies, Unicode length, selectors, and strict inputs", async () => { + const host = await loadPlugin(); + expect(() => commentBodySchema.parse(" ")).toThrow( + "Comment body is required", + ); + expect(() => commentBodySchema.parse("😀".repeat(10_001))).toThrow( + "10,000 Unicode code points", + ); + expect(() => + renderedTextSelectorSchema.parse({ + ...createInput("Valid body").selector, + end: 2, + }), + ).toThrow("UTF-16"); + await expect( + host.harness.callRpc("listCommentThreads", { + bbThreadId: "thr_1", + filter: "open", + unexpected: true, + }), + ).rejects.toThrow("rpc input validation failed"); + }); + + it("persists reply, edit, resolve, reopen, reply deletion, and root cascade with optimistic versions", async () => { + const host = await loadPlugin(); + const created = await createComment(host, "Root"); + const replied = commentThreadDetailSchema.parse( + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + body: "Reply", + }), + ); + const reply = replied.comments.find( + (comment) => comment.parentId !== null, + )!; + expect(replied.thread.replyCount).toBe(1); + + const edited = commentThreadDetailSchema.parse( + await host.harness.callRpc("updateComment", { + bbThreadId: "thr_1", + commentId: reply.id, + expectedVersion: reply.version, + body: "Edited reply", + }), + ); + expect(edited.comments.at(-1)?.body).toBe("Edited reply"); + await expect( + host.harness.callRpc("updateComment", { + bbThreadId: "thr_1", + commentId: reply.id, + expectedVersion: reply.version, + body: "Stale edit", + }), + ).rejects.toThrow("changed"); + + const resolved = commentThreadDetailSchema.parse( + await host.harness.callRpc("setThreadResolved", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + expectedVersion: edited.thread.version, + resolved: true, + }), + ); + expect(resolved.thread.resolvedAt).not.toBeNull(); + await expect( + host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + body: "Blocked", + }), + ).rejects.toThrow("Resolved"); + const reopened = commentThreadDetailSchema.parse( + await host.harness.callRpc("setThreadResolved", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + expectedVersion: resolved.thread.version, + resolved: false, + }), + ); + + const editedReply = reopened.comments.find( + (comment) => comment.parentId !== null, + )!; + const afterReplyDelete = await host.harness.callRpc("deleteComment", { + bbThreadId: "thr_1", + commentId: editedReply.id, + expectedVersion: editedReply.version, + expectedThreadVersion: reopened.thread.version, + }); + expect(afterReplyDelete).toMatchObject({ + deletedThreadId: null, + thread: { thread: { replyCount: 0 } }, + }); + + const latest = commentThreadDetailSchema.parse( + await host.harness.callRpc("getCommentThread", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + }), + ); + await host.harness.callRpc("deleteComment", { + bbThreadId: "thr_1", + commentId: latest.thread.rootComment.id, + expectedVersion: latest.thread.rootComment.version, + expectedThreadVersion: latest.thread.version, + }); + const db = host.bb.storage.database(); + expect( + db.prepare("SELECT COUNT(*) AS count FROM comment_threads").get(), + ).toEqual({ count: 0 }); + expect(db.prepare("SELECT COUNT(*) AS count FROM comments").get()).toEqual({ + count: 0, + }); + }); + + it("rejects a stale root deletion after a concurrent reply changes the thread", async () => { + const host = await loadPlugin(); + const created = await createComment(host, "Root"); + const replied = commentThreadDetailSchema.parse( + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + body: "Concurrent reply", + }), + ); + + await expect( + host.harness.callRpc("deleteComment", { + bbThreadId: "thr_1", + commentId: created.thread.rootComment.id, + expectedVersion: created.thread.rootComment.version, + expectedThreadVersion: created.thread.version, + }), + ).rejects.toThrow("Comment thread changed"); + + const retained = commentThreadDetailSchema.parse( + await host.harness.callRpc("getCommentThread", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + }), + ); + expect(retained.comments.map((comment) => comment.body)).toEqual([ + "Root", + "Concurrent reply", + ]); + + await host.harness.callRpc("deleteComment", { + bbThreadId: "thr_1", + commentId: retained.thread.rootComment.id, + expectedVersion: retained.thread.rootComment.version, + expectedThreadVersion: replied.thread.version, + }); + await expect( + host.harness.callRpc("getCommentThread", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + }), + ).rejects.toThrow("not found"); + }); + + it("keeps root and reply pagination independent and cursor-bound", async () => { + vi.spyOn(Date, "now").mockImplementation(() => 1_000); + const host = await loadPlugin(); + for (let index = 0; index < 51; index += 1) { + await createComment(host, `Root ${index}`, { + messageId: `msg_${index}`, + }); + } + const firstRoots = threadPageSchema.parse( + await host.harness.callRpc("listCommentThreads", { + bbThreadId: "thr_1", + filter: "open", + }), + ); + expect(firstRoots.threads).toHaveLength(50); + expect(firstRoots.nextCursor).not.toBeNull(); + const secondRoots = threadPageSchema.parse( + await host.harness.callRpc("listCommentThreads", { + bbThreadId: "thr_1", + filter: "open", + cursor: firstRoots.nextCursor ?? undefined, + }), + ); + expect(secondRoots.threads).toHaveLength(1); + await expect( + host.harness.callRpc("listCommentThreads", { + bbThreadId: "thr_other", + filter: "open", + cursor: firstRoots.nextCursor, + }), + ).rejects.toThrow("does not match"); + + const target = firstRoots.threads[0]!; + for (let index = 0; index < 100; index += 1) { + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: target.id, + body: `Reply ${index}`, + }); + } + const firstComments = commentThreadDetailSchema.parse( + await host.harness.callRpc("getCommentThread", { + bbThreadId: "thr_1", + commentThreadId: target.id, + }), + ); + expect(firstComments.comments).toHaveLength(100); + expect(firstComments.comments[0]?.parentId).toBeNull(); + expect( + firstComments.comments.slice(1).every(({ parentId }) => parentId !== null), + ).toBe(true); + expect(firstComments.nextCursor).not.toBeNull(); + const secondComments = commentThreadDetailSchema.parse( + await host.harness.callRpc("getCommentThread", { + bbThreadId: "thr_1", + commentThreadId: target.id, + cursor: firstComments.nextCursor ?? undefined, + }), + ); + expect(secondComments.comments).toHaveLength(1); + expect(secondComments.comments[0]?.parentId).not.toBeNull(); + const allComments = [ + ...firstComments.comments, + ...secondComments.comments, + ]; + expect(allComments).toHaveLength(101); + expect(new Set(allComments.map(({ id }) => id)).size).toBe(101); + expect( + allComments.filter(({ parentId }) => parentId === null), + ).toHaveLength(1); + + const firstCliPage = await host.harness.runCli( + ["get", target.id, "--limit", "1", "--json"], + { threadId: "thr_1" }, + ); + const firstCliComment = JSON.parse(firstCliPage.stdout ?? "").comments[0]; + expect(firstCliComment.parentId).toBeNull(); + const firstHumanCliPage = await host.harness.runCli( + ["get", target.id, "--limit", "1"], + { threadId: "thr_1" }, + ); + expect(firstHumanCliPage.stdout).toContain( + `Comment: ${target.rootComment.body}`, + ); + const secondCliPage = await host.harness.runCli( + [ + "get", + target.id, + "--limit", + "1", + "--cursor", + JSON.parse(firstCliPage.stdout ?? "").nextCursor, + "--json", + ], + { threadId: "thr_1" }, + ); + const secondCliResult = JSON.parse(secondCliPage.stdout ?? ""); + expect(secondCliResult.comments[0].parentId).not.toBeNull(); + const secondHumanCliPage = await host.harness.runCli( + [ + "get", + target.id, + "--limit", + "1", + "--cursor", + JSON.parse(firstCliPage.stdout ?? "").nextCursor, + ], + { threadId: "thr_1" }, + ); + expect(secondHumanCliPage.stdout).toContain( + `Reply: ${secondCliResult.comments[0].body}`, + ); + + const context = ( + await host.harness.registrations.mentionProviders[0]!.resolve("thr_1") + ).context; + expect(context.indexOf(`Comment: ${target.rootComment.body}`)).toBeLessThan( + context.indexOf("Reply: "), + ); + }); + + it("serializes every currently open comment for mention resolution and omits resolved threads", async () => { + const host = await loadPlugin(); + const first = await createComment(host, "First root", { + exact: "first source", + messageId: "msg_first", + }); + const second = await createComment(host, "Second root", { + exact: "second source", + messageId: "msg_second", + }); + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: first.thread.id, + body: "First reply", + }); + const summary = await host.harness.callRpc("getThreadHandoffSummary", { + bbThreadId: "thr_1", + }); + expect(summary).toMatchObject({ threadCount: 2, commentCount: 3 }); + + const provider = host.harness.registrations.mentionProviders[0]!; + const db = host.bb.storage.database(); + const prepare = vi.spyOn(db, "prepare"); + prepare.mockClear(); + await host.harness.callRpc("getThreadHandoffSummary", { + bbThreadId: "thr_1", + }); + expect( + prepare.mock.calls.filter(([sql]) => + String(sql).includes("JOIN comments c ON c.thread_id = t.id"), + ), + ).toHaveLength(1); + prepare.mockClear(); + provider.resolve("thr_1"); + expect( + prepare.mock.calls.filter(([sql]) => + String(sql).includes("JOIN comments c ON c.thread_id = t.id"), + ), + ).toHaveLength(1); + prepare.mockRestore(); + + expect( + provider.search({ + trigger: "@", + query: "comments", + projectId: "proj_1", + threadId: "thr_1", + }), + ).toMatchObject([{ title: "3 comments from 2 open threads" }]); + const resolved = await provider.resolve("thr_1"); + expect(resolved.context).toContain("first source"); + expect(resolved.context).toContain("First reply"); + expect(resolved.context).toContain("second source"); + + await host.harness.callRpc("setThreadResolved", { + bbThreadId: "thr_1", + commentThreadId: second.thread.id, + expectedVersion: second.thread.version, + resolved: true, + }); + expect( + provider.search({ + trigger: "@", + query: "comments", + projectId: "proj_1", + threadId: "thr_1", + }), + ).toMatchObject([{ title: "2 comments from 1 open threads" }]); + const refreshed = await provider.resolve("thr_1"); + expect(refreshed.context).not.toContain("second source"); + }); + + it("removes plugin-owned comments when their BB thread is deleted", async () => { + const host = await loadPlugin(); + await createComment(host, "Delete with the thread"); + await createComment(host, "Keep another thread", { + bbThreadId: "thr_2", + }); + + await host.harness.emitThreadEvent("thread.deleted", { + thread: makeThreadResponse({ id: "thr_1" }), + }); + + const db = host.bb.storage.database(); + expect( + db + .prepare( + "SELECT bb_thread_id AS bbThreadId, COUNT(*) AS count FROM comment_threads GROUP BY bb_thread_id", + ) + .all(), + ).toEqual([{ bbThreadId: "thr_2", count: 1 }]); + expect(db.prepare("SELECT COUNT(*) AS count FROM comments").get()).toEqual({ + count: 1, + }); + }); + + it("rejects bulk handoff before insertion and at mention resolution above 64k code points", async () => { + const host = await loadPlugin(); + const body = "x".repeat(10_000); + const created = await createComment(host, body); + for (let index = 0; index < 6; index += 1) { + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + body, + }); + } + host.bb.storage + .database() + .prepare( + `INSERT INTO comments ( + id, thread_id, parent_id, body, version, created_at, updated_at + ) VALUES (?, ?, ?, ?, 1, ?, ?)`, + ) + .run( + "invalid_after_limit", + created.thread.id, + created.thread.rootComment.id, + Buffer.from([1]), + 9_999_999_999_999, + 9_999_999_999_999, + ); + await expect( + host.harness.callRpc("getThreadHandoffSummary", { bbThreadId: "thr_1" }), + ).rejects.toThrow("64,000-code-point handoff limit"); + expect(() => + host.harness.registrations.mentionProviders[0]!.resolve("thr_1"), + ).toThrow("64,000-code-point handoff limit"); + }); + + it("exposes bounded read-only CLI discovery for agents", async () => { + const host = await loadPlugin(); + const created = await createComment(host, "Read me"); + await createComment(host, "Read me next", { messageId: "msg_2" }); + const listed = await host.harness.runCli( + ["list", "--limit", "1", "--json"], + { + threadId: "thr_1", + }, + ); + expect(listed.exitCode).toBe(0); + const firstListPage = JSON.parse(listed.stdout ?? ""); + expect(firstListPage.threads).toHaveLength(1); + expect(firstListPage.nextCursor).toEqual(expect.any(String)); + const nextListed = await host.harness.runCli( + ["list", "--limit", "1", "--cursor", firstListPage.nextCursor, "--json"], + { threadId: "thr_1" }, + ); + expect(JSON.parse(nextListed.stdout ?? "").threads).toHaveLength(1); + + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: created.thread.id, + body: "Reply", + }); + const fetched = await host.harness.runCli( + ["get", created.thread.id, "--limit", "1", "--json"], + { threadId: "thr_1" }, + ); + expect(fetched.exitCode).toBe(0); + const firstCommentPage = JSON.parse(fetched.stdout ?? ""); + expect(firstCommentPage.comments).toHaveLength(1); + expect(firstCommentPage.nextCursor).toEqual(expect.any(String)); + const nextFetched = await host.harness.runCli( + [ + "get", + created.thread.id, + "--limit", + "1", + "--cursor", + firstCommentPage.nextCursor, + "--json", + ], + { threadId: "thr_1" }, + ); + const secondCommentPage = JSON.parse(nextFetched.stdout ?? ""); + expect( + [ + firstCommentPage.comments[0].body, + secondCommentPage.comments[0].body, + ].sort(), + ).toEqual(["Read me", "Reply"]); + + const humanList = await host.harness.runCli(["list", "--limit", "1"], { + threadId: "thr_1", + }); + expect(humanList.stdout).toContain( + "More results available. Continue with --cursor ", + ); + const invalidLimit = await host.harness.runCli( + ["get", created.thread.id, "--limit", "101"], + { threadId: "thr_1" }, + ); + expect(invalidLimit).toMatchObject({ exitCode: 1 }); + expect(invalidLimit.stderr).toContain("integer from 1 to 100"); + }); + + it("rejects unknown CLI commands, options, arguments, and missing option values", async () => { + const host = await loadPlugin(); + const cases: Array<{ argv: string[]; message: string }> = [ + { argv: [], message: "A command is required" }, + { argv: ["ls"], message: "Unknown comments command" }, + { argv: ["list", "extra"], message: "Unexpected argument for list" }, + { argv: ["list", "--wat"], message: "Unknown option for list" }, + { argv: ["get", "--json"], message: "comment-thread-id" }, + { + argv: ["get", "comment_1", "--state", "open"], + message: "Unknown option for get", + }, + { argv: ["list", "--thread"], message: "--thread requires a value" }, + { + argv: ["list", "--thread", "--json"], + message: "--thread requires a value", + }, + { argv: ["list", "--cursor"], message: "--cursor requires a value" }, + { + argv: ["list", "--cursor", "--json"], + message: "--cursor requires a value", + }, + { argv: ["list", "--limit"], message: "--limit requires a value" }, + { + argv: ["list", "--limit", "--json"], + message: "--limit requires a value", + }, + { argv: ["list", "--state"], message: "--state requires a value" }, + { + argv: ["list", "--state", "--json"], + message: "--state requires a value", + }, + ]; + + for (const { argv, message } of cases) { + const result = await host.harness.runCli(argv, { threadId: "thr_1" }); + expect(result.exitCode, argv.join(" ")).toBe(1); + expect(result.stderr, argv.join(" ")).toContain(message); + } + }); + + it("accepts an escaped generated comment-thread ID that begins with dashes", async () => { + const host = await loadPlugin(); + const created = await createComment(host, "Escaped ID"); + const escapedId = "--generated-comment-thread"; + const db = host.bb.storage.database(); + db.transaction(() => { + db.prepare( + `INSERT INTO comment_threads ( + id, bb_thread_id, message_id, message_role, + selector_version, selector_start, selector_end, selector_exact, + selector_prefix, selector_suffix, version, created_at, updated_at, + resolved_at + ) + SELECT + ?, bb_thread_id, message_id, message_role, + selector_version, selector_start, selector_end, selector_exact, + selector_prefix, selector_suffix, version, created_at, updated_at, + resolved_at + FROM comment_threads + WHERE id = ?`, + ).run(escapedId, created.thread.id); + db.prepare("UPDATE comments SET thread_id = ? WHERE thread_id = ?").run( + escapedId, + created.thread.id, + ); + db.prepare("DELETE FROM comment_threads WHERE id = ?").run( + created.thread.id, + ); + })(); + + const fetched = await host.harness.runCli( + ["get", "--", escapedId, "--json"], + { threadId: "thr_1" }, + ); + expect(fetched.exitCode).toBe(0); + expect(JSON.parse(fetched.stdout ?? "")).toMatchObject({ + thread: { id: escapedId }, + comments: [{ body: "Escaped ID" }], + }); + + const unescapedFlag = await host.harness.runCli(["get", "--json"], { + threadId: "thr_1", + }); + expect(unescapedFlag.exitCode).toBe(1); + expect(unescapedFlag.stderr).toContain("comment-thread-id"); + }); + + it("keeps Unicode-heavy CLI pages below the host limit and fails clearly for one oversized record", async () => { + const host = await loadPlugin(); + const body = "😀".repeat(10_000); + for (let index = 0; index < 30; index += 1) { + await createComment(host, body, { messageId: `msg_${index}` }); + } + + const listed = await host.harness.runCli(["list", "--json"], { + threadId: "thr_1", + }); + expect(listed.exitCode).toBe(0); + expect(Buffer.byteLength(listed.stdout ?? "", "utf8")).toBeLessThan( + 1_048_576, + ); + const page = JSON.parse(listed.stdout ?? ""); + expect(page.threads.length).toBeGreaterThan(0); + expect(page.threads.length).toBeLessThan(30); + expect(page.nextCursor).toEqual(expect.any(String)); + + const oversized = page.threads[0]; + for (let index = 0; index < 24; index += 1) { + await host.harness.callRpc("reply", { + bbThreadId: "thr_1", + commentThreadId: oversized.id, + body, + }); + } + const fetched = await host.harness.runCli( + ["get", oversized.id, "--json"], + { threadId: "thr_1" }, + ); + expect(fetched.exitCode).toBe(0); + expect(Buffer.byteLength(fetched.stdout ?? "", "utf8")).toBeLessThan( + 1_048_576, + ); + const commentPage = JSON.parse(fetched.stdout ?? ""); + expect(commentPage.comments.length).toBeGreaterThan(0); + expect(commentPage.comments.length).toBeLessThan(25); + expect(commentPage.nextCursor).toEqual(expect.any(String)); + + const exact = "😀".repeat(240_000); + host.bb.storage + .database() + .prepare( + `UPDATE comment_threads + SET selector_start = 0, selector_end = ?, selector_exact = ? + WHERE id = ?`, + ) + .run(exact.length, exact, oversized.id); + const oneRecord = await host.harness.runCli( + ["list", "--limit", "1", "--json"], + { threadId: "thr_1" }, + ); + expect(oneRecord.exitCode).toBe(1); + expect(oneRecord.stderr).toContain( + "A single comment thread exceeds the CLI output size limit", + ); + }); +}); diff --git a/plugins/timeline-comments/server.ts b/plugins/timeline-comments/server.ts new file mode 100644 index 0000000..66504a3 --- /dev/null +++ b/plugins/timeline-comments/server.ts @@ -0,0 +1,1228 @@ +import { randomBytes } from "node:crypto"; +import { Buffer } from "node:buffer"; +import { + defineRpcContract, + PLUGIN_CLI_OUTPUT_MAX_BYTES, + type BbPluginApi, +} from "@bb/plugin-sdk"; +import { z } from "zod"; +import { COMMENT_BODY_CODE_POINT_LIMIT } from "./comment-body.js"; + +const ROOT_PAGE_SIZE = 50; +const COMMENT_PAGE_SIZE = 100; +const ANCHOR_PAGE_SIZE = 200; +const HANDOFF_CODE_POINT_LIMIT = 64_000; +const CLI_OUTPUT_BUDGET_BYTES = Math.min( + 900_000, + PLUGIN_CLI_OUTPUT_MAX_BYTES - 64 * 1_024, +); + +const idSchema = z.string().min(1).max(256); +const bodySchema = z + .string() + .transform((body) => body.trim()) + .refine((body) => body.length > 0, "Comment body is required") + .refine( + (body) => Array.from(body).length <= COMMENT_BODY_CODE_POINT_LIMIT, + `Comment body must be at most ${COMMENT_BODY_CODE_POINT_LIMIT.toLocaleString("en-US")} Unicode code points`, + ); +export const commentBodySchema = bodySchema; +const utf16ContextSchema = z + .string() + .refine((value) => value.length <= 32, "Selector context is too long"); +const rectSchema = z + .object({ + x: z.number().finite(), + y: z.number().finite(), + width: z.number().finite().nonnegative(), + height: z.number().finite().nonnegative(), + }) + .strict(); +const selectorFields = { + version: z.literal(1), + coordinateSpace: z.literal("rendered-text-utf16"), + start: z.number().int().nonnegative(), + end: z.number().int().positive(), + exact: z.string().refine((value) => value.trim().length > 0), + prefix: utf16ContextSchema, + suffix: utf16ContextSchema, +}; + +function refineSelectorSpan( + selector: { start: number; end: number; exact: string }, + context: z.core.$RefinementCtx, +) { + if (selector.end <= selector.start) { + context.addIssue({ + code: "custom", + message: "Selector end must be greater than start", + path: ["end"], + input: selector, + }); + } + if (selector.end - selector.start !== selector.exact.length) { + context.addIssue({ + code: "custom", + message: "Selector offsets must span exact text in UTF-16 units", + path: ["exact"], + input: selector, + }); + } +} + +export const renderedTextSelectorSchema = z + .object({ + ...selectorFields, + rects: z.array(rectSchema).min(1).max(1_000), + }) + .strict() + .superRefine(refineSelectorSpan); + +const storedSelectorSchema = z + .object(selectorFields) + .strict() + .superRefine(refineSelectorSpan); +const messageReferenceSchema = z + .object({ + id: idSchema, + threadId: idSchema, + role: z.enum(["user", "assistant"]), + text: z.string(), + sourceSeqEnd: z.number().int().nonnegative(), + }) + .strict(); +const commentSchema = z + .object({ + id: idSchema, + threadId: idSchema, + parentId: idSchema.nullable(), + body: z.string(), + version: z.number().int().positive(), + createdAt: z.number().int().nonnegative(), + updatedAt: z.number().int().nonnegative(), + }) + .strict(); +export const commentThreadSummarySchema = z + .object({ + id: idSchema, + bbThreadId: idSchema, + messageId: idSchema, + messageRole: z.enum(["user", "assistant"]), + selector: storedSelectorSchema, + version: z.number().int().positive(), + createdAt: z.number().int().nonnegative(), + updatedAt: z.number().int().nonnegative(), + resolvedAt: z.number().int().nonnegative().nullable(), + rootComment: commentSchema, + replyCount: z.number().int().nonnegative(), + }) + .strict(); +const anchorSchema = commentThreadSummarySchema.omit({ rootComment: true }); +export const commentThreadDetailSchema = z + .object({ + thread: commentThreadSummarySchema, + comments: z.array(commentSchema), + nextCursor: z.string().nullable(), + }) + .strict(); +export type TimelineComment = z.infer; +export type TimelineCommentThreadSummary = z.infer< + typeof commentThreadSummarySchema +>; +export type TimelineCommentThreadDetail = z.infer< + typeof commentThreadDetailSchema +>; +const cursorInputSchema = z.string().min(1).max(2_048).optional(); +const rootFilterSchema = z.enum(["open", "resolved", "all"]); + +export const timelineCommentsRpcContract = defineRpcContract({ + listOpenAnchors: { + input: z + .object({ + threadIds: z.array(idSchema).min(1).max(20), + cursor: cursorInputSchema, + }) + .strict(), + output: z + .object({ + anchors: z.array(anchorSchema), + nextCursor: z.string().nullable(), + }) + .strict(), + }, + listCommentThreads: { + input: z + .object({ + bbThreadId: idSchema, + filter: rootFilterSchema, + cursor: cursorInputSchema, + }) + .strict(), + output: z + .object({ + threads: z.array(commentThreadSummarySchema), + nextCursor: z.string().nullable(), + }) + .strict(), + }, + getCommentThread: { + input: z + .object({ + bbThreadId: idSchema, + commentThreadId: idSchema, + cursor: cursorInputSchema, + }) + .strict(), + output: commentThreadDetailSchema, + }, + getThreadHandoffSummary: { + input: z.object({ bbThreadId: idSchema }).strict(), + output: z + .object({ + threadCount: z.number().int().nonnegative(), + commentCount: z.number().int().nonnegative(), + codePointSize: z.number().int().nonnegative(), + }) + .strict(), + }, + createThread: { + input: z + .object({ + bbThreadId: idSchema, + message: messageReferenceSchema, + selector: renderedTextSelectorSchema, + body: bodySchema, + }) + .strict(), + output: commentThreadDetailSchema, + }, + reply: { + input: z + .object({ + bbThreadId: idSchema, + commentThreadId: idSchema, + body: bodySchema, + }) + .strict(), + output: commentThreadDetailSchema, + }, + updateComment: { + input: z + .object({ + bbThreadId: idSchema, + commentId: idSchema, + expectedVersion: z.number().int().positive(), + body: bodySchema, + }) + .strict(), + output: commentThreadDetailSchema, + }, + deleteComment: { + input: z + .object({ + bbThreadId: idSchema, + commentId: idSchema, + expectedVersion: z.number().int().positive(), + expectedThreadVersion: z.number().int().positive(), + }) + .strict(), + output: z + .object({ + deletedThreadId: idSchema.nullable(), + thread: commentThreadDetailSchema.nullable(), + }) + .strict(), + }, + setThreadResolved: { + input: z + .object({ + bbThreadId: idSchema, + commentThreadId: idSchema, + expectedVersion: z.number().int().positive(), + resolved: z.boolean(), + }) + .strict(), + output: commentThreadDetailSchema, + }, +}); + +type Database = ReturnType; + +const threadRowSchema = z.object({ + id: z.string(), + bbThreadId: z.string(), + messageId: z.string(), + messageRole: z.enum(["user", "assistant"]), + selectorVersion: z.number(), + selectorStart: z.number(), + selectorEnd: z.number(), + selectorExact: z.string(), + selectorPrefix: z.string(), + selectorSuffix: z.string(), + version: z.number(), + createdAt: z.number(), + updatedAt: z.number(), + resolvedAt: z.number().nullable(), + rootId: z.string(), + rootBody: z.string(), + rootVersion: z.number(), + rootCreatedAt: z.number(), + rootUpdatedAt: z.number(), + replyCount: z.number(), +}); +type ThreadRow = z.infer; +const commentRowSchema = z.object({ + id: z.string(), + threadId: z.string(), + parentId: z.string().nullable(), + body: z.string(), + version: z.number(), + createdAt: z.number(), + updatedAt: z.number(), +}); +type CommentRow = z.infer; + +const cursorSchema = z + .object({ + method: z.enum(["anchors", "threads", "comments"]), + scope: z.string(), + filter: z.string(), + parentRank: z.number().int().min(0).max(1).optional(), + createdAt: z.number().int().nonnegative(), + id: z.string(), + }) + .strict(); +type Cursor = z.infer; + +function encodeCursor(cursor: Cursor): string { + return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url"); +} + +function decodeCursor( + encoded: string | undefined, + expected: Pick, +): Cursor | null { + if (encoded === undefined) return null; + let parsed: unknown; + try { + parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")); + } catch { + throw new Error("Invalid pagination cursor"); + } + const cursor = cursorSchema.parse(parsed); + if ( + cursor.method !== expected.method || + cursor.scope !== expected.scope || + cursor.filter !== expected.filter + ) { + throw new Error("Pagination cursor does not match this query"); + } + return cursor; +} + +function randomId(): string { + return randomBytes(18).toString("base64url"); +} + +function mapComment(row: CommentRow): z.infer { + return commentSchema.parse(row); +} + +function mapThread(row: ThreadRow): z.infer { + return commentThreadSummarySchema.parse({ + id: row.id, + bbThreadId: row.bbThreadId, + messageId: row.messageId, + messageRole: row.messageRole, + selector: { + version: 1, + coordinateSpace: "rendered-text-utf16", + start: row.selectorStart, + end: row.selectorEnd, + exact: row.selectorExact, + prefix: row.selectorPrefix, + suffix: row.selectorSuffix, + }, + version: row.version, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + resolvedAt: row.resolvedAt, + rootComment: { + id: row.rootId, + threadId: row.id, + parentId: null, + body: row.rootBody, + version: row.rootVersion, + createdAt: row.rootCreatedAt, + updatedAt: row.rootUpdatedAt, + }, + replyCount: row.replyCount, + }); +} + +const THREAD_SELECT = ` + SELECT + t.id, + t.bb_thread_id AS bbThreadId, + t.message_id AS messageId, + t.message_role AS messageRole, + t.selector_version AS selectorVersion, + t.selector_start AS selectorStart, + t.selector_end AS selectorEnd, + t.selector_exact AS selectorExact, + t.selector_prefix AS selectorPrefix, + t.selector_suffix AS selectorSuffix, + t.version, + t.created_at AS createdAt, + t.updated_at AS updatedAt, + t.resolved_at AS resolvedAt, + root.id AS rootId, + root.body AS rootBody, + root.version AS rootVersion, + root.created_at AS rootCreatedAt, + root.updated_at AS rootUpdatedAt, + (SELECT COUNT(*) FROM comments replies + WHERE replies.thread_id = t.id AND replies.parent_id IS NOT NULL) AS replyCount + FROM comment_threads t + JOIN comments root ON root.thread_id = t.id AND root.parent_id IS NULL +`; + +function getThreadRow( + db: Database, + bbThreadId: string, + commentThreadId: string, +): ThreadRow { + const row = db + .prepare(`${THREAD_SELECT} WHERE t.id = ? AND t.bb_thread_id = ?`) + .get(commentThreadId, bbThreadId); + if (row === undefined) throw new Error("Comment thread not found"); + return threadRowSchema.parse(row); +} + +function getCommentRow(db: Database, commentId: string): CommentRow { + const row = db + .prepare( + `SELECT id, thread_id AS threadId, parent_id AS parentId, body, version, + created_at AS createdAt, updated_at AS updatedAt + FROM comments WHERE id = ?`, + ) + .get(commentId); + if (row === undefined) throw new Error("Comment not found"); + return commentRowSchema.parse(row); +} + +function getCommentThread( + db: Database, + input: { + bbThreadId: string; + commentThreadId: string; + cursor?: string; + limit?: number; + }, +): z.infer { + const pageSize = input.limit ?? COMMENT_PAGE_SIZE; + const thread = mapThread( + getThreadRow(db, input.bbThreadId, input.commentThreadId), + ); + const cursor = decodeCursor(input.cursor, { + method: "comments", + scope: `${input.bbThreadId}:${input.commentThreadId}`, + filter: "chronological", + }); + const cursorParentRank = cursor?.parentRank ?? 1; + const rows = db + .prepare( + `SELECT id, thread_id AS threadId, parent_id AS parentId, body, version, + created_at AS createdAt, updated_at AS updatedAt + FROM comments + WHERE thread_id = ? + AND ( + ? IS NULL + OR (CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END) > ? + OR ( + (CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END) = ? + AND (created_at > ? OR (created_at = ? AND id > ?)) + ) + ) + ORDER BY + CASE WHEN parent_id IS NULL THEN 0 ELSE 1 END ASC, + created_at ASC, + id ASC + LIMIT ?`, + ) + .all( + input.commentThreadId, + cursor?.id ?? null, + cursorParentRank, + cursorParentRank, + cursor?.createdAt ?? 0, + cursor?.createdAt ?? 0, + cursor?.id ?? "", + pageSize + 1, + ) + .map((row) => commentRowSchema.parse(row)); + const hasNext = rows.length > pageSize; + const page = rows.slice(0, pageSize); + const last = page.at(-1); + return commentThreadDetailSchema.parse({ + thread, + comments: page.map(mapComment), + nextCursor: + hasNext && last !== undefined + ? encodeCursor({ + method: "comments", + scope: `${input.bbThreadId}:${input.commentThreadId}`, + filter: "chronological", + parentRank: last.parentId === null ? 0 : 1, + createdAt: last.createdAt, + id: last.id, + }) + : null, + }); +} + +function listCommentThreads( + db: Database, + input: { + bbThreadId: string; + filter: "open" | "resolved" | "all"; + cursor?: string; + limit?: number; + }, +): { + threads: z.infer[]; + nextCursor: string | null; +} { + const pageSize = input.limit ?? ROOT_PAGE_SIZE; + const cursor = decodeCursor(input.cursor, { + method: "threads", + scope: input.bbThreadId, + filter: input.filter, + }); + const stateSql = + input.filter === "open" + ? "AND t.resolved_at IS NULL" + : input.filter === "resolved" + ? "AND t.resolved_at IS NOT NULL" + : ""; + const rows = db + .prepare( + `${THREAD_SELECT} + WHERE t.bb_thread_id = ? ${stateSql} + AND (? IS NULL OR t.created_at > ? OR (t.created_at = ? AND t.id > ?)) + ORDER BY t.created_at ASC, t.id ASC + LIMIT ?`, + ) + .all( + input.bbThreadId, + cursor?.id ?? null, + cursor?.createdAt ?? 0, + cursor?.createdAt ?? 0, + cursor?.id ?? "", + pageSize + 1, + ) + .map((row) => threadRowSchema.parse(row)); + const hasNext = rows.length > pageSize; + const page = rows.slice(0, pageSize); + const last = page.at(-1); + return { + threads: page.map(mapThread), + nextCursor: + hasNext && last !== undefined + ? encodeCursor({ + method: "threads", + scope: input.bbThreadId, + filter: input.filter, + createdAt: last.createdAt, + id: last.id, + }) + : null, + }; +} + +function listOpenAnchors( + db: Database, + input: { threadIds: string[]; cursor?: string }, +): { anchors: z.infer[]; nextCursor: string | null } { + const threadIds = [...new Set(input.threadIds)].sort(); + const scope = threadIds.join("\u001f"); + const cursor = decodeCursor(input.cursor, { + method: "anchors", + scope, + filter: "open", + }); + const placeholders = threadIds.map(() => "?").join(", "); + const rows = db + .prepare( + `${THREAD_SELECT} + WHERE t.bb_thread_id IN (${placeholders}) AND t.resolved_at IS NULL + AND (? IS NULL OR t.created_at > ? OR (t.created_at = ? AND t.id > ?)) + ORDER BY t.created_at ASC, t.id ASC + LIMIT ?`, + ) + .all( + ...threadIds, + cursor?.id ?? null, + cursor?.createdAt ?? 0, + cursor?.createdAt ?? 0, + cursor?.id ?? "", + ANCHOR_PAGE_SIZE + 1, + ) + .map((row) => threadRowSchema.parse(row)); + const hasNext = rows.length > ANCHOR_PAGE_SIZE; + const page = rows.slice(0, ANCHOR_PAGE_SIZE); + const last = page.at(-1); + return { + anchors: page.map((row) => { + const { rootComment: _rootComment, ...anchor } = mapThread(row); + return anchorSchema.parse(anchor); + }), + nextCursor: + hasNext && last !== undefined + ? encodeCursor({ + method: "anchors", + scope, + filter: "open", + createdAt: last.createdAt, + id: last.id, + }) + : null, + }; +} + +interface SerializedHandoff { + context: string; + threadCount: number; + commentCount: number; + codePointSize: number; +} + +const handoffRowSchema = z.object({ + commentThreadId: z.string(), + selectorExact: z.string(), + commentId: z.string(), + parentId: z.string().nullable(), + body: z.string(), +}); + +function handoffLimitError(): Error { + return new Error( + `Open comments exceed the ${HANDOFF_CODE_POINT_LIMIT.toLocaleString("en-US")}-code-point handoff limit`, + ); +} + +function readOpenComments( + db: Database, + bbThreadId: string, + includeContext: boolean, +): SerializedHandoff { + const rows = db + .prepare( + `SELECT + t.id AS commentThreadId, + t.selector_exact AS selectorExact, + c.id AS commentId, + c.parent_id AS parentId, + c.body + FROM comment_threads t + JOIN comments c ON c.thread_id = t.id + WHERE t.bb_thread_id = ? AND t.resolved_at IS NULL + ORDER BY + t.created_at ASC, + t.id ASC, + CASE WHEN c.parent_id IS NULL THEN 0 ELSE 1 END ASC, + c.created_at ASC, + c.id ASC`, + ) + .iterate(bbThreadId); + const sections: string[] = []; + let sectionCount = 0; + let codePointSize = 0; + let threadCount = 0; + let commentCount = 0; + let currentThreadId: string | null = null; + + const addSection = (section: string) => { + if (sectionCount > 0) codePointSize += 2; + codePointSize += Array.from(section).length; + if (codePointSize > HANDOFF_CODE_POINT_LIMIT) throw handoffLimitError(); + sectionCount += 1; + if (includeContext) sections.push(section); + }; + + for (const rawRow of rows) { + const row = handoffRowSchema.parse(rawRow); + if (currentThreadId === null) { + addSection(`# Open timeline comments for BB thread ${bbThreadId}`); + } + if (row.commentThreadId !== currentThreadId) { + currentThreadId = row.commentThreadId; + threadCount += 1; + addSection(`## Source: “${row.selectorExact}”`); + } + commentCount += 1; + addSection(`${row.parentId === null ? "Comment" : "Reply"}: ${row.body}`); + } + + if (currentThreadId === null) { + const context = `No open comments remain for BB thread ${bbThreadId}.`; + return { + context: includeContext ? context : "", + threadCount: 0, + commentCount: 0, + codePointSize: Array.from(context).length, + }; + } + return { + context: includeContext ? sections.join("\n\n") : "", + threadCount, + commentCount, + codePointSize, + }; +} + +function serializeOpenComments( + db: Database, + bbThreadId: string, +): SerializedHandoff { + return readOpenComments(db, bbThreadId, true); +} + +function summarizeOpenComments( + db: Database, + bbThreadId: string, +): Omit { + const { context: _context, ...summary } = readOpenComments( + db, + bbThreadId, + false, + ); + return summary; +} + +function countOpenComments( + db: Database, + bbThreadId: string, +): { threadCount: number; commentCount: number } { + const row = db + .prepare( + `SELECT + COUNT(DISTINCT t.id) AS threadCount, + COUNT(c.id) AS commentCount + FROM comment_threads t + LEFT JOIN comments c ON c.thread_id = t.id + WHERE t.bb_thread_id = ? AND t.resolved_at IS NULL`, + ) + .get(bbThreadId); + return z + .object({ + threadCount: z.number().int().nonnegative(), + commentCount: z.number().int().nonnegative(), + }) + .parse(row); +} + +function parseCli(argv: string[]): { + command: "list" | "get"; + threadId: string | undefined; + commentThreadId: string | undefined; + json: boolean; + filter: "open" | "resolved" | "all"; + cursor: string | undefined; + limit: number; +} { + const rawCommand = argv[0]; + if (rawCommand !== "list" && rawCommand !== "get") { + throw new Error( + rawCommand === undefined + ? "A command is required: list or get" + : `Unknown comments command: ${rawCommand}`, + ); + } + const command = rawCommand; + let index = 1; + let commentThreadId: string | undefined; + if (command === "get") { + if (argv[index] === "--") { + const candidate = argv[index + 1]; + if (candidate === undefined) { + throw new Error("get requires a comment-thread-id"); + } + commentThreadId = candidate; + index += 2; + } else { + const candidate = argv[index]; + if (candidate === undefined || candidate.startsWith("--")) { + throw new Error("get requires a comment-thread-id"); + } + commentThreadId = candidate; + index += 1; + } + } + let threadId: string | undefined; + let filter: "open" | "resolved" | "all" = "open"; + let json = false; + let cursor: string | undefined; + const maxLimit = command === "get" ? COMMENT_PAGE_SIZE : ROOT_PAGE_SIZE; + let limit = maxLimit; + const takeValue = (flag: string): string => { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + throw new Error(`${flag} requires a value`); + } + index += 2; + return value; + }; + while (index < argv.length) { + const arg = argv[index]!; + if (arg === "--json") { + json = true; + index += 1; + continue; + } + if (arg === "--thread") { + threadId = takeValue(arg); + continue; + } + if (arg === "--cursor") { + cursor = takeValue(arg); + continue; + } + if (arg === "--limit") { + const rawLimit = takeValue(arg); + limit = Number(rawLimit); + if (!Number.isInteger(limit) || limit < 1 || limit > maxLimit) { + throw new Error(`--limit must be an integer from 1 to ${maxLimit}`); + } + continue; + } + if (arg === "--state" && command === "list") { + const state = takeValue(arg); + if (state !== "open" && state !== "resolved" && state !== "all") { + throw new Error("--state must be open, resolved, or all"); + } + filter = state; + continue; + } + throw new Error( + arg.startsWith("--") + ? `Unknown option for ${command}: ${arg}` + : `Unexpected argument for ${command}: ${arg}`, + ); + } + return { + command, + threadId, + commentThreadId, + json, + filter, + cursor, + limit, + }; +} + +function utf8Size(value: string): number { + return Buffer.byteLength(value, "utf8"); +} + +function renderCliPage(input: { + records: T[]; + inheritedNextCursor: string | null; + cursorFor: (record: T) => string; + renderHuman: (record: T, index: number) => string; + renderJson: (records: T[], nextCursor: string | null) => string; + json: boolean; + oversizedLabel: string; +}): string { + const render = (records: T[], nextCursor: string | null): string => { + if (input.json) return `${input.renderJson(records, nextCursor)}\n`; + const rows = records.map(input.renderHuman).join("\n"); + const continuation = + nextCursor === null + ? "" + : `${rows.length > 0 ? "\n" : ""}More results available. Continue with --cursor ${nextCursor}`; + return `${rows}${continuation}${rows.length > 0 || continuation.length > 0 ? "\n" : ""}`; + }; + + const complete = render(input.records, input.inheritedNextCursor); + if (utf8Size(complete) <= CLI_OUTPUT_BUDGET_BYTES) return complete; + + for (let length = input.records.length - 1; length >= 1; length -= 1) { + const records = input.records.slice(0, length); + const nextCursor = input.cursorFor(records.at(-1)!); + const candidate = render(records, nextCursor); + if (utf8Size(candidate) <= CLI_OUTPUT_BUDGET_BYTES) return candidate; + } + + throw new Error( + `A single ${input.oversizedLabel} exceeds the CLI output size limit; request a smaller record`, + ); +} + +export default function timelineCommentsPlugin(bb: BbPluginApi): void { + const db = bb.storage.database(); + db.pragma("foreign_keys = ON"); + if (db.pragma("foreign_keys", { simple: true }) !== 1) { + throw new Error("Timeline comments requires SQLite foreign keys"); + } + bb.storage.migrate(db, [ + `CREATE TABLE comment_threads ( + id TEXT PRIMARY KEY, + bb_thread_id TEXT NOT NULL, + message_id TEXT NOT NULL, + message_role TEXT NOT NULL CHECK (message_role IN ('user', 'assistant')), + selector_version INTEGER NOT NULL CHECK (selector_version = 1), + selector_start INTEGER NOT NULL, + selector_end INTEGER NOT NULL, + selector_exact TEXT NOT NULL, + selector_prefix TEXT NOT NULL, + selector_suffix TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + resolved_at INTEGER + ); + CREATE TABLE comments ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE, + parent_id TEXT REFERENCES comments(id) ON DELETE CASCADE, + body TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE INDEX comment_threads_by_bb_thread + ON comment_threads(bb_thread_id, resolved_at, created_at, id); + CREATE INDEX comments_by_thread + ON comments(thread_id, created_at, id);`, + ]); + + const publishChanged = (bbThreadId: string, commentThreadId: string) => { + bb.realtime.publish("comments-changed", { + bbThreadId, + commentThreadId, + }); + }; + + bb.events.on("thread.deleted", ({ thread }) => { + db.prepare("DELETE FROM comment_threads WHERE bb_thread_id = ?").run( + thread.id, + ); + }); + + bb.rpc.register(timelineCommentsRpcContract, { + listOpenAnchors(input) { + return listOpenAnchors(db, input); + }, + listCommentThreads(input) { + return listCommentThreads(db, input); + }, + getCommentThread(input) { + return getCommentThread(db, input); + }, + getThreadHandoffSummary({ bbThreadId }) { + const summary = summarizeOpenComments(db, bbThreadId); + return { + threadCount: summary.threadCount, + commentCount: summary.commentCount, + codePointSize: summary.codePointSize, + }; + }, + createThread(input) { + if (input.message.threadId !== input.bbThreadId) { + throw new Error("Message does not belong to the requested BB thread"); + } + const now = Date.now(); + const commentThreadId = randomId(); + const rootCommentId = randomId(); + db.transaction(() => { + db.prepare( + `INSERT INTO comment_threads ( + id, bb_thread_id, message_id, message_role, + selector_version, selector_start, selector_end, selector_exact, + selector_prefix, selector_suffix, version, created_at, updated_at + ) VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, 1, ?, ?)`, + ).run( + commentThreadId, + input.bbThreadId, + input.message.id, + input.message.role, + input.selector.start, + input.selector.end, + input.selector.exact, + input.selector.prefix, + input.selector.suffix, + now, + now, + ); + db.prepare( + `INSERT INTO comments ( + id, thread_id, parent_id, body, version, created_at, updated_at + ) VALUES (?, ?, NULL, ?, 1, ?, ?)`, + ).run(rootCommentId, commentThreadId, input.body, now, now); + })(); + const result = getCommentThread(db, { + bbThreadId: input.bbThreadId, + commentThreadId, + }); + publishChanged(input.bbThreadId, commentThreadId); + return result; + }, + reply(input) { + const current = getThreadRow(db, input.bbThreadId, input.commentThreadId); + if (current.resolvedAt !== null) { + throw new Error("Resolved comment threads cannot receive replies"); + } + const now = Date.now(); + const id = randomId(); + db.transaction(() => { + db.prepare( + `INSERT INTO comments ( + id, thread_id, parent_id, body, version, created_at, updated_at + ) VALUES (?, ?, ?, ?, 1, ?, ?)`, + ).run(id, current.id, current.rootId, input.body, now, now); + db.prepare( + `UPDATE comment_threads + SET version = version + 1, updated_at = ? WHERE id = ?`, + ).run(now, current.id); + })(); + const result = getCommentThread(db, input); + publishChanged(input.bbThreadId, input.commentThreadId); + return result; + }, + updateComment(input) { + const comment = getCommentRow(db, input.commentId); + const current = getThreadRow(db, input.bbThreadId, comment.threadId); + const now = Date.now(); + const updated = db.transaction(() => { + const change = db + .prepare( + `UPDATE comments + SET body = ?, version = version + 1, updated_at = ? + WHERE id = ? AND version = ?`, + ) + .run(input.body, now, input.commentId, input.expectedVersion); + if (change.changes !== 1) return false; + db.prepare( + `UPDATE comment_threads + SET version = version + 1, updated_at = ? WHERE id = ?`, + ).run(now, current.id); + return true; + })(); + if (!updated) throw new Error("Comment changed; refresh and retry"); + const result = getCommentThread(db, { + bbThreadId: input.bbThreadId, + commentThreadId: current.id, + }); + publishChanged(input.bbThreadId, current.id); + return result; + }, + deleteComment(input) { + const comment = getCommentRow(db, input.commentId); + const current = getThreadRow(db, input.bbThreadId, comment.threadId); + if (comment.parentId === null) { + const change = db.transaction(() => + db + .prepare( + `DELETE FROM comment_threads + WHERE id = ? AND bb_thread_id = ? AND version = ? + AND EXISTS ( + SELECT 1 FROM comments + WHERE comments.id = ? AND comments.thread_id = comment_threads.id + AND comments.parent_id IS NULL AND comments.version = ? + )`, + ) + .run( + current.id, + input.bbThreadId, + input.expectedThreadVersion, + comment.id, + input.expectedVersion, + ), + )(); + if (change.changes !== 1) { + throw new Error("Comment thread changed; refresh and retry"); + } + publishChanged(input.bbThreadId, current.id); + return { deletedThreadId: current.id, thread: null }; + } + const now = Date.now(); + db.transaction(() => { + const threadChange = db + .prepare( + `UPDATE comment_threads + SET version = version + 1, updated_at = ? + WHERE id = ? AND bb_thread_id = ? AND version = ?`, + ) + .run( + now, + current.id, + input.bbThreadId, + input.expectedThreadVersion, + ); + if (threadChange.changes !== 1) { + throw new Error("Comment thread changed; refresh and retry"); + } + const commentChange = db + .prepare("DELETE FROM comments WHERE id = ? AND version = ?") + .run(comment.id, input.expectedVersion); + if (commentChange.changes !== 1) { + throw new Error("Comment changed; refresh and retry"); + } + })(); + const result = getCommentThread(db, { + bbThreadId: input.bbThreadId, + commentThreadId: current.id, + }); + publishChanged(input.bbThreadId, current.id); + return { deletedThreadId: null, thread: result }; + }, + setThreadResolved(input) { + const now = Date.now(); + const change = db.transaction(() => + db + .prepare( + `UPDATE comment_threads + SET resolved_at = ?, version = version + 1, updated_at = ? + WHERE id = ? AND bb_thread_id = ? AND version = ?`, + ) + .run( + input.resolved ? now : null, + now, + input.commentThreadId, + input.bbThreadId, + input.expectedVersion, + ), + )(); + if (change.changes !== 1) { + throw new Error("Comment thread changed; refresh and retry"); + } + const result = getCommentThread(db, input); + publishChanged(input.bbThreadId, input.commentThreadId); + return result; + }, + }); + + bb.ui.registerMentionProvider({ + id: "thread-comments", + label: "Thread comments", + triggers: ["@"], + search({ query, threadId }) { + if (threadId === null || !"comments".includes(query.toLowerCase())) { + return []; + } + const counts = countOpenComments(db, threadId); + return [ + { + id: threadId, + title: `${counts.commentCount} comments from ${counts.threadCount} open threads`, + subtitle: "Local timeline comments", + }, + ]; + }, + resolve(bbThreadId) { + const serialized = serializeOpenComments(db, bbThreadId); + return { context: serialized.context }; + }, + }); + + bb.cli.register({ + name: "comments", + summary: "Read timeline comments attached to BB threads", + commands: [ + { + name: "list", + summary: "List comment threads", + usage: + "bb comments list [--thread ] [--state open|resolved|all] [--cursor ] [--limit 1-50] [--json]", + }, + { + name: "get", + summary: "Read one comment thread", + usage: + "bb comments get [--thread ] [--cursor ] [--limit 1-100] [--json]", + }, + ], + run(argv, context) { + try { + const parsed = parseCli(argv); + const bbThreadId = parsed.threadId ?? context.threadId; + if (bbThreadId === undefined) { + return { + exitCode: 2, + stderr: "A BB thread context or --thread is required.\n", + }; + } + if (parsed.command === "get") { + if (parsed.commentThreadId === undefined) { + return { + exitCode: 2, + stderr: + "Usage: bb comments get [--thread ] [--cursor ] [--limit 1-100] [--json]\n", + }; + } + const detail = getCommentThread(db, { + bbThreadId, + commentThreadId: parsed.commentThreadId, + cursor: parsed.cursor, + limit: parsed.limit, + }); + return { + exitCode: 0, + stdout: renderCliPage({ + records: detail.comments, + inheritedNextCursor: detail.nextCursor, + cursorFor: (comment) => + encodeCursor({ + method: "comments", + scope: `${bbThreadId}:${parsed.commentThreadId}`, + filter: "chronological", + parentRank: comment.parentId === null ? 0 : 1, + createdAt: comment.createdAt, + id: comment.id, + }), + renderHuman: (comment) => + `${comment.parentId === null ? "Comment" : "Reply"}: ${comment.body}`, + renderJson: (comments, nextCursor) => + JSON.stringify({ ...detail, comments, nextCursor }), + json: parsed.json, + oversizedLabel: "comment", + }), + }; + } + const result = listCommentThreads(db, { + bbThreadId, + filter: parsed.filter, + cursor: parsed.cursor, + limit: parsed.limit, + }); + return { + exitCode: 0, + stdout: renderCliPage({ + records: result.threads, + inheritedNextCursor: result.nextCursor, + cursorFor: (thread) => + encodeCursor({ + method: "threads", + scope: bbThreadId, + filter: parsed.filter, + createdAt: thread.createdAt, + id: thread.id, + }), + renderHuman: (thread) => + `${thread.id}\t${thread.resolvedAt === null ? "open" : "resolved"}\t${thread.selector.exact}\t${thread.rootComment.body}`, + renderJson: (threads, nextCursor) => + JSON.stringify({ threads, nextCursor }), + json: parsed.json, + oversizedLabel: "comment thread", + }), + }; + } catch (error) { + return { + exitCode: 1, + stderr: `${error instanceof Error ? error.message : String(error)}\n`, + }; + } + }, + }); +} diff --git a/plugins/timeline-comments/test/plugin-sdk-app-harness.d.ts b/plugins/timeline-comments/test/plugin-sdk-app-harness.d.ts new file mode 100644 index 0000000..f88a50c --- /dev/null +++ b/plugins/timeline-comments/test/plugin-sdk-app-harness.d.ts @@ -0,0 +1,3 @@ +// The bb 0.0.34 test runtime adds selection placement and content-script +// lifecycle capabilities without changing the public 0.4 SDK major. +export * from "@bb/plugin-sdk/testing/app"; diff --git a/plugins/timeline-comments/test/plugin-sdk-app-harness.js b/plugins/timeline-comments/test/plugin-sdk-app-harness.js new file mode 100644 index 0000000..19cf4a2 --- /dev/null +++ b/plugins/timeline-comments/test/plugin-sdk-app-harness.js @@ -0,0 +1,1013 @@ +// src/testing/app.tsx +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useSyncExternalStore +} from "react"; +import { act, render } from "@testing-library/react"; + +// src/internal/composer-view.ts +function isComposerDraftEmpty(text, attachmentCount) { + return text.trim().length === 0 && attachmentCount === 0; +} + +// src/internal/composer-customization-validation.ts +var PLUGIN_SLOT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; +var PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; +function requireSlotId(kind, value) { + if (typeof value !== "string" || !PLUGIN_SLOT_ID_PATTERN.test(value)) { + throw new Error( + `${kind}: "id" must match ${String(PLUGIN_SLOT_ID_PATTERN)}, got ${JSON.stringify(value)}` + ); + } + return value; +} +function requireMessageDirectiveId(kind, value) { + if (typeof value !== "string" || !PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN.test(value)) { + throw new Error( + `${kind}: "id" must match ${String(PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN)}, got ${JSON.stringify(value)}` + ); + } + return value; +} +function requireNonEmptyString(kind, field, value) { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`${kind}: "${field}" must be a non-empty string`); + } + return value; +} +function requireOptionalString(kind, field, value) { + if (value !== void 0 && typeof value !== "string") { + throw new Error(`${kind}: "${field}" must be a string when set`); + } + return value; +} +function requireComponent(kind, value) { + if (typeof value !== "function") { + throw new Error(`${kind}: "component" must be a React component function`); + } + return value; +} +function requireFunction(kind, field, value) { + if (typeof value !== "function") { + throw new Error(`${kind}: "${field}" must be a function`); + } + return value; +} +function requireUniqueId(kind, seen, id) { + if (seen.has(id)) { + throw new Error(`${kind}: duplicate id "${id}"`); + } + seen.add(id); +} +function parseContributionArray(kind, value, onRejected, parse) { + if (value === void 0) return void 0; + if (!Array.isArray(value)) { + onRejected(`${kind}: must be an array when set`); + return void 0; + } + const seenIds = /* @__PURE__ */ new Set(); + const parsed = []; + for (const [index, entry] of value.entries()) { + const entryKind = `${kind}[${index}]`; + try { + const parsedEntry = parse(entryKind, entry); + requireUniqueId(entryKind, seenIds, parsedEntry.id); + parsed.push(parsedEntry); + } catch (error) { + onRejected(error instanceof Error ? error.message : String(error)); + } + } + return parsed; +} +function parseRegions(kind, registration, onRejected) { + const actions = parseContributionArray(`${kind}.actions`, registration.actions, onRejected, (entryKind, value) => { + const entry = value; + return { + id: requireSlotId(entryKind, entry?.id), + component: requireComponent(entryKind, entry?.component) + }; + }); + const banners = parseContributionArray(`${kind}.banners`, registration.banners, onRejected, (entryKind, value) => { + const entry = value; + const id = requireSlotId(entryKind, entry?.id); + const chrome = entry?.chrome; + if (chrome !== void 0 && chrome !== "card" && chrome !== "bare") { + throw new Error( + `${entryKind}: "chrome" must be "card" or "bare" when set` + ); + } + return { + id, + ...chrome !== void 0 ? { chrome } : {}, + component: requireComponent(entryKind, entry?.component) + }; + }); + const plusMenu = parseContributionArray( + `${kind}.plusMenu`, + registration.plusMenu, + onRejected, + (entryKind, value) => { + const entry = value; + const id = requireSlotId(entryKind, entry?.id); + const icon = requireOptionalString(entryKind, "icon", entry?.icon); + const description = requireOptionalString( + entryKind, + "description", + entry?.description + ); + const disabled = entry?.disabled; + if (disabled !== void 0 && typeof disabled !== "boolean" && typeof disabled !== "function") { + throw new Error( + `${entryKind}: "disabled" must be a boolean or function when set` + ); + } + return { + id, + label: requireNonEmptyString(entryKind, "label", entry?.label), + ...icon !== void 0 ? { icon } : {}, + ...description !== void 0 ? { description } : {}, + ...disabled !== void 0 ? { + disabled + } : {}, + run: requireFunction(entryKind, "run", entry?.run) + }; + } + ); + let richText; + if (registration.richText !== void 0) { + const raw = registration.richText; + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { + onRejected(`${kind}.richText: must be an object when set`); + } else { + const effects = parseContributionArray( + `${kind}.richText.effects`, + raw.effects, + onRejected, + (entryKind, value) => { + const entry = value; + return { + id: requireSlotId(entryKind, entry?.id), + match: requireFunction(entryKind, "match", entry?.match), + className: requireNonEmptyString( + entryKind, + "className", + entry?.className + ) + }; + } + ); + const onDraftChange = raw.onDraftChange; + if (onDraftChange !== void 0 && typeof onDraftChange !== "function") { + onRejected( + `${kind}.richText: "onDraftChange" must be a function when set` + ); + } + richText = { + ...effects !== void 0 ? { effects } : {}, + ...typeof onDraftChange === "function" ? { + onDraftChange + } : {} + }; + } + } + return { + ...actions !== void 0 ? { actions } : {}, + ...banners !== void 0 ? { banners } : {}, + ...plusMenu !== void 0 ? { plusMenu } : {}, + ...richText !== void 0 ? { richText } : {} + }; +} +function collectComposerCustomization(registration, seenIds, onRejected) { + const kind = "composer.customize"; + try { + const raw = registration; + const id = requireSlotId(kind, raw?.id); + const scopes = raw?.scopes; + if (scopes !== void 0) { + if (!Array.isArray(scopes)) { + throw new Error(`${kind}: "scopes" must be an array when set`); + } + for (const scope of scopes) { + if (scope !== "thread" && scope !== "queued-message" && scope !== "side-chat" && scope !== "new-thread") { + throw new Error( + `${kind}: invalid scope kind ${JSON.stringify(scope)}` + ); + } + } + } + requireUniqueId(kind, seenIds, id); + return { + id, + ...scopes !== void 0 ? { scopes: [...scopes] } : {}, + ...parseRegions(`${kind}(${id})`, raw ?? {}, onRejected) + }; + } catch (error) { + onRejected(error instanceof Error ? error.message : String(error)); + return null; + } +} + +// src/testing/app.tsx +import { jsx, jsxs } from "react/jsx-runtime"; +function SlotLifecycleGuard({ + children, + onUnmount +}) { + useEffect(() => () => onUnmount(), [onUnmount]); + return children; +} +var SlotEnvContext = createContext(null); +function useSlotEnv(hook) { + const env = useContext(SlotEnvContext); + if (!env) { + throw new Error( + `${hook}() needs the test slot environment \u2014 mount the component via renderSlot(...) from @bb/plugin-sdk/testing/app` + ); + } + return env; +} +function definePluginApp(setup) { + if (typeof setup !== "function") { + throw new Error("definePluginApp expects a setup function"); + } + return Object.freeze({ __bbPluginApp: true, setup }); +} +function isPluginAppDefinition(value) { + return typeof value === "object" && value !== null && value.__bbPluginApp === true && typeof value.setup === "function"; +} +function TestThreadChat({ + threadId, + variant = "full", + layout = "contained", + focusRequest, + className, + leadingContent, + messageActions +}) { + return /* @__PURE__ */ jsxs( + "div", + { + "data-testid": "bb-thread-chat", + "data-thread-id": threadId, + "data-variant": variant, + "data-layout": layout, + "data-focus-request": focusRequest ?? 0, + "data-message-actions": (messageActions ?? []).map((action) => action.id).join(" "), + className, + children: [ + leadingContent === void 0 ? null : /* @__PURE__ */ jsx("div", { "data-testid": "bb-thread-chat-leading-content", children: leadingContent }), + "ThreadChat stub (", + threadId, + ")", + (messageActions ?? []).map((action) => /* @__PURE__ */ jsx( + "button", + { + type: "button", + "data-testid": `bb-thread-chat-action-${action.id}`, + "data-roles": action.roles === void 0 ? "" : action.roles.join(" "), + onClick: () => { + void action.run({ + id: "test-message", + threadId, + role: action.roles?.[0] ?? "assistant", + text: "test message text", + sourceSeqEnd: 1 + }); + }, + children: action.title + }, + action.id + )) + ] + } + ); +} +function TestMarkdown({ content, className }) { + return /* @__PURE__ */ jsx("div", { "data-testid": "bb-markdown", className, children: content }); +} +var testPluginSdkApp = { + definePluginApp, + useRpc() { + return useSlotEnv("useRpc").rpcClient; + }, + useRealtime(channel, handler) { + const env = useSlotEnv("useRealtime"); + const handlerRef = useRef(handler); + useEffect(() => { + handlerRef.current = handler; + }); + useEffect(() => { + const listener = (payload) => handlerRef.current(payload); + let listeners = env.realtimeHandlers.get(channel); + if (!listeners) { + listeners = /* @__PURE__ */ new Set(); + env.realtimeHandlers.set(channel, listeners); + } + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, [env, channel]); + }, + useRealtimeConnectionState() { + const connection = useSlotEnv( + "useRealtimeConnectionState" + ).realtimeConnection; + return useSyncExternalStore( + connection.subscribe, + connection.getSnapshot, + connection.getSnapshot + ); + }, + useSettings() { + return useSlotEnv("useSettings").settingsState; + }, + useBbContext() { + return useSlotEnv("useBbContext").bbContext; + }, + useBbNavigate() { + return useSlotEnv("useBbNavigate").navigate; + }, + useComposer() { + const composer = useSlotEnv("useComposer").composer; + const version = useSyncExternalStore( + composer.subscribe, + composer.getVersionSnapshot, + composer.getVersionSnapshot + ); + return useMemo( + () => ({ + ...composer.api, + scope: composer.getScope(), + text: composer.getText() + }), + [composer, version] + ); + }, + experimental_ThreadChat: TestThreadChat, + experimental_Markdown: TestMarkdown, + useComposerView() { + const composer = useSlotEnv("useComposerView").composer; + const version = useSyncExternalStore( + composer.subscribe, + composer.getVersionSnapshot, + composer.getVersionSnapshot + ); + return useMemo(() => { + const text = composer.getText(); + const attachmentCount = composer.getAttachmentCount(); + return { + scope: composer.getScope(), + layout: "expanded", + draft: { + text, + isEmpty: isComposerDraftEmpty(text, attachmentCount), + attachmentCount + }, + run: { isRunning: false, isSubmitting: false } + }; + }, [composer, version]); + } +}; +function installTestPluginRuntime() { + const host = globalThis; + host.__bbPluginRuntime = { + ...host.__bbPluginRuntime, + pluginSdkApp: testPluginSdkApp + }; +} +function collectRegistrations(definition) { + const captured = { + homepageSections: [], + settingsSections: [], + navPanels: [], + threadPanelActions: [], + composerCustomizations: [], + pendingInteractions: [], + sidebarFooterActions: [], + fileOpeners: [], + messageDirectives: [], + messageActions: [], + contentScripts: [] + }; + const seenIds = { + homepageSection: /* @__PURE__ */ new Set(), + settingsSection: /* @__PURE__ */ new Set(), + navPanel: /* @__PURE__ */ new Set(), + threadPanelAction: /* @__PURE__ */ new Set(), + composerCustomization: /* @__PURE__ */ new Set(), + pendingInteraction: /* @__PURE__ */ new Set(), + sidebarFooterAction: /* @__PURE__ */ new Set(), + fileOpener: /* @__PURE__ */ new Set(), + messageDirective: /* @__PURE__ */ new Set(), + messageAction: /* @__PURE__ */ new Set(), + contentScript: /* @__PURE__ */ new Set() + }; + definition.setup({ + slots: { + homepageSection(registration) { + const kind = "slots.homepageSection"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.homepageSection, id); + captured.homepageSections.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + component: requireComponent(kind, registration.component) + }); + }, + settingsSection(registration) { + const kind = "slots.settingsSection"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.settingsSection, id); + const title = requireOptionalString(kind, "title", registration.title); + const description = requireOptionalString( + kind, + "description", + registration.description + ); + captured.settingsSections.push({ + id, + ...title !== void 0 ? { title } : {}, + ...description !== void 0 ? { description } : {}, + component: requireComponent(kind, registration.component) + }); + }, + navPanel(registration) { + const kind = "slots.navPanel"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.navPanel, id); + const path = requireNonEmptyString(kind, "path", registration.path); + if (!PLUGIN_SLOT_ID_PATTERN.test(path)) { + throw new Error( + `${kind}: "path" must match ${String(PLUGIN_SLOT_ID_PATTERN)} (it becomes a URL segment), got ${JSON.stringify(path)}` + ); + } + if (registration.headerContent !== void 0 && typeof registration.headerContent !== "function") { + throw new Error( + `${kind}: "headerContent" must be a React component function when set` + ); + } + captured.navPanels.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + icon: requireNonEmptyString(kind, "icon", registration.icon), + path, + component: requireComponent(kind, registration.component), + ...registration.headerContent !== void 0 ? { headerContent: registration.headerContent } : {} + }); + }, + threadPanelAction(registration) { + const kind = "slots.threadPanelAction"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.threadPanelAction, id); + if (registration.run !== void 0 && typeof registration.run !== "function") { + throw new Error(`${kind}: "run" must be a function when set`); + } + if (registration.layout !== void 0 && registration.layout !== "padded" && registration.layout !== "flush") { + throw new Error(`${kind}: "layout" must be "padded" or "flush"`); + } + captured.threadPanelActions.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + ...registration.icon !== void 0 ? { icon: requireNonEmptyString(kind, "icon", registration.icon) } : {}, + component: requireComponent(kind, registration.component), + ...registration.layout !== void 0 ? { layout: registration.layout } : {}, + ...registration.run !== void 0 ? { run: registration.run } : {} + }); + }, + pendingInteraction(registration) { + const kind = "slots.pendingInteraction"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.pendingInteraction, id); + captured.pendingInteractions.push({ + id, + component: requireComponent(kind, registration.component) + }); + }, + sidebarFooterAction(registration) { + const kind = "slots.sidebarFooterAction"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.sidebarFooterAction, id); + if (typeof registration.run !== "function") { + throw new Error(`${kind}: "run" must be a function`); + } + captured.sidebarFooterActions.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + icon: requireNonEmptyString(kind, "icon", registration.icon), + run: registration.run + }); + }, + fileOpener(registration) { + const kind = "slots.fileOpener"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.fileOpener, id); + const rawExtensions = registration?.extensions; + if (!Array.isArray(rawExtensions) || rawExtensions.length === 0) { + throw new Error( + `${kind}: "extensions" must be a non-empty array of lowercase extensions without the dot` + ); + } + const extensions = rawExtensions.map((extension) => { + if (typeof extension !== "string" || !/^[a-z0-9]+$/.test(extension)) { + throw new Error( + `${kind}: extensions must be lowercase alphanumerics without the dot, got ${JSON.stringify(extension)}` + ); + } + return extension; + }); + captured.fileOpeners.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + extensions, + component: requireComponent(kind, registration.component) + }); + }, + messageDirective(registration) { + const kind = "slots.messageDirective"; + const id = requireMessageDirectiveId(kind, registration?.id); + requireUniqueId(kind, seenIds.messageDirective, id); + captured.messageDirectives.push({ + id, + component: requireComponent(kind, registration.component) + }); + }, + experimental_messageAction(registration) { + const kind = "slots.experimental_messageAction"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.messageAction, id); + if (typeof registration.run !== "function") { + throw new Error(`${kind}: "run" must be a function`); + } + const rawPlacements = registration.placements; + let placements; + if (rawPlacements !== void 0) { + if (!Array.isArray(rawPlacements) || rawPlacements.length === 0) { + throw new Error( + `${kind}: "placements" must be a non-empty array when set` + ); + } + placements = []; + for (const placement of rawPlacements) { + if (placement !== "action-bar" && placement !== "selection-menu") { + throw new Error( + `${kind}: "placements" entries must be "action-bar" or "selection-menu"` + ); + } + if (placements.includes(placement)) { + throw new Error( + `${kind}: "placements" must not contain duplicates` + ); + } + placements.push(placement); + } + } + captured.messageActions.push({ + id, + title: requireNonEmptyString(kind, "title", registration.title), + ...registration.icon !== void 0 ? { icon: requireNonEmptyString(kind, "icon", registration.icon) } : {}, + ...placements !== void 0 ? { placements } : {}, + run: registration.run + }); + } + }, + composer: { + customize(registration) { + const customization = collectComposerCustomization( + registration, + seenIds.composerCustomization, + (reason) => console.warn(reason) + ); + if (customization !== null) { + captured.composerCustomizations.push(customization); + } + } + }, + experimental_contentScripts: { + register(registration) { + const kind = "experimental_contentScripts.register"; + const id = requireSlotId(kind, registration?.id); + requireUniqueId(kind, seenIds.contentScript, id); + if (typeof registration.mount !== "function") { + throw new Error(`${kind}: "mount" must be a function`); + } + captured.contentScripts.push({ id, mount: registration.mount }); + } + } + }); + return captured; +} +async function loadPluginApp(source) { + installTestPluginRuntime(); + const resolved = typeof source === "function" ? await source() : source; + const definition = isPluginAppDefinition(resolved) ? resolved : resolved.default; + if (!isPluginAppDefinition(definition)) { + throw new Error( + "the bundle's default export is not definePluginApp(...) from @bb/plugin-sdk/app" + ); + } + return collectRegistrations(definition); +} +async function mountPluginContentScripts(app, options) { + const controller = new AbortController(); + const generation = options.generation ?? 1; + const mounted = []; + const rpcCalls = []; + const navigateCalls = []; + const realtimeHandlers = /* @__PURE__ */ new Map(); + const connectionStateHandlers = /* @__PURE__ */ new Set(); + let connectionState = options.realtimeConnectionState ?? "connected"; + let disposed = false; + const dispose = async () => { + if (disposed) return; + disposed = true; + controller.abort(); + for (const script of [...mounted].reverse()) { + if (script.dispose === null) continue; + try { + await script.dispose(); + } catch (error) { + console.warn( + `[plugin:${options.pluginId}] content script "${script.id}" cleanup failed: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + }; + try { + for (const registration of app.contentScripts) { + const rpc = { + async call(method, input) { + rpcCalls.push({ method, input: input ?? null }); + const handler = options.rpc?.[method]; + if (handler === void 0) { + throw new Error(`No content-script RPC handler for "${method}"`); + } + return handler(input ?? null); + } + }; + const result = await registration.mount({ + pluginId: options.pluginId, + generation, + signal: controller.signal, + rpc, + realtime: { + subscribe(channel, handler) { + const handlers = realtimeHandlers.get(channel) ?? /* @__PURE__ */ new Set(); + handlers.add(handler); + realtimeHandlers.set(channel, handlers); + let active = true; + const unsubscribe = () => { + if (!active) return; + active = false; + handlers.delete(handler); + if (handlers.size === 0) realtimeHandlers.delete(channel); + }; + controller.signal.addEventListener("abort", unsubscribe, { + once: true + }); + return unsubscribe; + }, + getConnectionState: () => connectionState, + subscribeConnectionState(handler) { + connectionStateHandlers.add(handler); + let active = true; + const unsubscribe = () => { + if (!active) return; + active = false; + connectionStateHandlers.delete(handler); + }; + controller.signal.addEventListener("abort", unsubscribe, { + once: true + }); + return unsubscribe; + } + }, + navigate: { + toCompose(navigateOptions) { + navigateCalls.push({ + method: "toCompose", + ...navigateOptions !== void 0 ? { options: navigateOptions } : {} + }); + } + } + }); + if (result !== void 0 && typeof result !== "function") { + throw new Error( + `content script "${registration.id}" mount must return a cleanup function, a promise of one, or nothing` + ); + } + mounted.push({ id: registration.id, dispose: result ?? null }); + } + } catch (error) { + await dispose(); + throw error; + } + return { + inspection: { + get mountedIds() { + return mounted.map(({ id }) => id); + }, + signal: controller.signal, + get disposed() { + return disposed; + }, + rpcCalls, + navigateCalls + }, + behavior: { + publishRealtime(channel, payload) { + for (const handler of realtimeHandlers.get(channel) ?? []) { + handler(payload); + } + }, + setRealtimeConnectionState(state) { + connectionState = state; + for (const handler of connectionStateHandlers) handler(state); + } + }, + lifecycle: { dispose } + }; +} +function strictJsonRoundTrip(value, label) { + const ancestors = /* @__PURE__ */ new Set(); + function visit(current, path) { + if (current === null || typeof current === "string" || typeof current === "boolean") { + return; + } + if (typeof current === "number") { + if (!Number.isFinite(current)) { + throw new Error(`${label} at ${path} contains a non-finite number`); + } + return; + } + if (typeof current !== "object") { + throw new Error(`${label} at ${path} is not a JSON value`); + } + if (ancestors.has(current)) { + throw new Error(`${label} at ${path} is cyclic`); + } + ancestors.add(current); + try { + if (Array.isArray(current)) { + current.forEach((item, index) => visit(item, `${path}[${index}]`)); + return; + } + const prototype = Object.getPrototypeOf(current); + if (prototype !== Object.prototype && prototype !== null) { + throw new Error(`${label} at ${path} must be a plain JSON object`); + } + if (Reflect.ownKeys(current).some((key) => typeof key === "symbol")) { + throw new Error(`${label} at ${path} contains a symbol key`); + } + for (const [key, child] of Object.entries(current)) { + visit(child, `${path}.${key}`); + } + } finally { + ancestors.delete(current); + } + } + visit(value, "$"); + return JSON.parse(JSON.stringify(value)); +} +function renderSlot(registration, props, options = {}) { + const rpcCalls = []; + const rpcHandlers = options.rpc ?? {}; + const rpcClient = { + async call(method, input) { + const normalizedInput = input === void 0 ? null : strictJsonRoundTrip(input, `rpc "${method}" input`); + rpcCalls.push({ method, input: normalizedInput }); + const handler = rpcHandlers[method]; + if (!handler) { + throw new Error( + `no rpc handler for "${method}" \u2014 add it to renderSlot options.rpc` + ); + } + const result2 = await handler(normalizedInput); + return strictJsonRoundTrip(result2, `rpc "${method}" result`); + } + }; + const realtimeHandlers = /* @__PURE__ */ new Map(); + let realtimeConnectionState = options.realtimeConnectionState ?? "connected"; + const realtimeConnectionListeners = /* @__PURE__ */ new Set(); + const realtimeConnection = { + getSnapshot: () => realtimeConnectionState, + subscribe(listener) { + realtimeConnectionListeners.add(listener); + return () => realtimeConnectionListeners.delete(listener); + }, + setState(state) { + if (state === realtimeConnectionState) return; + realtimeConnectionState = state; + for (const listener of realtimeConnectionListeners) listener(); + } + }; + const navigateCalls = []; + const navigate = { + toThread(threadId2) { + navigateCalls.push({ method: "toThread", threadId: threadId2 }); + }, + toProject(projectId2) { + navigateCalls.push({ method: "toProject", projectId: projectId2 }); + }, + toPluginPanel(path, panelOptions) { + navigateCalls.push({ + method: "toPluginPanel", + path, + ...panelOptions !== void 0 ? { options: panelOptions } : {} + }); + }, + toCompose(composeOptions) { + navigateCalls.push({ + method: "toCompose", + ...composeOptions !== void 0 ? { options: composeOptions } : {} + }); + }, + experimental_openThreadPanel(panelOptions) { + navigateCalls.push({ + method: "experimental_openThreadPanel", + options: panelOptions + }); + return options.openThreadPanel?.(panelOptions) ?? false; + } + }; + const projectId = options.context?.projectId ?? null; + const threadId = options.context?.threadId ?? null; + let composerScope = options.composer?.scope ?? (threadId !== null ? { kind: "thread", threadId } : { kind: "new-thread", projectId }); + let composerText = options.composer?.text ?? ""; + const composerAttachmentCount = options.composer?.attachmentCount ?? 0; + let composerVersion = 0; + const composerListeners = /* @__PURE__ */ new Set(); + const notifyComposerListeners = () => { + composerVersion += 1; + for (const listener of composerListeners) listener(); + }; + const commitComposerText = (next) => { + if (next === composerText) return; + composerText = next; + notifyComposerListeners(); + }; + const composerLog = { + get text() { + return composerText; + }, + get scope() { + return composerScope; + }, + get attachmentCount() { + return composerAttachmentCount; + }, + textEffect: null, + textEffectCalls: [], + inputLocked: false, + inputLockCalls: [], + threadRowStatus: null, + threadRowStatusCalls: [], + quotes: [], + mentions: [], + focusCount: 0 + }; + const composerOwnership = { active: true }; + const composer = { + getAttachmentCount: () => composerAttachmentCount, + getScope: () => composerScope, + getText: () => composerText, + getVersionSnapshot: () => composerVersion, + subscribe(listener) { + composerListeners.add(listener); + return () => composerListeners.delete(listener); + }, + api: { + setText(next) { + commitComposerText(next); + }, + updateText(updater) { + commitComposerText(updater(composerText)); + }, + clear() { + commitComposerText(""); + }, + setTextEffect(effect) { + if (!composerOwnership.active) return; + composerLog.textEffect = effect; + composerLog.textEffectCalls.push(effect); + }, + setInputLock(locked) { + if (!composerOwnership.active) return; + composerLog.inputLocked = locked; + composerLog.inputLockCalls.push(locked); + }, + setThreadRowStatus(status) { + if (!composerOwnership.active || composerScope.kind === "new-thread") { + return; + } + composerLog.threadRowStatus = status; + composerLog.threadRowStatusCalls.push(status); + }, + addQuote(text) { + const trimmed = text.replace(/\r\n|\r/gu, "\n").trim(); + if (trimmed !== "") { + const block = trimmed.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n"); + commitComposerText( + composerText === "" ? `${block} +` : `${composerText} +${block} +` + ); + composerLog.quotes.push(text); + } + composerLog.focusCount += 1; + }, + insertMention(mention) { + const label = mention.label.trim() || mention.id; + const separator = composerText.length === 0 || /\s$/u.test(composerText) ? "" : " "; + commitComposerText(`${composerText}${separator}${label} `); + composerLog.mentions.push(mention); + composerLog.focusCount += 1; + }, + focus() { + composerLog.focusCount += 1; + } + } + }; + const env = { + rpcClient, + rpcCalls, + realtimeHandlers, + realtimeConnection, + settingsState: { values: options.settings, isLoading: false }, + bbContext: { projectId, threadId }, + navigate, + navigateCalls, + composer, + composerLog + }; + const releaseComposerOwnership = () => { + if (!composerOwnership.active) return; + composerOwnership.active = false; + composerLog.textEffect = null; + composerLog.inputLocked = false; + composerLog.threadRowStatus = null; + }; + const renderSlotTree = (ui) => /* @__PURE__ */ jsx(SlotEnvContext.Provider, { value: env, children: /* @__PURE__ */ jsx(SlotLifecycleGuard, { onUnmount: releaseComposerOwnership, children: ui }) }); + const Component = registration.component; + const element = renderSlotTree(/* @__PURE__ */ jsx(Component, { ...props })); + const result = render(element); + const rerenderSlot = (ui) => { + result.rerender(renderSlotTree(ui)); + }; + const emitRealtime = async (channel, payload) => { + const normalized = payload === void 0 ? null : strictJsonRoundTrip(payload, `realtime "${channel}" payload`); + const listeners = realtimeHandlers.get(channel); + await act(async () => { + for (const listener of [...listeners ?? []]) { + listener(normalized); + } + }); + }; + const setRealtimeConnectionState = async (state) => { + await act(async () => realtimeConnection.setState(state)); + }; + const setComposerText = async (text) => { + await act(async () => commitComposerText(text)); + }; + const setComposerScope = async (scope) => { + await act(async () => { + composerScope = scope; + notifyComposerListeners(); + }); + }; + const unmountSlot = () => { + if (!composerOwnership.active) return; + result.unmount(); + }; + return { + ...result, + rerender: rerenderSlot, + unmount: unmountSlot, + rpcCalls, + emitRealtime, + setRealtimeConnectionState, + setComposerText, + setComposerScope, + navigateCalls, + composer: composerLog, + behavior: { + emitRealtime, + setRealtimeConnectionState, + setComposerText, + setComposerScope + }, + inspection: { rpcCalls, navigateCalls, composer: composerLog }, + lifecycle: { rerender: rerenderSlot, unmount: unmountSlot } + }; +} +export { + installTestPluginRuntime, + loadPluginApp, + mountPluginContentScripts, + renderSlot +}; diff --git a/plugins/timeline-comments/tsconfig.json b/plugins/timeline-comments/tsconfig.json new file mode 100644 index 0000000..dfb6726 --- /dev/null +++ b/plugins/timeline-comments/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@bb/plugin-sdk": [ + "./types/bb-plugin-sdk.d.ts" + ], + "@bb/plugin-sdk/app": [ + "./types/bb-plugin-sdk-app.d.ts" + ] + }, + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": [ + "node", + "vitest/globals" + ] + }, + "include": [ + "*.ts", + "*.tsx" + ] +} diff --git a/plugins/timeline-comments/types/bb-plugin-sdk-app.d.ts b/plugins/timeline-comments/types/bb-plugin-sdk-app.d.ts new file mode 100644 index 0000000..22cf8e7 --- /dev/null +++ b/plugins/timeline-comments/types/bb-plugin-sdk-app.d.ts @@ -0,0 +1,794 @@ +// Portable type declarations for `@bb/plugin-sdk`. Unpublished BB +// workspace contracts are flattened; public subpaths may reuse the +// package root without requiring any other @bb/* package. +// +// Confused by the API, or need a symbol that isn't here? Clone the BB repo +// and read the real source: https://github.com/ymichael/bb + +import * as react from 'react'; +import { ComponentType, ReactNode } from 'react'; + +/** A JSON-safe path segment reported by a Standard Schema validation issue. */ +type PluginRpcIssuePathSegment = string | number; +/** Validator-neutral validation detail carried by an RPC error envelope. */ +interface PluginRpcValidationIssue { + message: string; + path?: PluginRpcIssuePathSegment[]; +} +/** Stable wire error categories for plugin RPC. */ +type PluginRpcErrorCode = "invalid_json" | "invalid_input" | "handler_error" | "invalid_output" | "non_json_result" | "unknown_method"; +/** Structured RPC failure returned as `{ ok: false, error }`. */ +interface PluginRpcError { + code: PluginRpcErrorCode; + message: string; + issues?: PluginRpcValidationIssue[]; +} +/** + * The validator-neutral subset of Standard Schema v1 used by plugin RPC. + * Zod 4 schemas implement this interface directly; other validators can do + * the same without becoming part of BB's public protocol. + */ +interface StandardSchemaV1 { + readonly "~standard": { + readonly version: 1; + readonly vendor: string; + readonly validate: (value: unknown) => StandardSchemaV1Result | Promise>; + readonly types?: { + readonly input: Input; + readonly output: Output; + }; + }; +} +type StandardSchemaV1Result = { + readonly value: Output; + readonly issues?: undefined; +} | { + readonly issues: readonly StandardSchemaV1Issue[]; +}; +interface StandardSchemaV1Issue { + readonly message: string; + readonly path?: PropertyKey | readonly (PropertyKey | { + readonly key: PropertyKey; + })[]; +} +type StandardSchemaV1InferInput = NonNullable["input"]; +type StandardSchemaV1InferOutput = NonNullable["output"]; +interface PluginRpcMethodContract { + readonly input: InputSchema; + readonly output: OutputSchema; +} +type PluginRpcContract = Readonly>; +type PluginRpcHandlers = { + [Method in keyof Contract]: (input: StandardSchemaV1InferOutput) => StandardSchemaV1InferInput | Promise>; +}; +type PluginRpcCallInput = StandardSchemaV1InferInput; +type PluginRpcCallArgs = null extends PluginRpcCallInput ? [input?: PluginRpcCallInput] : [input: PluginRpcCallInput]; +type PluginRpcResult = StandardSchemaV1InferOutput; + +/** + * A value that survives a JSON round trip without coercion or data loss. + * + * Host boundaries still validate values at runtime because TypeScript cannot + * exclude non-finite numbers and plugin bundles can bypass static types. + */ +type JsonValue = string | number | boolean | null | JsonValue[] | { + [key: string]: JsonValue; +}; + +/** + * The `@bb/plugin-sdk/app` contract (plugin design §5.2) — pure types with no + * side effects. The BB app imports these to keep its real implementation in + * sync (`satisfies PluginSdkApp`). Plugin authors import the same shapes through + * `@bb/plugin-sdk/app`. + * + * Per-slot props are versioned contracts: additive-only within an SDK major. + */ +/** Props passed to a `homepageSection` component. */ +interface PluginHomepageSectionProps { + /** Project in view on the compose surface; null when none is selected. */ + projectId: string | null; +} +/** + * Props passed to a `settingsSection` component. + * + * Deliberately empty in V1; versioned additive like the other slot props. + */ +interface PluginSettingsSectionProps { +} +/** Props passed to a `navPanel` component (it owns its whole route). */ +interface PluginNavPanelProps { + /** + * The route remainder after the panel root, "" at the root. The panel's + * route is `/plugins///*`, so a deep link like + * `/plugins/notes/notes/work/ideas.md` renders the panel with + * `subPath: "work/ideas.md"`. Navigate within the panel via + * `useBbNavigate().toPluginPanel(path, { subPath })` — browser + * back/forward then walks panel-internal history. + */ + subPath: string; +} +/** Props passed to a panel tab opened by a `threadPanelAction`. */ +interface PluginThreadPanelProps { + threadId: string; + /** + * The JSON value the action's `openPanel` call passed (round-tripped + * through persistence, so the tab restores across reloads); null when the + * action opened the panel without params. + */ + params: JsonValue | null; +} +interface PluginPendingInteractionView { + id: string; + threadId: string; + title: string; + payload: JsonValue; + createdAt: number; + expiresAt: number | null; +} +interface PluginPendingInteractionProps { + interaction: PluginPendingInteractionView; + submit(value: JsonValue): Promise; + cancel(): Promise; +} +/** + * Props for a `sidebarFooterAction` — host-rendered (no plugin component). + * Deliberately empty; the registration's `run` carries the behavior. + */ +interface PluginSidebarFooterActionProps { +} +/** + * Where a file being opened by a `fileOpener` lives. `path` semantics follow + * the source: workspace paths are relative to the environment's worktree, + * thread-storage paths are relative to the thread's storage root, host paths + * are absolute on the thread's host. + */ +interface PluginFileOpenerSource { + kind: "workspace" | "host" | "thread-storage"; + threadId: string | null; + environmentId: string | null; + projectId: string | null; +} +/** Props passed to a `fileOpener` component (rendered as a panel file tab). */ +interface PluginFileOpenerProps { + path: string; + source: PluginFileOpenerSource; +} +/** + * Message context passed to a `messageDirective` component — the assistant + * (or nested agent) message that contained the directive. + */ +interface PluginMessageDirectiveMessage { + id: string; + threadId: string; + turnId: string | null; + projectId: string | null; +} +/** + * Open a worktree-relative file in the host's workspace file viewer. Returns + * true when the host accepted the path; false when the path is invalid or the + * viewer declined it. + */ +type PluginMessageDirectiveOpenWorkspaceFile = (path: string) => boolean; +/** + * Props passed to a `messageDirective` component. Attributes are untrusted + * strings parsed from the directive; the plugin validates its own fields. + */ +interface PluginMessageDirectiveProps { + /** Parsed, untrusted directive attributes (e.g. `{ file: "demo.html" }`). */ + attributes: Readonly>; + /** Original directive source text (useful for diagnostics / crash fallback). */ + source: string; + message: PluginMessageDirectiveMessage; + /** + * Opens a worktree-relative file in the host's workspace file viewer. Null + * when the message surface has no workspace viewer available. + */ + openWorkspaceFile: PluginMessageDirectiveOpenWorkspaceFile | null; +} +interface PluginHomepageSectionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + title: string; + component: ComponentType; +} +interface PluginSettingsSectionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Optional host-rendered section heading. */ + title?: string; + /** + * Optional one-line host-rendered subheading under `title`, in the built-in + * SettingsSection idiom (ignored when `title` is absent). + */ + description?: string; + component: ComponentType; +} +interface PluginNavPanelRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon: string; + /** URL segment under `/plugins//`; letters, digits, `-`, `_`. */ + path: string; + component: ComponentType; + /** + * Optional component rendered on the right side of the shared title bar + * (e.g. a sync button or a count). Contained separately from the body: a + * throwing headerContent is hidden without breaking the title bar. + */ + headerContent?: ComponentType; +} +/** Context handed to a `threadPanelAction`'s `run`. */ +interface PluginThreadPanelActionContext { + /** The thread whose panel launcher invoked the action. */ + threadId: string; + /** + * Open a tab in the thread's side panel rendering this action's + * `component`. `title` labels the tab (default: the action's `title`); + * `params` must be JSON-serializable — it is persisted with the tab and + * reaches the component as its `params` prop. Opening with params + * identical to an already-open tab of this action focuses that tab + * (updating its title) instead of duplicating it. May be called more than + * once (different params ⇒ multiple tabs) or not at all. + */ + openPanel(options?: { + title?: string; + params?: JsonValue; + }): void; +} +interface PluginThreadPanelActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Label of the action row in the panel's new-tab launcher. */ + title: string; + /** + * Icon hint (BB icon name) used when the plugin ships no logo; the + * launcher row and opened tabs prefer the plugin's logo. + */ + icon?: string; + /** Rendered inside every panel tab this action opens. */ + component: ComponentType; + /** + * How the host frames the tab content. "padded" (default) wraps the + * component in the panel's scroll container with standard padding — + * right for document-like content. "flush" gives the component the full + * tab area (no padding, definite height, no host scrolling) — right for + * app-like content that manages its own layout, such as + * `experimental_ThreadChat`. + */ + layout?: "padded" | "flush"; + /** + * Runs when the user activates the action: call your RPC methods, show a + * toast, and/or open panel tabs via `context.openPanel`. Omitted = + * immediately open a panel tab with defaults. Errors (sync or async) are + * contained and logged; they never break the launcher. + */ + run?(context: PluginThreadPanelActionContext): void | Promise; +} +interface PluginPendingInteractionRegistration { + /** Matches `rendererId` passed to `bb.ui.requestInput`. */ + id: string; + component: ComponentType; +} +/** Context handed to a `sidebarFooterAction`'s `run`. */ +interface PluginSidebarFooterActionContext { + /** + * Navigate to this plugin's Settings detail page + * (`/settings/plugins/`), where declarative settings and + * `settingsSection` slots render. + */ + openSettings(): void; +} +/** + * An icon button in the app sidebar footer (next to Settings / bug report). + * Host-rendered for consistent chrome — plugins supply icon, label, and + * `run` behavior only. + */ +interface PluginSidebarFooterActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip and accessible label for the icon button. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon: string; + /** + * Runs when the user activates the action (e.g. call `openSettings()`, + * open a panel via other surfaces, toast). Errors (sync or async) are + * contained and logged; they never break the sidebar. + */ + run(context: PluginSidebarFooterActionContext): void | Promise; +} +/** + * Register this plugin as a viewer/editor for file extensions. The user + * picks (and can set as default) an opener per extension via the file tab's + * "Open with" menu; matching files opened in the panel then render + * `component` in a plugin tab instead of the built-in preview. Applies to + * working-tree, host, and thread-storage files — never to git-ref snapshots + * (diff views always use the built-in preview). The built-in preview stays + * one menu click away, and a missing/disabled opener falls back to it. + */ +interface PluginFileOpenerRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Label in the "Open with" menu (e.g. "Notes editor"). */ + title: string; + /** Lowercase extensions without the dot (e.g. ["md", "mdx"]). */ + extensions: readonly string[]; + component: ComponentType; +} +/** + * Register a leaf message directive rendered inside assistant (and nested + * agent) message Markdown. `id` is the directive name: `inline-vis` matches + * `::inline-vis{file="demo.html"}`. + */ +interface PluginMessageDirectiveRegistration { + /** + * The directive name. Lowercase kebab-case beginning with a letter. + */ + id: string; + component: ComponentType; +} +/** + * A narrow, stable reference to one rendered chat message — NOT an internal + * timeline row. `sourceSeqEnd` is the last source event sequence the message + * covers, the anchor the server accepts for provider-history forks. + */ +interface ThreadChatMessageReference { + id: string; + threadId: string; + role: "user" | "assistant"; + /** Visible text of the message. */ + text: string; + sourceSeqEnd: number; +} +interface PluginMessageActionThreadPanelOptions { + /** A `threadPanelAction` id registered by this same plugin. */ + actionId: string; + title?: string; + params?: JsonValue; +} +/** Context handed to a `messageAction`'s `run`. */ +interface PluginMessageActionContext { + /** The thread whose timeline surfaced the action. */ + threadId: string; + message: ThreadChatMessageReference; + /** + * Present only when the action was invoked from the text-selection menu; + * the exact text the user highlighted inside `message`. + */ + selectedText?: string; + /** + * Open one of this plugin's `threadPanelAction` components in the current + * thread's side panel — the registration-callback equivalent of + * `useBbNavigate().experimental_openThreadPanel`. Returns true when the host + * accepted (the action id exists and the surface has a panel); false + * otherwise. + */ + openPanel(options: PluginMessageActionThreadPanelOptions): boolean; +} +/** + * An action on chat messages: an icon button in the per-message action bar + * (user and assistant messages) and an entry in the assistant-message + * text-selection menu. Host-rendered chrome — the plugin supplies title, + * icon hint, and `run` behavior only. + */ +interface PluginMessageActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip / menu label for the action. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon?: string; + /** + * Runs when the user activates the action. Errors (sync or async) are + * contained and logged; they never break the timeline. + */ + run(context: PluginMessageActionContext): void | Promise; +} +interface PluginAppSlots { + homepageSection(registration: PluginHomepageSectionRegistration): void; + settingsSection(registration: PluginSettingsSectionRegistration): void; + navPanel(registration: PluginNavPanelRegistration): void; + threadPanelAction(registration: PluginThreadPanelActionRegistration): void; + pendingInteraction(registration: PluginPendingInteractionRegistration): void; + sidebarFooterAction(registration: PluginSidebarFooterActionRegistration): void; + fileOpener(registration: PluginFileOpenerRegistration): void; + messageDirective(registration: PluginMessageDirectiveRegistration): void; + experimental_messageAction(registration: PluginMessageActionRegistration): void; +} +interface PluginAppComposer { + customize(registration: ComposerCustomization): void; +} +/** Stable lifecycle values for one content-script instance in one bb client. */ +interface PluginContentScriptContext { + /** The id of the plugin that owns this script. */ + readonly pluginId: string; + /** Monotonic per-client generation, starting at 1. */ + readonly generation: number; + /** Aborted before cleanup begins on replacement, deactivation, or teardown. */ + readonly signal: AbortSignal; +} +/** Cleanup returned by a frontend content script. */ +type PluginContentScriptDisposer = () => void | Promise; +/** + * Trusted same-origin JavaScript/TypeScript mounted once per active frontend + * generation in each bb app window or browser tab. + */ +interface PluginContentScriptRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** + * Install behavior into the bb app shell. The host awaits a returned + * promise, contains failures, and calls the returned disposer exactly once. + */ + mount(context: PluginContentScriptContext): void | PluginContentScriptDisposer | Promise; +} +/** Experimental lifecycle surface for trusted frontend content scripts. */ +interface PluginAppContentScripts { + register(registration: PluginContentScriptRegistration): void; +} +interface PluginAppBuilder { + slots: PluginAppSlots; + composer: PluginAppComposer; + experimental_contentScripts: PluginAppContentScripts; +} +type PluginAppSetup = (app: PluginAppBuilder) => void; +/** + * The opaque product of `definePluginApp` — a plugin's `app.tsx` default + * export. The host re-runs `setup` against a fresh collector on every + * (re)interpretation, replacing that plugin's registrations wholesale. + */ +interface PluginAppDefinition { + /** Brand the host checks before interpreting a bundle's default export. */ + readonly __bbPluginApp: true; + readonly setup: PluginAppSetup; +} +interface PluginRpcClient { + /** + * Invoke one of the plugin's `bb.rpc` methods (POST + * /api/v1/plugins/<id>/rpc/<method>). Resolves with the method's + * inferred output; rejects with an `Error` carrying the server's message, + * stable `code`, and validation `issues` when present. + */ + call>(method: Method, ...args: PluginRpcCallArgs): Promise>; +} +interface PluginSettingsState { + /** + * Effective non-secret setting values (secret settings are excluded — + * read them server-side). Undefined while loading or unavailable. + */ + values: Record | undefined; + isLoading: boolean; +} +/** State of the app's shared realtime connection to the bb server. */ +type PluginRealtimeConnectionState = "connecting" | "connected" | "reconnecting"; +/** Where `useComposer()` writes. */ +type PluginComposerScope = { + kind: "thread"; + threadId: string; +} | { + kind: "queued-message"; + threadId: string; + queuedMessageId: string; +} | { + kind: "side-chat"; + projectId: string; + parentThreadId: string; + tabId: string; + childThreadId: string | null; +} | { + kind: "new-thread"; + /** Root compose's effective selected project; null only while unresolved. */ + projectId: string | null; +}; +/** One plugin-owned composer customization registration. */ +interface ComposerCustomization { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Composer kinds where this customization is active; omit for all kinds. */ + scopes?: readonly PluginComposerScope["kind"][]; + actions?: readonly { + id: string; + component: ComponentType; + }[]; + banners?: readonly { + id: string; + /** Host chrome around the banner. Defaults to `"card"`. */ + chrome?: "card" | "bare"; + component: ComponentType; + }[]; + plusMenu?: readonly ComposerPlusMenuItem[]; + richText?: ComposerRichTextSpec; +} +/** Host-rendered menu row in the composer's `+` menu. */ +interface ComposerPlusMenuItem { + id: string; + label: string; + /** BB icon name; unknown names fall back to the generic plugin icon. */ + icon?: string; + /** Accessible description for the host-rendered row. */ + description?: string; + disabled?: boolean | ((view: ComposerView) => boolean); + run(context: { + composer: PluginComposerApi; + view: ComposerView; + }): void | Promise; +} +/** Reactive read-side of the composer a plugin surface is mounted in. */ +interface ComposerView { + scope: PluginComposerScope; + layout: "expanded" | "compact" | "zen"; + draft: { + text: string; + isEmpty: boolean; + attachmentCount: number; + }; + run: { + isRunning: boolean; + isSubmitting: boolean; + }; +} +interface ComposerRichTextSpec { + /** Content-derived paint: match ranges receive `className`; text is never mutated. */ + effects?: readonly { + id: string; + /** Plain-text offsets into the current structured draft. */ + match(text: string): readonly { + from: number; + to: number; + }[]; + className: string; + }[]; + /** Debounced, read-only observation of the structured draft. */ + onDraftChange?(draft: ComposerStructuredDraft, view: ComposerView): void; +} +interface ComposerStructuredDraft { + text: string; + mentions: readonly { + from: number; + to: number; + provider: string; + id: string; + label: string; + }[]; +} +/** Host-rendered paint applied to the editable composer text. */ +interface PluginComposerTextEffect { + className: string; +} +/** Host-rendered status that temporarily replaces a thread's draft glyph. */ +interface PluginComposerThreadRowStatus { + /** BB icon-name hint; unknown names fall back to the generic plugin icon. */ + icon: string; + /** Accessible label for the status glyph. */ + label: string; + /** Semantic host color for the status glyph. Defaults to the neutral tone. */ + tone?: "default" | "success"; +} +/** An @-mention pill bound to one of the calling plugin's mention providers. */ +interface PluginComposerMention { + /** Mention provider id registered by THIS plugin via `bb.ui.registerMentionProvider`. */ + provider: string; + /** Item id your provider's `resolve` will receive at send time. */ + id: string; + /** Pill text shown in the composer. */ + label: string; +} +/** + * Programmatic access to the chat composer draft — the same shared draft the + * built-in "Add to chat" affordances (file preview, diff, terminal selections) + * write to. While a queued message is being edited, writes land in that + * message's inline editor. In a side chat, writes land in the visible side-chat + * draft. Otherwise, inside a thread context writes land in that thread's draft; + * anywhere else (nav panel, homepage section) they seed the new-thread composer + * draft, which persists until the user sends or clears it. + */ +interface PluginComposerApi { + scope: PluginComposerScope; + /** Current plain text for this composer scope. */ + readonly text: string; + /** + * Replace the draft's plain text. Attachments are preserved. Inline mentions + * outside the changed range are preserved and rebased; mentions overlapped + * by the replacement are removed because their text representation changed. + */ + setText(next: string): void; + /** + * Replace the draft's plain text from the latest committed value. Uses the + * same structured-state reconciliation as `setText`. + */ + updateText(updater: (current: string) => string): void; + /** Clear plain text without clearing independently attached files. */ + clear(): void; + /** + * Apply a host-rendered effect to this composer's editable text, or clear it. + * Effects are scoped to the calling plugin and automatically clear when the + * slot unmounts or its composer scope changes. + */ + setTextEffect(effect: PluginComposerTextEffect | null): void; + /** + * Lock or unlock editing for this composer. Locks are scoped to the calling + * plugin and automatically release when the slot unmounts or its composer + * scope changes. + */ + setInputLock(locked: boolean): void; + /** + * Replace this composer's thread-row draft glyph with a host-rendered status, + * or clear it. New-thread composers have no row, so calls are a no-op. + * Side-chat and queued side-chat scopes decorate the visible parent-thread + * row. Status is scoped to the calling plugin and automatically clears when + * the slot unmounts or its composer scope changes. + */ + setThreadRowStatus(status: PluginComposerThreadRowStatus | null): void; + /** + * Append text to the draft as a `> ` blockquote block and focus the + * composer. Blank text is a no-op. This is the "reference this selection + * in chat" primitive. + */ + addQuote(text: string): void; + /** + * Insert an @-mention pill that resolves through this plugin's mention + * provider at send time — the durable way to reference an entity whose + * content should be fetched fresh when the message is sent. + */ + insertMention(mention: PluginComposerMention): void; + /** Focus the composer caret at the end of the draft. */ + focus(): void; +} +/** + * A consumer-supplied action on the messages of one `ThreadChat` instance, + * rendered in the embedded timeline's per-message action bar alongside the + * native and slot-registered actions. Unlike the `messageAction` slot this is + * scoped to the rendering component, not registered globally. + */ +interface ThreadChatMessageAction { + /** Unique within this ThreadChat instance; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip / menu label for the action. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon?: string; + /** + * Message roles the action applies to. Omitted = both user and assistant + * messages. + */ + roles?: readonly ("user" | "assistant")[]; + /** + * Runs when the user activates the action. Errors (sync or async) are + * contained and logged; they never break the timeline. + */ + run(message: ThreadChatMessageReference): void | Promise; +} +/** + * Props of the host-owned `ThreadChat` component — one thread's chat + * (timeline, and for the composer variants the full send/queue/draft + * engine), rendered by the BB app inside a plugin slot. This is the + * deliberate exception to the no-host-components rule (§5.5): a stable + * product capability, not a UI kit. Versioned additive like slot props; + * internal timeline rows, query hooks, and prompt-box configuration are + * deliberately not exposed. + */ +interface ThreadChatProps { + threadId: string; + /** + * "full" (default) is the page presentation (centered reading width); + * "compact" is the side-panel presentation; "timeline" renders the + * transcript without a composer. + */ + variant?: "full" | "compact" | "timeline"; + /** + * "contained" (default) fills and scrolls inside a bounded parent; + * "document" grows with its content and defers scrolling to the page. + */ + layout?: "contained" | "document"; + /** Bump to focus the composer (ignored by `variant: "timeline"`). */ + focusRequest?: number; + className?: string; + /** Rendered above the conversation, scrolling with it. */ + leadingContent?: ReactNode; + /** + * Actions rendered in this instance's per-message action bar (see + * {@link ThreadChatMessageAction}). + */ + messageActions?: readonly ThreadChatMessageAction[]; +} +/** + * Props of the host-owned `Markdown` component — bb's chat message renderer + * (the same typography, spacing, and code styling as timeline messages). + * Use it wherever plugin UI quotes or previews message content so it reads + * like the rest of the chat. Like `ThreadChat`, this is a stable product + * capability, not a UI kit; renderer internals stay private. + */ +interface MarkdownProps { + /** Markdown source, rendered exactly like a chat message body. */ + content: string; + className?: string; +} +/** Current app selection, derived from the route. */ +interface BbContext { + projectId: string | null; + threadId: string | null; +} +interface BbNavigate { + toThread(threadId: string): void; + toProject(projectId: string): void; + /** + * Navigate to one of this plugin's own nav panels by its `path`. + * `subPath` targets a location inside the panel (the component's + * `subPath` prop); `replace` swaps the current history entry instead of + * pushing — use it for redirects so back does not bounce. + */ + toPluginPanel(path: string, options?: { + subPath?: string; + replace?: boolean; + }): void; + /** + * Navigate to the root compose surface (the new-thread screen). Pass + * `initialPrompt` to seed the composer draft and `focusPrompt` to focus the + * composer on arrival — the pairing behind "Create via chat" style entry + * points that drop the user into chat with a prefilled prompt. + */ + toCompose(options?: { + initialPrompt?: string; + focusPrompt?: boolean; + }): void; + /** + * Open one of this plugin's registered thread-panel actions in the current + * thread surface. Returns false when the surface has no thread side panel or + * the action is unavailable. + */ + experimental_openThreadPanel(options: { + actionId: string; + title?: string; + params?: JsonValue; + }): boolean; +} +/** + * Everything `@bb/plugin-sdk/app` resolves to at runtime. The BB app builds + * the real implementation and `satisfies` this interface; `bb plugin build` + * shims the specifier to that object on `globalThis.__bbPluginRuntime`. + */ +interface PluginSdkApp { + definePluginApp(setup: PluginAppSetup): PluginAppDefinition; + useRpc(): PluginRpcClient; + useRealtime(channel: string, handler: (payload: unknown) => void): void; + /** + * Observe the same shared connection that delivers `useRealtime` signals. + * Use a subsequent transition to `connected` to reconcile server state that + * may have changed while ephemeral signals could not be delivered. The first + * connection can transition from `connecting` and is not a reconnection. + */ + useRealtimeConnectionState(): PluginRealtimeConnectionState; + useSettings(): PluginSettingsState; + useBbContext(): BbContext; + useBbNavigate(): BbNavigate; + useComposer(): PluginComposerApi; + /** + * The host-owned chat component (see {@link ThreadChatProps}). Together + * with `Markdown`, the only components the SDK ships — everything else + * stays vendored per §5.5. + */ + experimental_ThreadChat: ComponentType; + /** + * The host-owned chat-message markdown renderer (see + * {@link MarkdownProps}). + */ + experimental_Markdown: ComponentType; + useComposerView(): ComposerView; +} + +declare const definePluginApp: (setup: PluginAppSetup) => PluginAppDefinition; +declare const experimental_ThreadChat: react.ComponentType; +declare const experimental_Markdown: react.ComponentType; +declare const useRpc: , StandardSchemaV1>>>>() => PluginRpcClient; +declare const useRealtime: (channel: string, handler: (payload: unknown) => void) => void; +declare const useRealtimeConnectionState: () => PluginRealtimeConnectionState; +declare const useSettings: () => PluginSettingsState; +declare const useBbContext: () => BbContext; +declare const useBbNavigate: () => BbNavigate; +declare const useComposer: () => PluginComposerApi; +declare const useComposerView: () => ComposerView; + +export { definePluginApp, experimental_Markdown, experimental_ThreadChat, useBbContext, useBbNavigate, useComposer, useComposerView, useRealtime, useRealtimeConnectionState, useRpc, useSettings }; +export type { BbContext, BbNavigate, ComposerCustomization, ComposerPlusMenuItem, ComposerRichTextSpec, ComposerStructuredDraft, ComposerView, JsonValue, MarkdownProps, PluginAppBuilder, PluginAppComposer, PluginAppContentScripts, PluginAppDefinition, PluginAppSetup, PluginAppSlots, PluginComposerApi, PluginComposerMention, PluginComposerScope, PluginComposerTextEffect, PluginComposerThreadRowStatus, PluginContentScriptContext, PluginContentScriptDisposer, PluginContentScriptRegistration, PluginFileOpenerProps, PluginFileOpenerRegistration, PluginFileOpenerSource, PluginHomepageSectionProps, PluginHomepageSectionRegistration, PluginMessageActionContext, PluginMessageActionRegistration, PluginMessageActionThreadPanelOptions, PluginMessageDirectiveMessage, PluginMessageDirectiveOpenWorkspaceFile, PluginMessageDirectiveProps, PluginMessageDirectiveRegistration, PluginNavPanelProps, PluginNavPanelRegistration, PluginPendingInteractionProps, PluginPendingInteractionRegistration, PluginPendingInteractionView, PluginRealtimeConnectionState, PluginRpcCallArgs, PluginRpcClient, PluginRpcContract, PluginRpcError, PluginRpcErrorCode, PluginRpcHandlers, PluginRpcIssuePathSegment, PluginRpcMethodContract, PluginRpcResult, PluginRpcValidationIssue, PluginSdkApp, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsState, PluginSidebarFooterActionContext, PluginSidebarFooterActionProps, PluginSidebarFooterActionRegistration, PluginThreadPanelActionContext, PluginThreadPanelActionRegistration, PluginThreadPanelProps, StandardSchemaV1, StandardSchemaV1InferInput, StandardSchemaV1InferOutput, StandardSchemaV1Issue, StandardSchemaV1Result, ThreadChatMessageAction, ThreadChatMessageReference, ThreadChatProps }; diff --git a/plugins/timeline-comments/types/bb-plugin-sdk.d.ts b/plugins/timeline-comments/types/bb-plugin-sdk.d.ts new file mode 100644 index 0000000..8125dda --- /dev/null +++ b/plugins/timeline-comments/types/bb-plugin-sdk.d.ts @@ -0,0 +1,11762 @@ +// Portable type declarations for `@bb/plugin-sdk`. Unpublished BB +// workspace contracts are flattened; public subpaths may reuse the +// package root without requiring any other @bb/* package. +// +// Confused by the API, or need a symbol that isn't here? Clone the BB repo +// and read the real source: https://github.com/ymichael/bb + +import { ComponentType, ReactNode } from 'react'; +import Database from 'better-sqlite3'; +import { Context } from 'hono'; +import * as z from 'zod'; +import { z as z$1 } from 'zod'; + +/** + * A value that survives a JSON round trip without coercion or data loss. + * + * Host boundaries still validate values at runtime because TypeScript cannot + * exclude non-finite numbers and plugin bundles can bypass static types. + */ +type JsonValue$1 = string | number | boolean | null | JsonValue$1[] | { + [key: string]: JsonValue$1; +}; + +/** A JSON-safe path segment reported by a Standard Schema validation issue. */ +type PluginRpcIssuePathSegment = string | number; +/** Validator-neutral validation detail carried by an RPC error envelope. */ +interface PluginRpcValidationIssue { + message: string; + path?: PluginRpcIssuePathSegment[]; +} +/** Stable wire error categories for plugin RPC. */ +type PluginRpcErrorCode = "invalid_json" | "invalid_input" | "handler_error" | "invalid_output" | "non_json_result" | "unknown_method"; +/** Structured RPC failure returned as `{ ok: false, error }`. */ +interface PluginRpcError { + code: PluginRpcErrorCode; + message: string; + issues?: PluginRpcValidationIssue[]; +} +/** + * The validator-neutral subset of Standard Schema v1 used by plugin RPC. + * Zod 4 schemas implement this interface directly; other validators can do + * the same without becoming part of BB's public protocol. + */ +interface StandardSchemaV1 { + readonly "~standard": { + readonly version: 1; + readonly vendor: string; + readonly validate: (value: unknown) => StandardSchemaV1Result | Promise>; + readonly types?: { + readonly input: Input; + readonly output: Output; + }; + }; +} +type StandardSchemaV1Result = { + readonly value: Output; + readonly issues?: undefined; +} | { + readonly issues: readonly StandardSchemaV1Issue[]; +}; +interface StandardSchemaV1Issue { + readonly message: string; + readonly path?: PropertyKey | readonly (PropertyKey | { + readonly key: PropertyKey; + })[]; +} +type StandardSchemaV1InferInput = NonNullable["input"]; +type StandardSchemaV1InferOutput = NonNullable["output"]; +interface PluginRpcMethodContract { + readonly input: InputSchema; + readonly output: OutputSchema; +} +type PluginRpcContract = Readonly>; +/** Define a shared RPC contract while preserving exact method/schema types. */ +declare function defineRpcContract(contract: Contract): Contract; +type PluginRpcHandlers = { + [Method in keyof Contract]: (input: StandardSchemaV1InferOutput) => StandardSchemaV1InferInput | Promise>; +}; +type PluginRpcCallInput = StandardSchemaV1InferInput; +type PluginRpcCallArgs = null extends PluginRpcCallInput ? [input?: PluginRpcCallInput] : [input: PluginRpcCallInput]; +type PluginRpcResult = StandardSchemaV1InferOutput; + +/** + * The `@bb/plugin-sdk/app` contract (plugin design §5.2) — pure types with no + * side effects. The BB app imports these to keep its real implementation in + * sync (`satisfies PluginSdkApp`). Plugin authors import the same shapes through + * `@bb/plugin-sdk/app`. + * + * Per-slot props are versioned contracts: additive-only within an SDK major. + */ +/** Props passed to a `homepageSection` component. */ +interface PluginHomepageSectionProps { + /** Project in view on the compose surface; null when none is selected. */ + projectId: string | null; +} +/** + * Props passed to a `settingsSection` component. + * + * Deliberately empty in V1; versioned additive like the other slot props. + */ +interface PluginSettingsSectionProps { +} +/** Props passed to a `navPanel` component (it owns its whole route). */ +interface PluginNavPanelProps { + /** + * The route remainder after the panel root, "" at the root. The panel's + * route is `/plugins///*`, so a deep link like + * `/plugins/notes/notes/work/ideas.md` renders the panel with + * `subPath: "work/ideas.md"`. Navigate within the panel via + * `useBbNavigate().toPluginPanel(path, { subPath })` — browser + * back/forward then walks panel-internal history. + */ + subPath: string; +} +/** Props passed to a panel tab opened by a `threadPanelAction`. */ +interface PluginThreadPanelProps { + threadId: string; + /** + * The JSON value the action's `openPanel` call passed (round-tripped + * through persistence, so the tab restores across reloads); null when the + * action opened the panel without params. + */ + params: JsonValue$1 | null; +} +interface PluginPendingInteractionView { + id: string; + threadId: string; + title: string; + payload: JsonValue$1; + createdAt: number; + expiresAt: number | null; +} +interface PluginPendingInteractionProps { + interaction: PluginPendingInteractionView; + submit(value: JsonValue$1): Promise; + cancel(): Promise; +} +/** + * Props for a `sidebarFooterAction` — host-rendered (no plugin component). + * Deliberately empty; the registration's `run` carries the behavior. + */ +interface PluginSidebarFooterActionProps { +} +/** + * Where a file being opened by a `fileOpener` lives. `path` semantics follow + * the source: workspace paths are relative to the environment's worktree, + * thread-storage paths are relative to the thread's storage root, host paths + * are absolute on the thread's host. + */ +interface PluginFileOpenerSource { + kind: "workspace" | "host" | "thread-storage"; + threadId: string | null; + environmentId: string | null; + projectId: string | null; +} +/** Props passed to a `fileOpener` component (rendered as a panel file tab). */ +interface PluginFileOpenerProps { + path: string; + source: PluginFileOpenerSource; +} +/** + * Message context passed to a `messageDirective` component — the assistant + * (or nested agent) message that contained the directive. + */ +interface PluginMessageDirectiveMessage { + id: string; + threadId: string; + turnId: string | null; + projectId: string | null; +} +/** + * Open a worktree-relative file in the host's workspace file viewer. Returns + * true when the host accepted the path; false when the path is invalid or the + * viewer declined it. + */ +type PluginMessageDirectiveOpenWorkspaceFile = (path: string) => boolean; +/** + * Props passed to a `messageDirective` component. Attributes are untrusted + * strings parsed from the directive; the plugin validates its own fields. + */ +interface PluginMessageDirectiveProps { + /** Parsed, untrusted directive attributes (e.g. `{ file: "demo.html" }`). */ + attributes: Readonly>; + /** Original directive source text (useful for diagnostics / crash fallback). */ + source: string; + message: PluginMessageDirectiveMessage; + /** + * Opens a worktree-relative file in the host's workspace file viewer. Null + * when the message surface has no workspace viewer available. + */ + openWorkspaceFile: PluginMessageDirectiveOpenWorkspaceFile | null; +} +interface PluginHomepageSectionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + title: string; + component: ComponentType; +} +interface PluginSettingsSectionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Optional host-rendered section heading. */ + title?: string; + /** + * Optional one-line host-rendered subheading under `title`, in the built-in + * SettingsSection idiom (ignored when `title` is absent). + */ + description?: string; + component: ComponentType; +} +interface PluginNavPanelRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon: string; + /** URL segment under `/plugins//`; letters, digits, `-`, `_`. */ + path: string; + component: ComponentType; + /** + * Optional component rendered on the right side of the shared title bar + * (e.g. a sync button or a count). Contained separately from the body: a + * throwing headerContent is hidden without breaking the title bar. + */ + headerContent?: ComponentType; +} +/** Context handed to a `threadPanelAction`'s `run`. */ +interface PluginThreadPanelActionContext { + /** The thread whose panel launcher invoked the action. */ + threadId: string; + /** + * Open a tab in the thread's side panel rendering this action's + * `component`. `title` labels the tab (default: the action's `title`); + * `params` must be JSON-serializable — it is persisted with the tab and + * reaches the component as its `params` prop. Opening with params + * identical to an already-open tab of this action focuses that tab + * (updating its title) instead of duplicating it. May be called more than + * once (different params ⇒ multiple tabs) or not at all. + */ + openPanel(options?: { + title?: string; + params?: JsonValue$1; + }): void; +} +interface PluginThreadPanelActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Label of the action row in the panel's new-tab launcher. */ + title: string; + /** + * Icon hint (BB icon name) used when the plugin ships no logo; the + * launcher row and opened tabs prefer the plugin's logo. + */ + icon?: string; + /** Rendered inside every panel tab this action opens. */ + component: ComponentType; + /** + * How the host frames the tab content. "padded" (default) wraps the + * component in the panel's scroll container with standard padding — + * right for document-like content. "flush" gives the component the full + * tab area (no padding, definite height, no host scrolling) — right for + * app-like content that manages its own layout, such as + * `experimental_ThreadChat`. + */ + layout?: "padded" | "flush"; + /** + * Runs when the user activates the action: call your RPC methods, show a + * toast, and/or open panel tabs via `context.openPanel`. Omitted = + * immediately open a panel tab with defaults. Errors (sync or async) are + * contained and logged; they never break the launcher. + */ + run?(context: PluginThreadPanelActionContext): void | Promise; +} +interface PluginPendingInteractionRegistration { + /** Matches `rendererId` passed to `bb.ui.requestInput`. */ + id: string; + component: ComponentType; +} +/** Context handed to a `sidebarFooterAction`'s `run`. */ +interface PluginSidebarFooterActionContext { + /** + * Navigate to this plugin's Settings detail page + * (`/settings/plugins/`), where declarative settings and + * `settingsSection` slots render. + */ + openSettings(): void; +} +/** + * An icon button in the app sidebar footer (next to Settings / bug report). + * Host-rendered for consistent chrome — plugins supply icon, label, and + * `run` behavior only. + */ +interface PluginSidebarFooterActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip and accessible label for the icon button. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon: string; + /** + * Runs when the user activates the action (e.g. call `openSettings()`, + * open a panel via other surfaces, toast). Errors (sync or async) are + * contained and logged; they never break the sidebar. + */ + run(context: PluginSidebarFooterActionContext): void | Promise; +} +/** + * Register this plugin as a viewer/editor for file extensions. The user + * picks (and can set as default) an opener per extension via the file tab's + * "Open with" menu; matching files opened in the panel then render + * `component` in a plugin tab instead of the built-in preview. Applies to + * working-tree, host, and thread-storage files — never to git-ref snapshots + * (diff views always use the built-in preview). The built-in preview stays + * one menu click away, and a missing/disabled opener falls back to it. + */ +interface PluginFileOpenerRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Label in the "Open with" menu (e.g. "Notes editor"). */ + title: string; + /** Lowercase extensions without the dot (e.g. ["md", "mdx"]). */ + extensions: readonly string[]; + component: ComponentType; +} +/** + * Register a leaf message directive rendered inside assistant (and nested + * agent) message Markdown. `id` is the directive name: `inline-vis` matches + * `::inline-vis{file="demo.html"}`. + */ +interface PluginMessageDirectiveRegistration { + /** + * The directive name. Lowercase kebab-case beginning with a letter. + */ + id: string; + component: ComponentType; +} +/** + * A narrow, stable reference to one rendered chat message — NOT an internal + * timeline row. `sourceSeqEnd` is the last source event sequence the message + * covers, the anchor the server accepts for provider-history forks. + */ +interface ThreadChatMessageReference { + id: string; + threadId: string; + role: "user" | "assistant"; + /** Visible text of the message. */ + text: string; + sourceSeqEnd: number; +} +interface PluginMessageActionThreadPanelOptions { + /** A `threadPanelAction` id registered by this same plugin. */ + actionId: string; + title?: string; + params?: JsonValue$1; +} +/** Context handed to a `messageAction`'s `run`. */ +interface PluginMessageActionContext { + /** The thread whose timeline surfaced the action. */ + threadId: string; + message: ThreadChatMessageReference; + /** + * Present only when the action was invoked from the text-selection menu; + * the exact text the user highlighted inside `message`. + */ + selectedText?: string; + /** + * Open one of this plugin's `threadPanelAction` components in the current + * thread's side panel — the registration-callback equivalent of + * `useBbNavigate().experimental_openThreadPanel`. Returns true when the host + * accepted (the action id exists and the surface has a panel); false + * otherwise. + */ + openPanel(options: PluginMessageActionThreadPanelOptions): boolean; +} +/** + * An action on chat messages: an icon button in the per-message action bar + * (user and assistant messages) and an entry in the assistant-message + * text-selection menu. Host-rendered chrome — the plugin supplies title, + * icon hint, and `run` behavior only. + */ +interface PluginMessageActionRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip / menu label for the action. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon?: string; + /** + * Runs when the user activates the action. Errors (sync or async) are + * contained and logged; they never break the timeline. + */ + run(context: PluginMessageActionContext): void | Promise; +} +interface PluginAppSlots { + homepageSection(registration: PluginHomepageSectionRegistration): void; + settingsSection(registration: PluginSettingsSectionRegistration): void; + navPanel(registration: PluginNavPanelRegistration): void; + threadPanelAction(registration: PluginThreadPanelActionRegistration): void; + pendingInteraction(registration: PluginPendingInteractionRegistration): void; + sidebarFooterAction(registration: PluginSidebarFooterActionRegistration): void; + fileOpener(registration: PluginFileOpenerRegistration): void; + messageDirective(registration: PluginMessageDirectiveRegistration): void; + experimental_messageAction(registration: PluginMessageActionRegistration): void; +} +interface PluginAppComposer { + customize(registration: ComposerCustomization): void; +} +/** Stable lifecycle values for one content-script instance in one bb client. */ +interface PluginContentScriptContext { + /** The id of the plugin that owns this script. */ + readonly pluginId: string; + /** Monotonic per-client generation, starting at 1. */ + readonly generation: number; + /** Aborted before cleanup begins on replacement, deactivation, or teardown. */ + readonly signal: AbortSignal; +} +/** Cleanup returned by a frontend content script. */ +type PluginContentScriptDisposer = () => void | Promise; +/** + * Trusted same-origin JavaScript/TypeScript mounted once per active frontend + * generation in each bb app window or browser tab. + */ +interface PluginContentScriptRegistration { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** + * Install behavior into the bb app shell. The host awaits a returned + * promise, contains failures, and calls the returned disposer exactly once. + */ + mount(context: PluginContentScriptContext): void | PluginContentScriptDisposer | Promise; +} +/** Experimental lifecycle surface for trusted frontend content scripts. */ +interface PluginAppContentScripts { + register(registration: PluginContentScriptRegistration): void; +} +interface PluginAppBuilder { + slots: PluginAppSlots; + composer: PluginAppComposer; + experimental_contentScripts: PluginAppContentScripts; +} +type PluginAppSetup = (app: PluginAppBuilder) => void; +/** + * The opaque product of `definePluginApp` — a plugin's `app.tsx` default + * export. The host re-runs `setup` against a fresh collector on every + * (re)interpretation, replacing that plugin's registrations wholesale. + */ +interface PluginAppDefinition { + /** Brand the host checks before interpreting a bundle's default export. */ + readonly __bbPluginApp: true; + readonly setup: PluginAppSetup; +} +interface PluginRpcClient { + /** + * Invoke one of the plugin's `bb.rpc` methods (POST + * /api/v1/plugins/<id>/rpc/<method>). Resolves with the method's + * inferred output; rejects with an `Error` carrying the server's message, + * stable `code`, and validation `issues` when present. + */ + call>(method: Method, ...args: PluginRpcCallArgs): Promise>; +} +interface PluginSettingsState { + /** + * Effective non-secret setting values (secret settings are excluded — + * read them server-side). Undefined while loading or unavailable. + */ + values: Record | undefined; + isLoading: boolean; +} +/** State of the app's shared realtime connection to the bb server. */ +type PluginRealtimeConnectionState = "connecting" | "connected" | "reconnecting"; +/** Where `useComposer()` writes. */ +type PluginComposerScope = { + kind: "thread"; + threadId: string; +} | { + kind: "queued-message"; + threadId: string; + queuedMessageId: string; +} | { + kind: "side-chat"; + projectId: string; + parentThreadId: string; + tabId: string; + childThreadId: string | null; +} | { + kind: "new-thread"; + /** Root compose's effective selected project; null only while unresolved. */ + projectId: string | null; +}; +/** One plugin-owned composer customization registration. */ +interface ComposerCustomization { + /** Unique within the plugin; letters, digits, `-`, `_`. */ + id: string; + /** Composer kinds where this customization is active; omit for all kinds. */ + scopes?: readonly PluginComposerScope["kind"][]; + actions?: readonly { + id: string; + component: ComponentType; + }[]; + banners?: readonly { + id: string; + /** Host chrome around the banner. Defaults to `"card"`. */ + chrome?: "card" | "bare"; + component: ComponentType; + }[]; + plusMenu?: readonly ComposerPlusMenuItem[]; + richText?: ComposerRichTextSpec; +} +/** Host-rendered menu row in the composer's `+` menu. */ +interface ComposerPlusMenuItem { + id: string; + label: string; + /** BB icon name; unknown names fall back to the generic plugin icon. */ + icon?: string; + /** Accessible description for the host-rendered row. */ + description?: string; + disabled?: boolean | ((view: ComposerView) => boolean); + run(context: { + composer: PluginComposerApi; + view: ComposerView; + }): void | Promise; +} +/** Reactive read-side of the composer a plugin surface is mounted in. */ +interface ComposerView { + scope: PluginComposerScope; + layout: "expanded" | "compact" | "zen"; + draft: { + text: string; + isEmpty: boolean; + attachmentCount: number; + }; + run: { + isRunning: boolean; + isSubmitting: boolean; + }; +} +interface ComposerRichTextSpec { + /** Content-derived paint: match ranges receive `className`; text is never mutated. */ + effects?: readonly { + id: string; + /** Plain-text offsets into the current structured draft. */ + match(text: string): readonly { + from: number; + to: number; + }[]; + className: string; + }[]; + /** Debounced, read-only observation of the structured draft. */ + onDraftChange?(draft: ComposerStructuredDraft, view: ComposerView): void; +} +interface ComposerStructuredDraft { + text: string; + mentions: readonly { + from: number; + to: number; + provider: string; + id: string; + label: string; + }[]; +} +/** Host-rendered paint applied to the editable composer text. */ +interface PluginComposerTextEffect { + className: string; +} +/** Host-rendered status that temporarily replaces a thread's draft glyph. */ +interface PluginComposerThreadRowStatus { + /** BB icon-name hint; unknown names fall back to the generic plugin icon. */ + icon: string; + /** Accessible label for the status glyph. */ + label: string; + /** Semantic host color for the status glyph. Defaults to the neutral tone. */ + tone?: "default" | "success"; +} +/** An @-mention pill bound to one of the calling plugin's mention providers. */ +interface PluginComposerMention { + /** Mention provider id registered by THIS plugin via `bb.ui.registerMentionProvider`. */ + provider: string; + /** Item id your provider's `resolve` will receive at send time. */ + id: string; + /** Pill text shown in the composer. */ + label: string; +} +/** + * Programmatic access to the chat composer draft — the same shared draft the + * built-in "Add to chat" affordances (file preview, diff, terminal selections) + * write to. While a queued message is being edited, writes land in that + * message's inline editor. In a side chat, writes land in the visible side-chat + * draft. Otherwise, inside a thread context writes land in that thread's draft; + * anywhere else (nav panel, homepage section) they seed the new-thread composer + * draft, which persists until the user sends or clears it. + */ +interface PluginComposerApi { + scope: PluginComposerScope; + /** Current plain text for this composer scope. */ + readonly text: string; + /** + * Replace the draft's plain text. Attachments are preserved. Inline mentions + * outside the changed range are preserved and rebased; mentions overlapped + * by the replacement are removed because their text representation changed. + */ + setText(next: string): void; + /** + * Replace the draft's plain text from the latest committed value. Uses the + * same structured-state reconciliation as `setText`. + */ + updateText(updater: (current: string) => string): void; + /** Clear plain text without clearing independently attached files. */ + clear(): void; + /** + * Apply a host-rendered effect to this composer's editable text, or clear it. + * Effects are scoped to the calling plugin and automatically clear when the + * slot unmounts or its composer scope changes. + */ + setTextEffect(effect: PluginComposerTextEffect | null): void; + /** + * Lock or unlock editing for this composer. Locks are scoped to the calling + * plugin and automatically release when the slot unmounts or its composer + * scope changes. + */ + setInputLock(locked: boolean): void; + /** + * Replace this composer's thread-row draft glyph with a host-rendered status, + * or clear it. New-thread composers have no row, so calls are a no-op. + * Side-chat and queued side-chat scopes decorate the visible parent-thread + * row. Status is scoped to the calling plugin and automatically clears when + * the slot unmounts or its composer scope changes. + */ + setThreadRowStatus(status: PluginComposerThreadRowStatus | null): void; + /** + * Append text to the draft as a `> ` blockquote block and focus the + * composer. Blank text is a no-op. This is the "reference this selection + * in chat" primitive. + */ + addQuote(text: string): void; + /** + * Insert an @-mention pill that resolves through this plugin's mention + * provider at send time — the durable way to reference an entity whose + * content should be fetched fresh when the message is sent. + */ + insertMention(mention: PluginComposerMention): void; + /** Focus the composer caret at the end of the draft. */ + focus(): void; +} +/** + * A consumer-supplied action on the messages of one `ThreadChat` instance, + * rendered in the embedded timeline's per-message action bar alongside the + * native and slot-registered actions. Unlike the `messageAction` slot this is + * scoped to the rendering component, not registered globally. + */ +interface ThreadChatMessageAction { + /** Unique within this ThreadChat instance; letters, digits, `-`, `_`. */ + id: string; + /** Tooltip / menu label for the action. */ + title: string; + /** Icon hint (BB icon name); unknown names fall back to a generic icon. */ + icon?: string; + /** + * Message roles the action applies to. Omitted = both user and assistant + * messages. + */ + roles?: readonly ("user" | "assistant")[]; + /** + * Runs when the user activates the action. Errors (sync or async) are + * contained and logged; they never break the timeline. + */ + run(message: ThreadChatMessageReference): void | Promise; +} +/** + * Props of the host-owned `ThreadChat` component — one thread's chat + * (timeline, and for the composer variants the full send/queue/draft + * engine), rendered by the BB app inside a plugin slot. This is the + * deliberate exception to the no-host-components rule (§5.5): a stable + * product capability, not a UI kit. Versioned additive like slot props; + * internal timeline rows, query hooks, and prompt-box configuration are + * deliberately not exposed. + */ +interface ThreadChatProps { + threadId: string; + /** + * "full" (default) is the page presentation (centered reading width); + * "compact" is the side-panel presentation; "timeline" renders the + * transcript without a composer. + */ + variant?: "full" | "compact" | "timeline"; + /** + * "contained" (default) fills and scrolls inside a bounded parent; + * "document" grows with its content and defers scrolling to the page. + */ + layout?: "contained" | "document"; + /** Bump to focus the composer (ignored by `variant: "timeline"`). */ + focusRequest?: number; + className?: string; + /** Rendered above the conversation, scrolling with it. */ + leadingContent?: ReactNode; + /** + * Actions rendered in this instance's per-message action bar (see + * {@link ThreadChatMessageAction}). + */ + messageActions?: readonly ThreadChatMessageAction[]; +} +/** + * Props of the host-owned `Markdown` component — bb's chat message renderer + * (the same typography, spacing, and code styling as timeline messages). + * Use it wherever plugin UI quotes or previews message content so it reads + * like the rest of the chat. Like `ThreadChat`, this is a stable product + * capability, not a UI kit; renderer internals stay private. + */ +interface MarkdownProps { + /** Markdown source, rendered exactly like a chat message body. */ + content: string; + className?: string; +} +/** Current app selection, derived from the route. */ +interface BbContext { + projectId: string | null; + threadId: string | null; +} +interface BbNavigate { + toThread(threadId: string): void; + toProject(projectId: string): void; + /** + * Navigate to one of this plugin's own nav panels by its `path`. + * `subPath` targets a location inside the panel (the component's + * `subPath` prop); `replace` swaps the current history entry instead of + * pushing — use it for redirects so back does not bounce. + */ + toPluginPanel(path: string, options?: { + subPath?: string; + replace?: boolean; + }): void; + /** + * Navigate to the root compose surface (the new-thread screen). Pass + * `initialPrompt` to seed the composer draft and `focusPrompt` to focus the + * composer on arrival — the pairing behind "Create via chat" style entry + * points that drop the user into chat with a prefilled prompt. + */ + toCompose(options?: { + initialPrompt?: string; + focusPrompt?: boolean; + }): void; + /** + * Open one of this plugin's registered thread-panel actions in the current + * thread surface. Returns false when the surface has no thread side panel or + * the action is unavailable. + */ + experimental_openThreadPanel(options: { + actionId: string; + title?: string; + params?: JsonValue$1; + }): boolean; +} +/** + * Everything `@bb/plugin-sdk/app` resolves to at runtime. The BB app builds + * the real implementation and `satisfies` this interface; `bb plugin build` + * shims the specifier to that object on `globalThis.__bbPluginRuntime`. + */ +interface PluginSdkApp { + definePluginApp(setup: PluginAppSetup): PluginAppDefinition; + useRpc(): PluginRpcClient; + useRealtime(channel: string, handler: (payload: unknown) => void): void; + /** + * Observe the same shared connection that delivers `useRealtime` signals. + * Use a subsequent transition to `connected` to reconcile server state that + * may have changed while ephemeral signals could not be delivered. The first + * connection can transition from `connecting` and is not a reconnection. + */ + useRealtimeConnectionState(): PluginRealtimeConnectionState; + useSettings(): PluginSettingsState; + useBbContext(): BbContext; + useBbNavigate(): BbNavigate; + useComposer(): PluginComposerApi; + /** + * The host-owned chat component (see {@link ThreadChatProps}). Together + * with `Markdown`, the only components the SDK ships — everything else + * stays vendored per §5.5. + */ + experimental_ThreadChat: ComponentType; + /** + * The host-owned chat-message markdown renderer (see + * {@link MarkdownProps}). + */ + experimental_Markdown: ComponentType; + useComposerView(): ComposerView; +} + +/** + * App-wide server-backed preferences. + * Client-local settings stay in the frontend localStorage helpers instead. + */ +declare const appSettingsSchema: z$1.ZodObject<{ + caffeinate: z$1.ZodBoolean; + showKeyboardHints: z$1.ZodBoolean; + showUnhandledProviderEvents: z$1.ZodBoolean; + codexMemoryEnabled: z$1.ZodBoolean; + claudeCodeMemoryEnabled: z$1.ZodBoolean; + codexSubagentsDisabled: z$1.ZodBoolean; + claudeCodeSubagentsDisabled: z$1.ZodBoolean; + claudeCodeWorkflowsDisabled: z$1.ZodBoolean; +}, z$1.core.$strict>; +type AppSettings = z$1.infer; + +declare const appKeybindingOverridesSchema: z$1.ZodArray; + shortcut: z$1.ZodNullable>; +}, z$1.core.$strict>>; +type AppKeybindingOverrides = z$1.infer; + +declare const appThemeSchema: z$1.ZodObject<{ + themeId: z$1.ZodString; + customCss: z$1.ZodNullable; + faviconColor: z$1.ZodEnum<{ + default: "default"; + red: "red"; + orange: "orange"; + yellow: "yellow"; + green: "green"; + teal: "teal"; + blue: "blue"; + purple: "purple"; + pink: "pink"; + }>; +}, z$1.core.$strip>; +type AppTheme = z$1.infer; +/** + * The complete appearance selection a client sends when changing the palette + * and/or favicon tint. The server validates `themeId` (built-in id or an + * existing custom theme) and resolves the CSS from disk for custom themes. + * Callers changing only one facet must carry the other facet forward explicitly. + */ +declare const appThemeSelectionSchema: z$1.ZodObject<{ + themeId: z$1.ZodString; + faviconColor: z$1.ZodEnum<{ + default: "default"; + red: "red"; + orange: "orange"; + yellow: "yellow"; + green: "green"; + teal: "teal"; + blue: "blue"; + purple: "purple"; + pink: "pink"; + }>; +}, z$1.core.$strip>; +type AppThemeSelection = z$1.infer; + +declare const changedMessageSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"changed">; + entity: z$1.ZodLiteral<"thread">; + id: z$1.ZodOptional; + metadata: z$1.ZodOptional; + eventTypes: z$1.ZodOptional>>>>; + hasPendingInteraction: z$1.ZodOptional; + projectId: z$1.ZodOptional; + }, z$1.core.$strict>>; + changes: z$1.ZodReadonly>>; +}, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"changed">; + entity: z$1.ZodLiteral<"project">; + id: z$1.ZodOptional; + changes: z$1.ZodReadonly>>; +}, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"changed">; + entity: z$1.ZodLiteral<"environment">; + id: z$1.ZodOptional; + changes: z$1.ZodReadonly>>; +}, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"changed">; + entity: z$1.ZodLiteral<"host">; + id: z$1.ZodOptional; + changes: z$1.ZodReadonly>>; +}, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"changed">; + entity: z$1.ZodLiteral<"system">; + changes: z$1.ZodReadonly>>; +}, z$1.core.$strict>], "entity">; +type ChangedMessage = z$1.infer; + +declare const environmentSchema: z$1.ZodObject<{ + id: z$1.ZodString; + name: z$1.ZodNullable; + projectId: z$1.ZodString; + hostId: z$1.ZodString; + path: z$1.ZodNullable; + managed: z$1.ZodBoolean; + isGitRepo: z$1.ZodBoolean; + isWorktree: z$1.ZodBoolean; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + branchName: z$1.ZodNullable; + baseBranch: z$1.ZodNullable; + defaultBranch: z$1.ZodNullable; + mergeBaseBranch: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + ready: "ready"; + retiring: "retiring"; + destroying: "destroying"; + destroyed: "destroyed"; + }>; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; +}, z$1.core.$strip>; +type Environment = z$1.infer; + +/** + * User-opt-in experiments (the Settings → Experiments toggles). Distinct from + * `FeatureFlags`: flags are operator-set via env at server start, experiments + * are user-toggled at runtime and persisted server-side so server-owned + * policy (e.g. skill injection) can honor them. + * + * Every experiment defaults to off — opting in is the point. + */ +declare const experimentsSchema: z$1.ZodObject<{ + claudeCodeMockCliTraffic: z$1.ZodBoolean; + plugins: z$1.ZodBoolean; + sideChatPlugin: z$1.ZodBoolean; +}, z$1.core.$strip>; +type Experiments = z$1.infer; + +declare const hostSchema: z$1.ZodObject<{ + id: z$1.ZodString; + name: z$1.ZodString; + type: z$1.ZodEnum<{ + persistent: "persistent"; + }>; + status: z$1.ZodEnum<{ + connected: "connected"; + disconnected: "disconnected"; + }>; + lastSeenAt: z$1.ZodNullable; + lastRejectedProtocolVersion: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; +}, z$1.core.$strip>; +type Host = z$1.infer; + +interface JsonObject { + [key: string]: JsonValue; +} +type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject; + +declare const pendingInteractionResolutionSchema: z$1.ZodUnion; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"allow_for_session">; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"deny">; +}, z$1.core.$strip>], "decision">, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_answer">; + answers: z$1.ZodRecord; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin_submitted">; +}, z$1.core.$strip>]>; +type PendingInteractionResolution = z$1.infer; +declare const providerPendingInteractionSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + resolved: "resolved"; + }>; + statusReason: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + expiresAt: z$1.ZodOptional>; + resolvedAt: z$1.ZodNullable; + turnId: z$1.ZodString; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerRequestId: z$1.ZodString; + origin: z$1.ZodOptional; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerRequestId: z$1.ZodString; + }, z$1.core.$strip>>; + payload: z$1.ZodUnion; + subject: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + itemId: z$1.ZodString; + command: z$1.ZodString; + cwd: z$1.ZodNullable; + actions: z$1.ZodArray; + command: z$1.ZodString; + name: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"listFiles">; + command: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"search">; + command: z$1.ZodString; + query: z$1.ZodNullable; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"unknown">; + command: z$1.ZodString; + }, z$1.core.$strip>], "type">>; + sessionGrant: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"file_change">; + itemId: z$1.ZodString; + writeScope: z$1.ZodNullable; + sessionGrant: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"permission_grant">; + itemId: z$1.ZodString; + toolName: z$1.ZodNullable; + permissions: z$1.ZodObject<{ + network: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>; + }, z$1.core.$strip>], "kind">; + reason: z$1.ZodNullable; + availableDecisions: z$1.ZodArray>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_question">; + questions: z$1.ZodArray; + multiSelect: z$1.ZodBoolean; + options: z$1.ZodOptional; + }, z$1.core.$strip>>>; + allowFreeText: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>]>; + resolution: z$1.ZodNullable; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"allow_for_session">; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"deny">; + }, z$1.core.$strip>], "decision">, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_answer">; + answers: z$1.ZodRecord; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>]>>; +}, z$1.core.$strip>; +type ProviderPendingInteraction = z$1.infer; +declare const pluginPendingInteractionSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + resolved: "resolved"; + }>; + statusReason: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + expiresAt: z$1.ZodOptional>; + resolvedAt: z$1.ZodNullable; + turnId: z$1.ZodNullable; + origin: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + rendererId: z$1.ZodString; + }, z$1.core.$strip>; + payload: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + title: z$1.ZodString; + data: z$1.ZodType>; + }, z$1.core.$strip>; + resolution: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type PluginPendingInteraction = z$1.infer; +type PendingInteraction = ProviderPendingInteraction | PluginPendingInteraction; + +declare const projectSourceSchema: z$1.ZodObject<{ + id: z$1.ZodString; + projectId: z$1.ZodString; + isDefault: z$1.ZodBoolean; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + type: z$1.ZodLiteral<"local_path">; + hostId: z$1.ZodString; + path: z$1.ZodString; +}, z$1.core.$strip>; +type ProjectSource = z$1.infer; + +declare const resolvedThreadExecutionOptionsSchema: z$1.ZodObject<{ + seq: z$1.ZodOptional; + model: z$1.ZodString; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + full: "full"; + auto: "auto"; + "accept-edits": "accept-edits"; + }>; + source: z$1.ZodEnum<{ + "client/thread/start": "client/thread/start"; + "client/turn/requested": "client/turn/requested"; + "client/turn/start": "client/turn/start"; + }>; +}, z$1.core.$strip>; +type ResolvedThreadExecutionOptions = z$1.infer; +declare const projectExecutionDefaultsSchema: z$1.ZodObject<{ + providerId: z$1.ZodString; + model: z$1.ZodString; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + full: "full"; + auto: "auto"; + "accept-edits": "accept-edits"; + }>; +}, z$1.core.$strip>; +type ProjectExecutionDefaults = z$1.infer; + +/** All thread events — provider-originated or system-originated. */ +declare const threadEventSchema: z$1.ZodPipe; + threadId: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/identity">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"turn/started">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"turn/completed">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + error: z$1.ZodOptional>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"turn/input/accepted">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + clientRequestId: z$1.ZodString; + scope: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"turn">; + turnId: z$1.ZodString; + }, z$1.core.$strip>], "kind">; +}, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/name/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + threadName: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/compacted">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/goal/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + objective: z$1.ZodString; + status: z$1.ZodEnum<{ + paused: "paused"; + active: "active"; + budgetLimited: "budgetLimited"; + complete: "complete"; + }>; + tokenBudget: z$1.ZodNullable; + tokensUsed: z$1.ZodNumber; + timeUsedSeconds: z$1.ZodNumber; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/goal/cleared">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/started">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + item: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"userMessage">; + id: z$1.ZodString; + content: z$1.ZodArray; + text: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + }, z$1.core.$strip>], "type">>; + clientRequestId: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"agentMessage">; + id: z$1.ZodString; + text: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commandExecution">; + id: z$1.ZodString; + command: z$1.ZodString; + cwd: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + approvalStatus: z$1.ZodNullable>; + aggregatedOutput: z$1.ZodOptional; + exitCode: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + truncation: z$1.ZodOptional>; + result: z$1.ZodOptional>; + resultText: z$1.ZodOptional>; + }, z$1.core.$strip>>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"fileChange">; + id: z$1.ZodString; + changes: z$1.ZodArray; + movePath: z$1.ZodOptional; + diff: z$1.ZodOptional; + }, z$1.core.$strip>>; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + approvalStatus: z$1.ZodNullable>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"webSearch">; + id: z$1.ZodString; + queries: z$1.ZodArray; + resultText: z$1.ZodNullable; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"webFetch">; + id: z$1.ZodString; + url: z$1.ZodString; + prompt: z$1.ZodNullable; + pattern: z$1.ZodNullable; + resultText: z$1.ZodNullable; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"imageView">; + id: z$1.ZodString; + path: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"toolCall">; + id: z$1.ZodString; + server: z$1.ZodOptional; + tool: z$1.ZodString; + arguments: z$1.ZodOptional>; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + result: z$1.ZodOptional; + error: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + truncation: z$1.ZodOptional>; + result: z$1.ZodOptional>; + resultText: z$1.ZodOptional>; + }, z$1.core.$strip>>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"reasoning">; + id: z$1.ZodString; + summary: z$1.ZodArray; + content: z$1.ZodArray; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"plan">; + id: z$1.ZodString; + text: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"contextCompaction">; + id: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"backgroundTask">; + id: z$1.ZodString; + taskType: z$1.ZodString; + description: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + running: "running"; + paused: "paused"; + completed: "completed"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + skipTranscript: z$1.ZodBoolean; + workflowName: z$1.ZodOptional; + workflow: z$1.ZodOptional; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodOptional>; + summary: z$1.ZodOptional; + error: z$1.ZodOptional; + outputFile: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>], "type">; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/completed">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + item: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"userMessage">; + id: z$1.ZodString; + content: z$1.ZodArray; + text: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + }, z$1.core.$strip>], "type">>; + clientRequestId: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"agentMessage">; + id: z$1.ZodString; + text: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commandExecution">; + id: z$1.ZodString; + command: z$1.ZodString; + cwd: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + approvalStatus: z$1.ZodNullable>; + aggregatedOutput: z$1.ZodOptional; + exitCode: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + truncation: z$1.ZodOptional>; + result: z$1.ZodOptional>; + resultText: z$1.ZodOptional>; + }, z$1.core.$strip>>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"fileChange">; + id: z$1.ZodString; + changes: z$1.ZodArray; + movePath: z$1.ZodOptional; + diff: z$1.ZodOptional; + }, z$1.core.$strip>>; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + approvalStatus: z$1.ZodNullable>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"webSearch">; + id: z$1.ZodString; + queries: z$1.ZodArray; + resultText: z$1.ZodNullable; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"webFetch">; + id: z$1.ZodString; + url: z$1.ZodString; + prompt: z$1.ZodNullable; + pattern: z$1.ZodNullable; + resultText: z$1.ZodNullable; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"imageView">; + id: z$1.ZodString; + path: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"toolCall">; + id: z$1.ZodString; + server: z$1.ZodOptional; + tool: z$1.ZodString; + arguments: z$1.ZodOptional>; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + result: z$1.ZodOptional; + error: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + truncation: z$1.ZodOptional>; + result: z$1.ZodOptional>; + resultText: z$1.ZodOptional>; + }, z$1.core.$strip>>; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"reasoning">; + id: z$1.ZodString; + summary: z$1.ZodArray; + content: z$1.ZodArray; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"plan">; + id: z$1.ZodString; + text: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"contextCompaction">; + id: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"backgroundTask">; + id: z$1.ZodString; + taskType: z$1.ZodString; + description: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + running: "running"; + paused: "paused"; + completed: "completed"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + skipTranscript: z$1.ZodBoolean; + workflowName: z$1.ZodOptional; + workflow: z$1.ZodOptional; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodOptional>; + summary: z$1.ZodOptional; + error: z$1.ZodOptional; + outputFile: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>], "type">; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/agentMessage/delta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/commandExecution/outputDelta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + reset: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/fileChange/outputDelta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/reasoning/summaryTextDelta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/reasoning/textDelta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/plan/delta">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + delta: z$1.ZodString; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/mcpToolCall/progress">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + message: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/toolCall/progress">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + itemId: z$1.ZodString; + message: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/backgroundTask/progress">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + item: z$1.ZodObject<{ + type: z$1.ZodLiteral<"backgroundTask">; + id: z$1.ZodString; + taskType: z$1.ZodString; + description: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + running: "running"; + paused: "paused"; + completed: "completed"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + skipTranscript: z$1.ZodBoolean; + workflowName: z$1.ZodOptional; + workflow: z$1.ZodOptional; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodOptional>; + summary: z$1.ZodOptional; + error: z$1.ZodOptional; + outputFile: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"item/backgroundTask/completed">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + item: z$1.ZodObject<{ + type: z$1.ZodLiteral<"backgroundTask">; + id: z$1.ZodString; + taskType: z$1.ZodString; + description: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + failed: "failed"; + interrupted: "interrupted"; + }>; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + running: "running"; + paused: "paused"; + completed: "completed"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + skipTranscript: z$1.ZodBoolean; + workflowName: z$1.ZodOptional; + workflow: z$1.ZodOptional; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodOptional>; + summary: z$1.ZodOptional; + error: z$1.ZodOptional; + outputFile: z$1.ZodOptional; + parentToolCallId: z$1.ZodOptional; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/tokenUsage/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + tokenUsage: z$1.ZodObject<{ + total: z$1.ZodObject<{ + totalTokens: z$1.ZodNumber; + inputTokens: z$1.ZodNumber; + cachedInputTokens: z$1.ZodNumber; + outputTokens: z$1.ZodNumber; + reasoningOutputTokens: z$1.ZodNumber; + }, z$1.core.$strip>; + last: z$1.ZodObject<{ + totalTokens: z$1.ZodNumber; + inputTokens: z$1.ZodNumber; + cachedInputTokens: z$1.ZodNumber; + outputTokens: z$1.ZodNumber; + reasoningOutputTokens: z$1.ZodNumber; + }, z$1.core.$strip>; + modelContextWindow: z$1.ZodNullable; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"thread/contextWindowUsage/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + contextWindowUsage: z$1.ZodObject<{ + usedTokens: z$1.ZodNullable; + modelContextWindow: z$1.ZodNullable; + estimated: z$1.ZodBoolean; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"turn/plan/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + plan: z$1.ZodArray>; + }, z$1.core.$strip>>; + explanation: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"turn/diff/updated">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + diff: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider/error">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + message: z$1.ZodString; + detail: z$1.ZodOptional; + willRetry: z$1.ZodOptional; + errorInfo: z$1.ZodOptional; + providerCode: z$1.ZodNullable; + httpStatusCode: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider/warning">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + category: z$1.ZodEnum<{ + deprecation: "deprecation"; + config: "config"; + general: "general"; + }>; + summary: z$1.ZodOptional; + details: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider/modelFallback">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + originalModel: z$1.ZodString; + fallbackModel: z$1.ZodString; + reason: z$1.ZodEnum<{ + refusal: "refusal"; + provider: "provider"; + }>; + message: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider/unhandled">; + threadId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerId: z$1.ZodString; + rawType: z$1.ZodString; + rawEvent: z$1.ZodObject<{ + jsonrpc: z$1.ZodLiteral<"2.0">; + id: z$1.ZodOptional>; + method: z$1.ZodString; + params: z$1.ZodOptional>>; + }, z$1.core.$strip>; + parentToolCallId: z$1.ZodOptional; +}, z$1.core.$strip>], "type">, z$1.ZodObject<{ + scope: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"turn">; + turnId: z$1.ZodString; + }, z$1.core.$strip>], "kind">; +}, z$1.core.$strip>>, z$1.ZodIntersection; + threadId: z$1.ZodString; + direction: z$1.ZodLiteral<"outbound">; + source: z$1.ZodEnum<{ + spawn: "spawn"; + tell: "tell"; + }>; + initiator: z$1.ZodEnum<{ + system: "system"; + user: "user"; + agent: "agent"; + }>; + request: z$1.ZodObject<{ + method: z$1.ZodEnum<{ + "thread/start": "thread/start"; + "turn/start": "turn/start"; + }>; + params: z$1.ZodRecord; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"client/turn/requested">; + threadId: z$1.ZodString; + direction: z$1.ZodLiteral<"outbound">; + requestId: z$1.ZodString; + source: z$1.ZodEnum<{ + spawn: "spawn"; + tell: "tell"; + }>; + initiator: z$1.ZodEnum<{ + system: "system"; + user: "user"; + agent: "agent"; + }>; + senderThreadId: z$1.ZodNullable; + systemMessageKind: z$1.ZodOptional>; + systemMessageSubject: z$1.ZodOptional; + threadId: z$1.ZodString; + threadName: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread-batch">; + count: z$1.ZodNumber; + }, z$1.core.$strip>], "kind">>>; + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + inputGroups: z$1.ZodOptional>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>>>; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread-start">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"new-turn">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"auto">; + expectedTurnId: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"steer">; + expectedTurnId: z$1.ZodNullable; + }, z$1.core.$strip>], "kind">; + request: z$1.ZodObject<{ + method: z$1.ZodEnum<{ + "thread/start": "thread/start"; + "turn/start": "turn/start"; + }>; + params: z$1.ZodRecord; + }, z$1.core.$strip>; + execution: z$1.ZodObject<{ + seq: z$1.ZodOptional; + model: z$1.ZodString; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + source: z$1.ZodEnum<{ + "client/thread/start": "client/thread/start"; + "client/turn/requested": "client/turn/requested"; + "client/turn/start": "client/turn/start"; + }>; + permissionMode: z$1.ZodEnum<{ + readonly: "readonly"; + full: "full"; + auto: "auto"; + "accept-edits": "accept-edits"; + "workspace-write": "workspace-write"; + }>; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"client/turn/start">; + threadId: z$1.ZodString; + direction: z$1.ZodLiteral<"outbound">; + source: z$1.ZodEnum<{ + spawn: "spawn"; + tell: "tell"; + }>; + initiator: z$1.ZodEnum<{ + system: "system"; + user: "user"; + agent: "agent"; + }>; + request: z$1.ZodObject<{ + method: z$1.ZodEnum<{ + "thread/start": "thread/start"; + "turn/start": "turn/start"; + }>; + params: z$1.ZodRecord; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/error">; + threadId: z$1.ZodString; + code: z$1.ZodOptional; + message: z$1.ZodString; + detail: z$1.ZodOptional; + reconnectAttempt: z$1.ZodOptional; + reconnectTotal: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/manager/user_message">; + threadId: z$1.ZodString; + text: z$1.ZodString; + toolCallId: z$1.ZodOptional; + turnId: z$1.ZodOptional; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/thread/interrupted">; + threadId: z$1.ZodString; + reason: z$1.ZodEnum<{ + "manual-stop": "manual-stop"; + "host-daemon-restarted": "host-daemon-restarted"; + "provider-turn-idle": "provider-turn-idle"; + }>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/operation">; + threadId: z$1.ZodString; + operation: z$1.ZodString; + status: z$1.ZodString; + message: z$1.ZodString; + operationId: z$1.ZodString; + metadata: z$1.ZodOptional>>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/permissionGrant/lifecycle">; + threadId: z$1.ZodString; + interactionId: z$1.ZodString; + providerId: z$1.ZodString; + providerRequestId: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + resolved: "resolved"; + }>; + resolution: z$1.ZodDefault; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"allow_for_session">; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"deny">; + }, z$1.core.$strip>], "decision">>>; + statusReason: z$1.ZodDefault>; + subject: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"permission_grant">; + itemId: z$1.ZodString; + toolName: z$1.ZodNullable; + permissions: z$1.ZodObject<{ + network: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/userQuestion/lifecycle">; + threadId: z$1.ZodString; + interactionId: z$1.ZodString; + providerId: z$1.ZodString; + providerRequestId: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + resolved: "resolved"; + }>; + resolution: z$1.ZodDefault; + answers: z$1.ZodRecord; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>>; + statusReason: z$1.ZodDefault>; + payload: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_question">; + questions: z$1.ZodArray; + multiSelect: z$1.ZodBoolean; + options: z$1.ZodOptional; + }, z$1.core.$strip>>>; + allowFreeText: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/thread-provisioning">; + threadId: z$1.ZodString; + provisioningId: z$1.ZodString; + status: z$1.ZodEnum<{ + completed: "completed"; + failed: "failed"; + active: "active"; + cancelled: "cancelled"; + }>; + environmentId: z$1.ZodString; + entries: z$1.ZodArray; + key: z$1.ZodString; + text: z$1.ZodString; + startedAt: z$1.ZodOptional; + status: z$1.ZodOptional>; + metadata: z$1.ZodOptional>; + }, z$1.core.$strip>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"system/provider-turn-watchdog">; + threadId: z$1.ZodString; + reason: z$1.ZodLiteral<"provider-turn-idle">; + thresholdMs: z$1.ZodNumber; + elapsedMs: z$1.ZodNumber; + activeTurnId: z$1.ZodString; + activeTurnStartedAt: z$1.ZodNumber; + lastActivityEventSequence: z$1.ZodNumber; + lastActivityEventType: z$1.ZodString; + lastActivityEventAt: z$1.ZodNumber; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodNullable; + firedAt: z$1.ZodNumber; +}, z$1.core.$strip>]>, z$1.ZodObject<{ + scope: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"turn">; + turnId: z$1.ZodString; + }, z$1.core.$strip>], "kind">; +}, z$1.core.$strip>>]>>; +type ThreadEvent = z$1.infer; +type ThreadEventType = ThreadEvent["type"]; + +declare const providerInfoSchema: z$1.ZodObject<{ + id: z$1.ZodString; + displayName: z$1.ZodString; + logoUrl: z$1.ZodNullable; + capabilities: z$1.ZodObject<{ + supportsArchive: z$1.ZodBoolean; + supportsRename: z$1.ZodBoolean; + supportsServiceTier: z$1.ZodBoolean; + supportsUserQuestion: z$1.ZodBoolean; + supportsFork: z$1.ZodBoolean; + supportedPermissionModes: z$1.ZodArray>; + }, z$1.core.$strip>; + composerActions: z$1.ZodArray; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plan">; + command: z$1.ZodObject<{ + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + trailingText: z$1.ZodString; + }, z$1.core.$strip>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"goal">; + command: z$1.ZodObject<{ + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + trailingText: z$1.ZodString; + }, z$1.core.$strip>; + }, z$1.core.$strip>], "kind">>; + available: z$1.ZodBoolean; +}, z$1.core.$strip>; +type ProviderInfo = z$1.infer; + +declare const threadEventScopeSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; +}, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"turn">; + turnId: z$1.ZodString; +}, z$1.core.$strip>], "kind">; +type ThreadEventScope = z$1.infer; + +type ThreadEventByType = { + [TType in ThreadEventType]: Extract; +}; +type ThreadEventForType = ThreadEventByType[TType]; +type StoredThreadEventDataFromEvent = Omit; +interface ThreadEventRowBase { + id: string; + scope: ThreadEventScope; + threadId: string; + seq: number; + createdAt: number; +} +type ThreadEventRowFromEvent = ThreadEventRowBase & { + type: TEvent["type"]; + data: StoredThreadEventDataFromEvent; +}; +type ThreadEventRowOfType = ThreadEventRowFromEvent>; +type ThreadEventRow = { + [TType in ThreadEventType]: ThreadEventRowOfType; +}[ThreadEventType]; + +declare const threadStatusSchema: z$1.ZodEnum<{ + error: "error"; + active: "active"; + starting: "starting"; + idle: "idle"; + stopping: "stopping"; +}>; +type ThreadStatus = z$1.infer; + +declare const threadTimelinePendingTodosSchema: z$1.ZodObject<{ + sourceSeq: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + items: z$1.ZodArray; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type ThreadTimelinePendingTodos = z$1.infer; + +declare const threadQueuedMessageSchema: z$1.ZodObject<{ + id: z$1.ZodString; + content: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodString; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + full: "full"; + auto: "auto"; + "accept-edits": "accept-edits"; + }>; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + groupWithNext: z$1.ZodBoolean; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; +}, z$1.core.$strip>; +type ThreadQueuedMessage = z$1.infer; + +declare const workspaceFileListResponseSchema: z$1.ZodObject<{ + files: z$1.ZodArray>; + truncated: z$1.ZodBoolean; +}, z$1.core.$strip>; +type WorkspaceFileListResponse = z$1.infer; +declare const workspacePathListResponseSchema: z$1.ZodObject<{ + paths: z$1.ZodArray; + path: z$1.ZodString; + name: z$1.ZodString; + score: z$1.ZodNumber; + positions: z$1.ZodArray; + }, z$1.core.$strip>>; + truncated: z$1.ZodBoolean; +}, z$1.core.$strip>; +type WorkspacePathListResponse = z$1.infer; + +declare const createProjectSourceRequestSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + hostId: z$1.ZodString; + type: z$1.ZodLiteral<"local_path">; + path: z$1.ZodPipe>; +}, z$1.core.$strict>, z$1.ZodObject<{ + hostId: z$1.ZodString; + type: z$1.ZodLiteral<"clone">; + targetPath: z$1.ZodOptional>>; + remoteUrl: z$1.ZodOptional; +}, z$1.core.$strict>], "type">; +type CreateProjectSourceRequest = z$1.infer; +declare const createProjectRequestSchema: z$1.ZodObject<{ + name: z$1.ZodString; + source: z$1.ZodObject<{ + hostId: z$1.ZodString; + type: z$1.ZodLiteral<"local_path">; + path: z$1.ZodPipe>; + }, z$1.core.$strict>; +}, z$1.core.$strip>; +type CreateProjectRequest = z$1.infer; +declare const threadSectionSchema: z$1.ZodObject<{ + id: z$1.ZodString; + name: z$1.ZodString; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; +}, z$1.core.$strict>; +type ThreadSectionResponse = z$1.infer; +declare const createThreadSectionRequestSchema: z$1.ZodObject<{ + name: z$1.ZodString; +}, z$1.core.$strict>; +type CreateThreadSectionRequest = z$1.infer; +declare const updateThreadSectionRequestSchema: z$1.ZodObject<{ + id: z$1.ZodString; + name: z$1.ZodString; +}, z$1.core.$strict>; +type UpdateThreadSectionRequest = z$1.infer; +declare const deleteThreadSectionRequestSchema: z$1.ZodObject<{ + id: z$1.ZodString; +}, z$1.core.$strict>; +type DeleteThreadSectionRequest = z$1.infer; +declare const threadSectionMutationResponseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + name: z$1.ZodString; + updatedThreadCount: z$1.ZodNumber; +}, z$1.core.$strict>; +type ThreadSectionMutationResponse = z$1.infer; +declare const reorderProjectRequestSchema: z$1.ZodObject<{ + previousProjectId: z$1.ZodNullable; + nextProjectId: z$1.ZodNullable; +}, z$1.core.$strip>; +type ReorderProjectRequest = z$1.infer; +declare const projectListQuerySchema: z$1.ZodObject<{ + include: z$1.ZodOptional; + includePersonal: z$1.ZodOptional>; +}, z$1.core.$strip>; +type ProjectListQuery = z$1.infer; +declare const projectFilesQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional>; + limit: z$1.ZodOptional>; + hostId: z$1.ZodOptional; + environmentId: z$1.ZodOptional, z$1.ZodOptional>>; +}, z$1.core.$strip>; +type ProjectFilesQuery = z$1.infer; +declare const projectPathsQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional>; + limit: z$1.ZodOptional>; + includeFiles: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; + includeDirectories: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; + hostId: z$1.ZodOptional; + environmentId: z$1.ZodOptional, z$1.ZodOptional>>; +}, z$1.core.$strip>; +type ProjectPathsQuery = z$1.infer; +declare const projectFileContentQuerySchema: z$1.ZodObject<{ + path: z$1.ZodString; + hostId: z$1.ZodOptional; + environmentId: z$1.ZodOptional, z$1.ZodOptional>>; +}, z$1.core.$strip>; +type ProjectFileContentQuery = z$1.infer; +declare const projectBranchesQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional; + limit: z$1.ZodOptional; + hostId: z$1.ZodString; + selectedBranch: z$1.ZodOptional; +}, z$1.core.$strip>; +type ProjectBranchesQuery = z$1.infer; +declare const projectBranchesResponseSchema: z$1.ZodObject<{ + branches: z$1.ZodArray; + branchesTruncated: z$1.ZodBoolean; + checkout: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"branch">; + branchName: z$1.ZodString; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"detached">; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unborn">; + branchName: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + }, z$1.core.$strip>], "kind">; + defaultBranch: z$1.ZodNullable; + defaultBranchRelation: z$1.ZodNullable>; + hasUncommittedChanges: z$1.ZodBoolean; + operation: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"none">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"merge">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"rebase">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"cherry-pick">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"revert">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>], "kind">; + originDefaultBranch: z$1.ZodNullable; + remoteBranches: z$1.ZodArray; + remoteBranchesTruncated: z$1.ZodBoolean; + selectedBranch: z$1.ZodNullable; + }, z$1.core.$strip>>; + defaultWorktreeBaseBranch: z$1.ZodNullable; +}, z$1.core.$strip>; +type ProjectBranchesResponse = z$1.infer; +declare const promptHistoryQuerySchema: z$1.ZodObject<{ + limit: z$1.ZodOptional; +}, z$1.core.$strip>; +type PromptHistoryQuery = z$1.infer; +declare const promptHistoryResponseSchema: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; +}, z$1.core.$strip>>; +type PromptHistoryResponse = z$1.infer; +declare const updateProjectRequestSchema: z$1.ZodObject<{ + name: z$1.ZodOptional; +}, z$1.core.$strip>; +type UpdateProjectRequest = z$1.infer; +declare const updateProjectSourceRequestSchema: z$1.ZodObject<{ + type: z$1.ZodLiteral<"local_path">; + path: z$1.ZodOptional>>; + isDefault: z$1.ZodOptional>; +}, z$1.core.$strict>; +type UpdateProjectSourceRequest = z$1.infer; +declare const commandListResponseSchema: z$1.ZodObject<{ + commands: z$1.ZodArray; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + description: z$1.ZodNullable; + argumentHint: z$1.ZodNullable; + pluginId: z$1.ZodOptional; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type CommandListResponse = z$1.infer; +/** Query for the complete command catalog available to a project and provider. */ +declare const projectCommandsQuerySchema: z$1.ZodObject<{ + provider: z$1.ZodString; + hostId: z$1.ZodOptional; + environmentId: z$1.ZodOptional, z$1.ZodOptional>>; +}, z$1.core.$strict>; +type ProjectCommandsQuery = z$1.infer; +declare const projectResponseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodEnum<{ + standard: "standard"; + personal: "personal"; + }>; + name: z$1.ZodString; + gitRemoteUrl: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + sources: z$1.ZodArray; + hostId: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type ProjectResponse = z$1.infer; +declare const projectWithThreadsResponseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodEnum<{ + standard: "standard"; + personal: "personal"; + }>; + name: z$1.ZodString; + gitRemoteUrl: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + sources: z$1.ZodArray; + hostId: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>>; + threads: z$1.ZodArray; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + activity: z$1.ZodObject<{ + activeWorkflowCount: z$1.ZodNumber; + activeBackgroundAgentCount: z$1.ZodNumber; + activeBackgroundCommandCount: z$1.ZodNumber; + activePlanModeCount: z$1.ZodNumber; + activeGoalCount: z$1.ZodNumber; + }, z$1.core.$strip>; + pinSortKey: z$1.ZodNullable; + hasPendingInteraction: z$1.ZodBoolean; + environmentHostId: z$1.ZodNullable; + environmentName: z$1.ZodNullable; + environmentBranchName: z$1.ZodNullable; + environmentWorkspaceDisplayKind: z$1.ZodEnum<{ + "managed-worktree": "managed-worktree"; + "unmanaged-worktree": "unmanaged-worktree"; + other: "other"; + }>; + }, z$1.core.$strip>>; + defaultExecutionOptions: z$1.ZodNullable; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + "accept-edits": "accept-edits"; + auto: "auto"; + full: "full"; + }>; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type ProjectWithThreadsResponse = z$1.infer; +declare const uploadedPromptAttachmentSchema: z$1.ZodObject<{ + type: z$1.ZodEnum<{ + localImage: "localImage"; + localFile: "localFile"; + }>; + path: z$1.ZodString; + name: z$1.ZodString; + mimeType: z$1.ZodOptional; + sizeBytes: z$1.ZodNumber; +}, z$1.core.$strip>; +type UploadedPromptAttachment = z$1.infer; +declare const copyProjectAttachmentsRequestSchema: z$1.ZodObject<{ + sourceProjectId: z$1.ZodString; + paths: z$1.ZodArray; +}, z$1.core.$strict>; +type CopyProjectAttachmentsRequest = z$1.infer; + +declare const updateEnvironmentRequestSchema: z$1.ZodObject<{ + mergeBaseBranch: z$1.ZodOptional>; + name: z$1.ZodOptional>; +}, z$1.core.$strip>; +type UpdateEnvironmentRequest = z$1.infer; +/** + * Query for searching paths in an environment's workspace. Unlike the + * project-scoped variant this needs no `environmentId` — the environment is + * the route param — and is project-agnostic, so it works for projectless + * (personal) environments too. + */ +declare const environmentPathsQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional; + limit: z$1.ZodOptional; + includeFiles: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; + includeDirectories: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; +}, z$1.core.$strip>; +type EnvironmentPathsQuery = z$1.infer; +declare const environmentDiffBranchesQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional; + limit: z$1.ZodOptional; + selectedBranch: z$1.ZodOptional; +}, z$1.core.$strip>; +type EnvironmentDiffBranchesQuery = z$1.infer; +declare const environmentDiffBranchesResponseSchema: z$1.ZodObject<{ + branches: z$1.ZodArray; + branchesTruncated: z$1.ZodBoolean; + remoteBranches: z$1.ZodArray; + remoteBranchesTruncated: z$1.ZodBoolean; + selectedBranch: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type EnvironmentDiffBranchesResponse = z$1.infer; +declare const environmentStatusQuerySchema: z$1.ZodObject<{ + mergeBaseBranch: z$1.ZodOptional>; +}, z$1.core.$strip>; +type EnvironmentStatusQuery = z$1.infer; +declare const environmentDiffQuerySchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + target: z$1.ZodLiteral<"uncommitted">; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"branch_committed">; + mergeBaseBranch: z$1.ZodPipe; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"all">; + mergeBaseBranch: z$1.ZodPipe; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; +}, z$1.core.$strip>], "target">; +type EnvironmentDiffQuery = z$1.infer; +/** + * Query for fetching a single file's contents at one side of a diff target. + * Used by the diff card to reparse the card's patch with full old/new contents + * so `@pierre/diffs` can render expand-context buttons between hunks. + * + * For `branch_committed` / `all`, callers pass the resolved merge-base SHA + * (`mergeBaseRef`, surfaced by `workspace.diff`) rather than the branch name + * — the diff itself was computed against that SHA, so reading the old side + * from the same SHA keeps the file content aligned with the hunk line + * numbers. Reading from the branch tip is wrong whenever the branch has + * moved past the merge-base since the file existed there. + */ +declare const environmentDiffFileQuerySchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + target: z$1.ZodLiteral<"uncommitted">; + path: z$1.ZodString; + side: z$1.ZodEnum<{ + new: "new"; + old: "old"; + }>; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"branch_committed">; + mergeBaseRef: z$1.ZodString; + path: z$1.ZodString; + side: z$1.ZodEnum<{ + new: "new"; + old: "old"; + }>; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"all">; + mergeBaseRef: z$1.ZodString; + path: z$1.ZodString; + side: z$1.ZodEnum<{ + new: "new"; + old: "old"; + }>; +}, z$1.core.$strip>, z$1.ZodObject<{ + target: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; + path: z$1.ZodString; + side: z$1.ZodEnum<{ + new: "new"; + old: "old"; + }>; +}, z$1.core.$strip>], "target">; +type EnvironmentDiffFileQuery = z$1.infer; +declare const environmentDiffFileResponseSchema: z$1.ZodObject<{ + path: z$1.ZodString; + content: z$1.ZodString; + contentEncoding: z$1.ZodEnum<{ + utf8: "utf8"; + base64: "base64"; + }>; + mimeType: z$1.ZodOptional; + sizeBytes: z$1.ZodNumber; +}, z$1.core.$strip>; +type EnvironmentDiffFileResponse = z$1.infer; +declare const environmentArchiveThreadsResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + archivedThreadIds: z$1.ZodArray; +}, z$1.core.$strip>; +type EnvironmentArchiveThreadsResponse = z$1.infer; +declare const pullRequestMergeMethodSchema: z$1.ZodEnum<{ + merge: "merge"; + rebase: "rebase"; + squash: "squash"; +}>; +type PullRequestMergeMethod = z$1.infer; +declare const commitActionResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + action: z$1.ZodLiteral<"commit">; + message: z$1.ZodString; + commitSha: z$1.ZodString; + commitSubject: z$1.ZodString; +}, z$1.core.$strip>; +type CommitActionResponse = z$1.infer; +declare const squashMergeActionResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + action: z$1.ZodLiteral<"squash_merge">; + merged: z$1.ZodBoolean; + message: z$1.ZodString; + commitSha: z$1.ZodString; + commitSubject: z$1.ZodString; +}, z$1.core.$strip>; +type SquashMergeActionResponse = z$1.infer; +declare const pullRequestReadyActionResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + action: z$1.ZodLiteral<"pull_request_ready">; + message: z$1.ZodString; +}, z$1.core.$strip>; +type PullRequestReadyActionResponse = z$1.infer; +declare const pullRequestMergeActionResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + action: z$1.ZodLiteral<"pull_request_merge">; + method: z$1.ZodEnum<{ + merge: "merge"; + rebase: "rebase"; + squash: "squash"; + }>; + message: z$1.ZodString; +}, z$1.core.$strip>; +type PullRequestMergeActionResponse = z$1.infer; +declare const pullRequestDraftActionResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + action: z$1.ZodLiteral<"pull_request_draft">; + message: z$1.ZodString; +}, z$1.core.$strip>; +type PullRequestDraftActionResponse = z$1.infer; +declare const environmentStatusResponseSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + workspace: z$1.ZodObject<{ + workingTree: z$1.ZodObject<{ + insertions: z$1.ZodNumber; + deletions: z$1.ZodNumber; + files: z$1.ZodArray; + insertions: z$1.ZodNullable; + deletions: z$1.ZodNullable; + }, z$1.core.$strip>>; + hasUncommittedChanges: z$1.ZodBoolean; + state: z$1.ZodEnum<{ + clean: "clean"; + untracked: "untracked"; + dirty_uncommitted: "dirty_uncommitted"; + committed_unmerged: "committed_unmerged"; + dirty_and_committed_unmerged: "dirty_and_committed_unmerged"; + }>; + }, z$1.core.$strip>; + checkout: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"branch">; + branchName: z$1.ZodString; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"detached">; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unborn">; + branchName: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + }, z$1.core.$strip>], "kind">; + branch: z$1.ZodObject<{ + currentBranch: z$1.ZodNullable; + defaultBranch: z$1.ZodString; + }, z$1.core.$strip>; + mergeBase: z$1.ZodNullable; + insertions: z$1.ZodNullable; + deletions: z$1.ZodNullable; + }, z$1.core.$strip>>; + mergeBaseBranch: z$1.ZodString; + baseRef: z$1.ZodNullable; + aheadCount: z$1.ZodNumber; + behindCount: z$1.ZodNumber; + hasCommittedUnmergedChanges: z$1.ZodBoolean; + commits: z$1.ZodArray>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"not_applicable">; + reason: z$1.ZodEnum<{ + non_git_environment: "non_git_environment"; + }>; + message: z$1.ZodString; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + unknown: "unknown"; + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; +}, z$1.core.$strict>], "outcome">; +/** + * Structured pull-request lookup outcome. "absent" is a real answer — the + * host checked and the branch has no PR (non-git environments resolve to + * "absent" without a daemon call). "unavailable" means the lookup itself + * failed (gh missing, not authenticated, timeout, unreachable workspace), so + * callers must not render it as "no PR exists". + */ +declare const environmentPullRequestResponseSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + pullRequest: z$1.ZodObject<{ + number: z$1.ZodNumber; + title: z$1.ZodString; + state: z$1.ZodEnum<{ + merged: "merged"; + draft: "draft"; + open: "open"; + closed: "closed"; + }>; + url: z$1.ZodString; + baseRefName: z$1.ZodString; + headRefName: z$1.ZodString; + updatedAt: z$1.ZodString; + checks: z$1.ZodObject<{ + state: z$1.ZodEnum<{ + unknown: "unknown"; + pending: "pending"; + passing: "passing"; + failing: "failing"; + no_checks: "no_checks"; + }>; + totalCount: z$1.ZodNumber; + passedCount: z$1.ZodNumber; + failedCount: z$1.ZodNumber; + pendingCount: z$1.ZodNumber; + }, z$1.core.$strict>; + review: z$1.ZodObject<{ + state: z$1.ZodEnum<{ + none: "none"; + approved: "approved"; + changes_requested: "changes_requested"; + review_required: "review_required"; + review_requested: "review_requested"; + }>; + reviewRequestCount: z$1.ZodNumber; + }, z$1.core.$strict>; + mergeability: z$1.ZodObject<{ + state: z$1.ZodEnum<{ + unknown: "unknown"; + draft: "draft"; + mergeable: "mergeable"; + conflicts: "conflicts"; + blocked: "blocked"; + }>; + mergeStateStatus: z$1.ZodNullable>; + mergeable: z$1.ZodNullable>; + }, z$1.core.$strict>; + attention: z$1.ZodEnum<{ + none: "none"; + merged: "merged"; + draft: "draft"; + closed: "closed"; + changes_requested: "changes_requested"; + review_requested: "review_requested"; + conflicts: "conflicts"; + blocked: "blocked"; + checks_failed: "checks_failed"; + checks_pending: "checks_pending"; + ready_to_merge: "ready_to_merge"; + }>; + }, z$1.core.$strict>; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"absent">; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + message: z$1.ZodString; +}, z$1.core.$strict>], "outcome">; +type EnvironmentPullRequestResponse = z$1.infer; +declare const environmentDiffResponseSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + diff: z$1.ZodObject<{ + diff: z$1.ZodString; + truncated: z$1.ZodBoolean; + shortstat: z$1.ZodString; + files: z$1.ZodString; + mergeBaseRef: z$1.ZodNullable; + }, z$1.core.$strip>; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"not_applicable">; + reason: z$1.ZodEnum<{ + non_git_environment: "non_git_environment"; + }>; + message: z$1.ZodString; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + unknown: "unknown"; + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; +}, z$1.core.$strict>], "outcome">; +type EnvironmentDiffResponse = z$1.infer; +declare const environmentDiffFilesResponseSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + files: z$1.ZodArray; + changeKind: z$1.ZodEnum<{ + deleted: "deleted"; + added: "added"; + modified: "modified"; + renamed: "renamed"; + copied: "copied"; + type_changed: "type_changed"; + }>; + additions: z$1.ZodNumber; + deletions: z$1.ZodNumber; + binary: z$1.ZodBoolean; + origin: z$1.ZodEnum<{ + untracked: "untracked"; + tracked: "tracked"; + }>; + loadMode: z$1.ZodEnum<{ + auto: "auto"; + on_demand: "on_demand"; + too_large: "too_large"; + }>; + }, z$1.core.$strip>>; + shortstat: z$1.ZodString; + mergeBaseRef: z$1.ZodNullable; + initialPatches: z$1.ZodArray>; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"not_applicable">; + reason: z$1.ZodEnum<{ + non_git_environment: "non_git_environment"; + too_many_files: "too_many_files"; + }>; + message: z$1.ZodString; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + unknown: "unknown"; + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; +}, z$1.core.$strict>], "outcome">; +type EnvironmentDiffFilesResponse = z$1.infer; +declare const environmentDiffPatchResponseSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + patches: z$1.ZodArray>; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"not_applicable">; + reason: z$1.ZodEnum<{ + non_git_environment: "non_git_environment"; + }>; + message: z$1.ZodString; +}, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + unknown: "unknown"; + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; +}, z$1.core.$strict>], "outcome">; +type EnvironmentDiffPatchResponse = z$1.infer; +/** + * Body for `POST /diff/patch`: the diff target plus the list of new paths whose + * patches the client wants. A POST (not GET) because the repeated `paths` array + * cannot survive flat query parsing. The client supplies only new paths; the + * server re-derives each file's rename/copy pairing (`previousPath`) from its + * own TOC. + */ +declare const environmentDiffPatchRequestSchema: z$1.ZodObject<{ + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"uncommitted">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"branch_committed">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"all">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; + }, z$1.core.$strip>], "type">; + paths: z$1.ZodArray; +}, z$1.core.$strict>; +type EnvironmentDiffPatchRequest = z$1.infer; +type EnvironmentStatusResponse = z$1.infer; + +declare const providerUsageResponseSchema: z$1.ZodObject<{ + codex: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; + claudeCode: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; + cursor: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; +}, z$1.core.$strip>; +type ProviderUsageResponse = z$1.infer; +type HostDaemonCommandTransport = "settled" | "onlineRpc"; +type HostDaemonCommandEnvironmentLane = "read" | "write"; +type HostDaemonFlushEventsBeforeResult = boolean | "when-initiated"; +interface HostDaemonCommandDescriptor { + type: Type; + schema: Schema; + resultSchema: ResultSchema; + transport: Transport; + retryable: Retryable; + flushEventsBeforeResult: HostDaemonFlushEventsBeforeResult; + envLane: HostDaemonCommandEnvironmentLane | null; +} +declare const hostDaemonCommandRegistry: { + "thread.start": HostDaemonCommandDescriptor<"thread.start", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + projectId: z$1.ZodString; + providerId: z$1.ZodString; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + options: z$1.ZodIntersection; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + claudeCodePermissionMode: z$1.ZodOptional>; + claudeCodeMockCliTraffic: z$1.ZodOptional>; + workflowsEnabled: z$1.ZodBoolean; + memoryEnabled: z$1.ZodOptional; + providerSubagentsEnabled: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"accept-edits">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"user">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"auto">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"automatic">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"full">; + permissionScope: z$1.ZodLiteral<"full">; + approvalReviewer: z$1.ZodNull; + permissionEscalation: z$1.ZodNull; + }, z$1.core.$strip>], "permissionMode">>; + instructions: z$1.ZodString; + dynamicTools: z$1.ZodArray>; + injectedSkillSources: z$1.ZodArray; + treeHash: z$1.ZodString; + entryPath: z$1.ZodString; + sourceType: z$1.ZodEnum<{ + builtin: "builtin"; + "data-dir": "data-dir"; + }>; + }, z$1.core.$strict>, z$1.ZodObject<{ + name: z$1.ZodString; + description: z$1.ZodString; + kind: z$1.ZodLiteral<"workspace-path">; + sourceType: z$1.ZodLiteral<"project">; + sourceRootPath: z$1.ZodString; + skillFilePath: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; + disallowedTools: z$1.ZodOptional>; + instructionMode: z$1.ZodEnum<{ + append: "append"; + replace: "replace"; + }>; + type: z$1.ZodLiteral<"thread.start">; + requestId: z$1.ZodString; + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + builtin: "builtin"; + project: "project"; + user: "user"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + inputGroups: z$1.ZodOptional>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + builtin: "builtin"; + project: "project"; + user: "user"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>>>; + threadStoragePath: z$1.ZodOptional; + fork: z$1.ZodOptional>; + }, z$1.core.$strict>, z$1.ZodObject<{ + providerThreadId: z$1.ZodString; + }, z$1.core.$strip>, "settled", false>; + "turn.submit": HostDaemonCommandDescriptor<"turn.submit", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"turn.submit">; + requestId: z$1.ZodString; + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + builtin: "builtin"; + project: "project"; + user: "user"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + inputGroups: z$1.ZodOptional>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + builtin: "builtin"; + project: "project"; + user: "user"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>>>; + options: z$1.ZodIntersection; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + claudeCodePermissionMode: z$1.ZodOptional>; + claudeCodeMockCliTraffic: z$1.ZodOptional>; + workflowsEnabled: z$1.ZodBoolean; + memoryEnabled: z$1.ZodOptional; + providerSubagentsEnabled: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"accept-edits">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"user">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"auto">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"automatic">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"full">; + permissionScope: z$1.ZodLiteral<"full">; + approvalReviewer: z$1.ZodNull; + permissionEscalation: z$1.ZodNull; + }, z$1.core.$strip>], "permissionMode">>; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + resumeContext: z$1.ZodObject<{ + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + instructionMode: z$1.ZodEnum<{ + append: "append"; + replace: "replace"; + }>; + projectId: z$1.ZodString; + providerId: z$1.ZodString; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + instructions: z$1.ZodString; + dynamicTools: z$1.ZodArray>; + injectedSkillSources: z$1.ZodArray; + treeHash: z$1.ZodString; + entryPath: z$1.ZodString; + sourceType: z$1.ZodEnum<{ + builtin: "builtin"; + "data-dir": "data-dir"; + }>; + }, z$1.core.$strict>, z$1.ZodObject<{ + name: z$1.ZodString; + description: z$1.ZodString; + kind: z$1.ZodLiteral<"workspace-path">; + sourceType: z$1.ZodLiteral<"project">; + sourceRootPath: z$1.ZodString; + skillFilePath: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; + disallowedTools: z$1.ZodOptional>; + providerThreadId: z$1.ZodString; + }, z$1.core.$strict>; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + mode: z$1.ZodLiteral<"start">; + }, z$1.core.$strip>, z$1.ZodObject<{ + mode: z$1.ZodLiteral<"auto">; + expectedTurnId: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + mode: z$1.ZodLiteral<"steer">; + expectedTurnId: z$1.ZodNullable; + }, z$1.core.$strip>], "mode">; + }, z$1.core.$strict>, z$1.ZodObject<{ + appliedAs: z$1.ZodEnum<{ + steer: "steer"; + "new-turn": "new-turn"; + }>; + }, z$1.core.$strip>, "settled", false>; + "thread.stop": HostDaemonCommandDescriptor<"thread.stop", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"thread.stop">; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "thread.goal.clear": HostDaemonCommandDescriptor<"thread.goal.clear", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"thread.goal.clear">; + options: z$1.ZodIntersection; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + claudeCodePermissionMode: z$1.ZodOptional>; + claudeCodeMockCliTraffic: z$1.ZodOptional>; + workflowsEnabled: z$1.ZodBoolean; + memoryEnabled: z$1.ZodOptional; + providerSubagentsEnabled: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"accept-edits">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"user">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"auto">; + permissionScope: z$1.ZodLiteral<"workspace">; + approvalReviewer: z$1.ZodLiteral<"automatic">; + permissionEscalation: z$1.ZodEnum<{ + ask: "ask"; + deny: "deny"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + permissionMode: z$1.ZodLiteral<"full">; + permissionScope: z$1.ZodLiteral<"full">; + approvalReviewer: z$1.ZodNull; + permissionEscalation: z$1.ZodNull; + }, z$1.core.$strip>], "permissionMode">>; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + resumeContext: z$1.ZodObject<{ + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + instructionMode: z$1.ZodEnum<{ + append: "append"; + replace: "replace"; + }>; + projectId: z$1.ZodString; + providerId: z$1.ZodString; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + instructions: z$1.ZodString; + dynamicTools: z$1.ZodArray>; + injectedSkillSources: z$1.ZodArray; + treeHash: z$1.ZodString; + entryPath: z$1.ZodString; + sourceType: z$1.ZodEnum<{ + builtin: "builtin"; + "data-dir": "data-dir"; + }>; + }, z$1.core.$strict>, z$1.ZodObject<{ + name: z$1.ZodString; + description: z$1.ZodString; + kind: z$1.ZodLiteral<"workspace-path">; + sourceType: z$1.ZodLiteral<"project">; + sourceRootPath: z$1.ZodString; + skillFilePath: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; + disallowedTools: z$1.ZodOptional>; + providerThreadId: z$1.ZodString; + }, z$1.core.$strict>; + }, z$1.core.$strict>, z$1.ZodObject<{ + cleared: z$1.ZodBoolean; + }, z$1.core.$strict>, "settled", false>; + "thread.plan.cancel": HostDaemonCommandDescriptor<"thread.plan.cancel", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"thread.plan.cancel">; + expectedTurnId: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + cancelled: z$1.ZodBoolean; + }, z$1.core.$strict>, "settled", false>; + "thread.rename": HostDaemonCommandDescriptor<"thread.rename", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"thread.rename">; + title: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "thread.archive": HostDaemonCommandDescriptor<"thread.archive", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"thread.archive">; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "thread.unarchive": HostDaemonCommandDescriptor<"thread.unarchive", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"thread.unarchive">; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "interactive.resolve": HostDaemonCommandDescriptor<"interactive.resolve", z$1.ZodObject<{ + environmentId: z$1.ZodString; + threadId: z$1.ZodString; + type: z$1.ZodLiteral<"interactive.resolve">; + interactionId: z$1.ZodString; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerRequestId: z$1.ZodString; + resolution: z$1.ZodUnion; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"allow_for_session">; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"deny">; + }, z$1.core.$strip>], "decision">, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_answer">; + answers: z$1.ZodRecord; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin_submitted">; + }, z$1.core.$strip>]>; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "codex.inference.complete": HostDaemonCommandDescriptor<"codex.inference.complete", z$1.ZodObject<{ + type: z$1.ZodLiteral<"codex.inference.complete">; + model: z$1.ZodString; + prompt: z$1.ZodString; + outputSchema: z$1.ZodType>; + timeoutMs: z$1.ZodNumber; + }, z$1.core.$strict>, z$1.ZodObject<{ + model: z$1.ZodString; + value: z$1.ZodType>; + }, z$1.core.$strip>, "settled", false>; + "codex.voice.transcribe": HostDaemonCommandDescriptor<"codex.voice.transcribe", z$1.ZodObject<{ + type: z$1.ZodLiteral<"codex.voice.transcribe">; + model: z$1.ZodString; + audioBase64: z$1.ZodString; + mimeType: z$1.ZodString; + filename: z$1.ZodString; + prompt: z$1.ZodNullable; + timeoutMs: z$1.ZodNumber; + }, z$1.core.$strict>, z$1.ZodObject<{ + model: z$1.ZodString; + text: z$1.ZodString; + }, z$1.core.$strip>, "settled", false>; + "environment.provision": HostDaemonCommandDescriptor<"environment.provision", z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + environmentId: z$1.ZodString; + type: z$1.ZodLiteral<"environment.provision">; + initiator: z$1.ZodNullable>; + workspaceProvisionType: z$1.ZodLiteral<"unmanaged">; + path: z$1.ZodString; + checkout: z$1.ZodOptional; + name: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"new">; + name: z$1.ZodString; + baseBranch: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodString; + type: z$1.ZodLiteral<"environment.provision">; + initiator: z$1.ZodNullable>; + sourcePath: z$1.ZodString; + targetPath: z$1.ZodString; + branchName: z$1.ZodString; + baseBranch: z$1.ZodNullable; + setupTimeoutMs: z$1.ZodNumber; + workspaceProvisionType: z$1.ZodLiteral<"managed-worktree">; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodString; + type: z$1.ZodLiteral<"environment.provision">; + initiator: z$1.ZodNullable>; + workspaceProvisionType: z$1.ZodLiteral<"personal">; + targetPath: z$1.ZodString; + }, z$1.core.$strict>], "workspaceProvisionType">, z$1.ZodObject<{ + path: z$1.ZodString; + isGitRepo: z$1.ZodBoolean; + isWorktree: z$1.ZodBoolean; + branchName: z$1.ZodNullable; + defaultBranch: z$1.ZodNullable; + transcript: z$1.ZodArray; + key: z$1.ZodString; + text: z$1.ZodString; + startedAt: z$1.ZodOptional; + status: z$1.ZodOptional>; + metadata: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, "settled", false>; + "project.clone": HostDaemonCommandDescriptor<"project.clone", z$1.ZodObject<{ + type: z$1.ZodLiteral<"project.clone">; + remoteUrl: z$1.ZodString; + projectSlug: z$1.ZodString; + targetPath: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodString; + gitRemoteUrl: z$1.ZodNullable; + }, z$1.core.$strict>, "settled", false>; + "environment.provision.cancel": HostDaemonCommandDescriptor<"environment.provision.cancel", z$1.ZodObject<{ + environmentId: z$1.ZodString; + type: z$1.ZodLiteral<"environment.provision.cancel">; + }, z$1.core.$strict>, z$1.ZodObject<{ + aborted: z$1.ZodBoolean; + }, z$1.core.$strip>, "settled", false>; + "environment.destroy": HostDaemonCommandDescriptor<"environment.destroy", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"environment.destroy">; + }, z$1.core.$strict>, z$1.ZodObject<{}, z$1.core.$strip>, "settled", false>; + "workspace.commit": HostDaemonCommandDescriptor<"workspace.commit", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.commit">; + message: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + commitSha: z$1.ZodString; + commitSubject: z$1.ZodString; + }, z$1.core.$strip>, "settled", false>; + "workspace.squash_merge": HostDaemonCommandDescriptor<"workspace.squash_merge", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.squash_merge">; + targetBranch: z$1.ZodString; + commitMessage: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + commitSha: z$1.ZodString; + commitSubject: z$1.ZodString; + merged: z$1.ZodBoolean; + }, z$1.core.$strip>, "settled", false>; + "workspace.pull_request_action": HostDaemonCommandDescriptor<"workspace.pull_request_action", z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.pull_request_action">; + operation: z$1.ZodLiteral<"ready">; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.pull_request_action">; + operation: z$1.ZodLiteral<"draft">; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.pull_request_action">; + operation: z$1.ZodLiteral<"merge">; + method: z$1.ZodEnum<{ + merge: "merge"; + squash: "squash"; + rebase: "rebase"; + }>; + }, z$1.core.$strict>], "operation">, z$1.ZodObject<{}, z$1.core.$strict>, "settled", false>; + "host.list_files": HostDaemonCommandDescriptor<"host.list_files", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.list_files">; + path: z$1.ZodString; + query: z$1.ZodOptional; + limit: z$1.ZodNumber; + }, z$1.core.$strip>, z$1.ZodObject<{ + files: z$1.ZodArray>; + truncated: z$1.ZodBoolean; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.list_paths": HostDaemonCommandDescriptor<"host.list_paths", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.list_paths">; + path: z$1.ZodString; + query: z$1.ZodOptional; + limit: z$1.ZodNumber; + includeFiles: z$1.ZodBoolean; + includeDirectories: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + paths: z$1.ZodArray; + path: z$1.ZodString; + name: z$1.ZodString; + score: z$1.ZodNumber; + positions: z$1.ZodArray; + }, z$1.core.$strip>>; + truncated: z$1.ZodBoolean; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.mkdir": HostDaemonCommandDescriptor<"host.mkdir", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.mkdir">; + path: z$1.ZodString; + rootPath: z$1.ZodOptional; + recursive: z$1.ZodBoolean; + }, z$1.core.$strict>, z$1.ZodObject<{ + ok: z$1.ZodLiteral; + }, z$1.core.$strict>, "onlineRpc", false>; + "host.move_path": HostDaemonCommandDescriptor<"host.move_path", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.move_path">; + sourcePath: z$1.ZodString; + destinationPath: z$1.ZodString; + rootPath: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + ok: z$1.ZodLiteral; + }, z$1.core.$strict>, "onlineRpc", false>; + "host.remove_path": HostDaemonCommandDescriptor<"host.remove_path", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.remove_path">; + path: z$1.ZodString; + rootPath: z$1.ZodOptional; + recursive: z$1.ZodBoolean; + }, z$1.core.$strict>, z$1.ZodObject<{ + ok: z$1.ZodLiteral; + }, z$1.core.$strict>, "onlineRpc", false>; + "host.browse_directory": HostDaemonCommandDescriptor<"host.browse_directory", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.browse_directory">; + path: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + directory: z$1.ZodString; + parent: z$1.ZodNullable; + entries: z$1.ZodArray; + name: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.paths_exist": HostDaemonCommandDescriptor<"host.paths_exist", z$1.ZodObject<{ + paths: z$1.ZodPipe, z$1.ZodTransform>; + type: z$1.ZodLiteral<"host.paths_exist">; + }, z$1.core.$strict>, z$1.ZodObject<{ + existence: z$1.ZodRecord; + }, z$1.core.$strip>, "onlineRpc", true>; + "project.inspect": HostDaemonCommandDescriptor<"project.inspect", z$1.ZodObject<{ + type: z$1.ZodLiteral<"project.inspect">; + path: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodString; + gitRemoteUrl: z$1.ZodNullable; + }, z$1.core.$strict>, "onlineRpc", true>; + "project.clone_default_path": HostDaemonCommandDescriptor<"project.clone_default_path", z$1.ZodObject<{ + type: z$1.ZodLiteral<"project.clone_default_path">; + projectSlug: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodString; + }, z$1.core.$strict>, "onlineRpc", true>; + "host.pick_folder": HostDaemonCommandDescriptor<"host.pick_folder", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.pick_folder">; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodNullable; + }, z$1.core.$strip>, "onlineRpc", false>; + "host.caffeinate": HostDaemonCommandDescriptor<"host.caffeinate", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.caffeinate">; + enabled: z$1.ZodBoolean; + }, z$1.core.$strict>, z$1.ZodObject<{ + enabled: z$1.ZodBoolean; + supported: z$1.ZodBoolean; + }, z$1.core.$strict>, "onlineRpc", false>; + "connect-tunnel.ensure-identity": HostDaemonCommandDescriptor<"connect-tunnel.ensure-identity", z$1.ZodObject<{ + type: z$1.ZodLiteral<"connect-tunnel.ensure-identity">; + }, z$1.core.$strict>, z$1.ZodObject<{ + label: z$1.ZodString; + baseDomain: z$1.ZodString; + }, z$1.core.$strict>, "onlineRpc", true>; + "host.list_commands": HostDaemonCommandDescriptor<"host.list_commands", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.list_commands">; + providerId: z$1.ZodString; + cwd: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + commands: z$1.ZodArray; + origin: z$1.ZodEnum<{ + project: "project"; + user: "user"; + }>; + description: z$1.ZodNullable; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.list_branches": HostDaemonCommandDescriptor<"host.list_branches", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.list_branches">; + path: z$1.ZodString; + query: z$1.ZodOptional; + selectedBranch: z$1.ZodOptional; + limit: z$1.ZodNumber; + }, z$1.core.$strip>, z$1.ZodObject<{ + branches: z$1.ZodArray; + branchesTruncated: z$1.ZodBoolean; + checkout: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"branch">; + branchName: z$1.ZodString; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"detached">; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unborn">; + branchName: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + }, z$1.core.$strip>], "kind">; + defaultBranch: z$1.ZodNullable; + defaultBranchRelation: z$1.ZodNullable>; + hasUncommittedChanges: z$1.ZodBoolean; + operation: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"none">; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"merge">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"rebase">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"cherry-pick">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"revert">; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + hasConflicts: z$1.ZodBoolean; + }, z$1.core.$strip>], "kind">; + originDefaultBranch: z$1.ZodNullable; + remoteBranches: z$1.ZodArray; + remoteBranchesTruncated: z$1.ZodBoolean; + selectedBranch: z$1.ZodNullable; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.file_metadata": HostDaemonCommandDescriptor<"host.file_metadata", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.file_metadata">; + path: z$1.ZodString; + rootPath: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodString; + modifiedAtMs: z$1.ZodNumber; + sizeBytes: z$1.ZodNumber; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.read_file": HostDaemonCommandDescriptor<"host.read_file", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.read_file">; + path: z$1.ZodString; + rootPath: z$1.ZodOptional; + ref: z$1.ZodOptional; + }, z$1.core.$strip>, z$1.ZodObject<{ + path: z$1.ZodString; + content: z$1.ZodString; + contentEncoding: z$1.ZodEnum<{ + base64: "base64"; + utf8: "utf8"; + }>; + mimeType: z$1.ZodOptional; + sizeBytes: z$1.ZodNumber; + modifiedAtMs: z$1.ZodOptional; + sha256: z$1.ZodString; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.read_file_relative": HostDaemonCommandDescriptor<"host.read_file_relative", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.read_file_relative">; + rootPath: z$1.ZodString; + path: z$1.ZodString; + dotfiles: z$1.ZodEnum<{ + deny: "deny"; + allow: "allow"; + }>; + }, z$1.core.$strict>, z$1.ZodObject<{ + path: z$1.ZodString; + content: z$1.ZodString; + contentEncoding: z$1.ZodEnum<{ + base64: "base64"; + utf8: "utf8"; + }>; + mimeType: z$1.ZodOptional; + sizeBytes: z$1.ZodNumber; + modifiedAtMs: z$1.ZodOptional; + sha256: z$1.ZodString; + }, z$1.core.$strip>, "onlineRpc", true>; + "host.write_file": HostDaemonCommandDescriptor<"host.write_file", z$1.ZodObject<{ + type: z$1.ZodLiteral<"host.write_file">; + path: z$1.ZodString; + rootPath: z$1.ZodOptional; + content: z$1.ZodString; + contentEncoding: z$1.ZodEnum<{ + base64: "base64"; + utf8: "utf8"; + }>; + createParents: z$1.ZodBoolean; + expectedSha256: z$1.ZodOptional>; + mode: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"written">; + sha256: z$1.ZodString; + sizeBytes: z$1.ZodNumber; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"conflict">; + currentSha256: z$1.ZodNullable; + }, z$1.core.$strict>], "outcome">, "onlineRpc", false>; + "provider.list_models": HostDaemonCommandDescriptor<"provider.list_models", z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider.list_models">; + providerId: z$1.ZodString; + acpLaunchSpec: z$1.ZodOptional; + env: z$1.ZodRecord; + cwd: z$1.ZodOptional; + modelCli: z$1.ZodOptional; + selectFlag: z$1.ZodOptional; + primaryModels: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodTransform<{ + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + } | undefined, { + listArgs: string[]; + primaryModels: string[]; + selectFlag?: string | undefined; + }>>>; + reasoningCli: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + nativeReasoning: z$1.ZodOptional>; + levelValues: z$1.ZodOptional & z$1.core.$partial, z$1.ZodString>>; + defaultLevel: z$1.ZodOptional>; + }, z$1.core.$strict>>; + permissionCli: z$1.ZodOptional>; + workspaceWrite: z$1.ZodOptional>; + readonly: z$1.ZodOptional>; + insertAfterArgs: z$1.ZodOptional; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + models: z$1.ZodArray; + description: z$1.ZodString; + }, z$1.core.$strip>>; + defaultReasoningEffort: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + isDefault: z$1.ZodBoolean; + }, z$1.core.$strip>>; + selectedOnlyModels: z$1.ZodArray; + description: z$1.ZodString; + }, z$1.core.$strip>>; + defaultReasoningEffort: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + isDefault: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, "onlineRpc", true>; + "known_acp_agents.status": HostDaemonCommandDescriptor<"known_acp_agents.status", z$1.ZodObject<{ + type: z$1.ZodLiteral<"known_acp_agents.status">; + agents: z$1.ZodArray>; + }, z$1.core.$strict>, z$1.ZodObject<{ + agents: z$1.ZodArray; + }, z$1.core.$strict>>; + }, z$1.core.$strict>, "onlineRpc", true>; + "provider.usage": HostDaemonCommandDescriptor<"provider.usage", z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider.usage">; + }, z$1.core.$strict>, z$1.ZodObject<{ + codex: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; + claudeCode: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; + cursor: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + status: z$1.ZodLiteral<"ok">; + accountEmail: z$1.ZodNullable; + planLabel: z$1.ZodNullable; + windows: z$1.ZodArray; + cost: z$1.ZodOptional>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"not_installed">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"unauthenticated">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"expired">; + }, z$1.core.$strip>, z$1.ZodObject<{ + status: z$1.ZodLiteral<"error">; + message: z$1.ZodString; + }, z$1.core.$strip>], "status">; + }, z$1.core.$strip>, "onlineRpc", true>; + "provider_cli.status": HostDaemonCommandDescriptor<"provider_cli.status", z$1.ZodObject<{ + type: z$1.ZodLiteral<"provider_cli.status">; + }, z$1.core.$strict>, z$1.ZodRecord, z$1.ZodObject<{ + displayName: z$1.ZodString; + executableName: z$1.ZodString; + executablePath: z$1.ZodNullable; + installed: z$1.ZodBoolean; + installSource: z$1.ZodEnum<{ + external: "external"; + notInstalled: "notInstalled"; + npmGlobal: "npmGlobal"; + }>; + currentVersion: z$1.ZodNullable; + latestVersion: z$1.ZodNullable; + minimumSupportedVersion: z$1.ZodNullable; + npmPackageName: z$1.ZodNullable; + npmGlobalPackageVersion: z$1.ZodNullable; + installAction: z$1.ZodNullable; + label: z$1.ZodEnum<{ + Install: "Install"; + Update: "Update"; + }>; + commandKind: z$1.ZodEnum<{ + exec: "exec"; + shell: "shell"; + }>; + command: z$1.ZodString; + }, z$1.core.$strip>>; + needsUpdate: z$1.ZodBoolean; + versionUnsupported: z$1.ZodBoolean; + }, z$1.core.$strip>>, "onlineRpc", true>; + "provider_cli.install": HostDaemonCommandDescriptor<"provider_cli.install", z$1.ZodObject<{ + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + actionKind: z$1.ZodEnum<{ + install: "install"; + update: "update"; + }>; + type: z$1.ZodLiteral<"provider_cli.install">; + }, z$1.core.$strict>, z$1.ZodObject<{ + events: z$1.ZodArray; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + command: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"output">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + stream: z$1.ZodEnum<{ + stdout: "stdout"; + stderr: "stderr"; + }>; + text: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"completed">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + exitCode: z$1.ZodNullable; + signal: z$1.ZodNullable; + success: z$1.ZodBoolean; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"error">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + message: z$1.ZodString; + }, z$1.core.$strip>], "type">>; + }, z$1.core.$strict>, "onlineRpc", false>; + "workspace.status": HostDaemonCommandDescriptor<"workspace.status", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.status">; + mergeBaseBranch: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + workspaceStatus: z$1.ZodObject<{ + workingTree: z$1.ZodObject<{ + insertions: z$1.ZodNumber; + deletions: z$1.ZodNumber; + files: z$1.ZodArray; + insertions: z$1.ZodNullable; + deletions: z$1.ZodNullable; + }, z$1.core.$strip>>; + hasUncommittedChanges: z$1.ZodBoolean; + state: z$1.ZodEnum<{ + clean: "clean"; + untracked: "untracked"; + dirty_uncommitted: "dirty_uncommitted"; + committed_unmerged: "committed_unmerged"; + dirty_and_committed_unmerged: "dirty_and_committed_unmerged"; + }>; + }, z$1.core.$strip>; + checkout: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"branch">; + branchName: z$1.ZodString; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"detached">; + headSha: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unborn">; + branchName: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"unknown">; + reason: z$1.ZodString; + }, z$1.core.$strip>], "kind">; + branch: z$1.ZodObject<{ + currentBranch: z$1.ZodNullable; + defaultBranch: z$1.ZodString; + }, z$1.core.$strip>; + mergeBase: z$1.ZodNullable; + insertions: z$1.ZodNullable; + deletions: z$1.ZodNullable; + }, z$1.core.$strip>>; + mergeBaseBranch: z$1.ZodString; + baseRef: z$1.ZodNullable; + aheadCount: z$1.ZodNumber; + behindCount: z$1.ZodNumber; + hasCommittedUnmergedChanges: z$1.ZodBoolean; + commits: z$1.ZodArray>; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + unknown: "unknown"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; + }, z$1.core.$strict>], "outcome">, "onlineRpc", true>; + "workspace.diff": HostDaemonCommandDescriptor<"workspace.diff", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.diff">; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"uncommitted">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"branch_committed">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"all">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; + }, z$1.core.$strip>], "type">; + maxDiffBytes: z$1.ZodNumber; + maxFileListBytes: z$1.ZodNumber; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + diff: z$1.ZodObject<{ + diff: z$1.ZodString; + truncated: z$1.ZodBoolean; + shortstat: z$1.ZodString; + files: z$1.ZodString; + mergeBaseRef: z$1.ZodNullable; + }, z$1.core.$strip>; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + unknown: "unknown"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; + }, z$1.core.$strict>], "outcome">, "onlineRpc", true>; + "workspace.diffFiles": HostDaemonCommandDescriptor<"workspace.diffFiles", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.diffFiles">; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"uncommitted">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"branch_committed">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"all">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; + }, z$1.core.$strip>], "type">; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + files: z$1.ZodArray; + statusLetter: z$1.ZodEnum<{ + M: "M"; + A: "A"; + D: "D"; + R: "R"; + C: "C"; + T: "T"; + }>; + additions: z$1.ZodNumber; + deletions: z$1.ZodNumber; + binary: z$1.ZodBoolean; + origin: z$1.ZodEnum<{ + untracked: "untracked"; + tracked: "tracked"; + }>; + }, z$1.core.$strip>>; + shortstat: z$1.ZodString; + mergeBaseRef: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + unknown: "unknown"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; + }, z$1.core.$strict>], "outcome">, "onlineRpc", true>; + "workspace.diffPatch": HostDaemonCommandDescriptor<"workspace.diffPatch", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.diffPatch">; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"uncommitted">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"branch_committed">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"all">; + mergeBaseBranch: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"commit">; + sha: z$1.ZodString; + }, z$1.core.$strip>], "type">; + paths: z$1.ZodArray; + maxBytesPerFile: z$1.ZodNumber; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + patches: z$1.ZodArray>; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + failure: z$1.ZodObject<{ + code: z$1.ZodEnum<{ + path_not_found: "path_not_found"; + not_git_repo: "not_git_repo"; + not_worktree: "not_worktree"; + workspace_type_mismatch: "workspace_type_mismatch"; + permission_denied: "permission_denied"; + unknown_environment: "unknown_environment"; + unknown: "unknown"; + }>; + workspacePath: z$1.ZodString; + message: z$1.ZodString; + }, z$1.core.$strict>; + }, z$1.core.$strict>], "outcome">, "onlineRpc", true>; + "workspace.pull_request": HostDaemonCommandDescriptor<"workspace.pull_request", z$1.ZodObject<{ + environmentId: z$1.ZodString; + workspaceContext: z$1.ZodObject<{ + workspacePath: z$1.ZodString; + workspaceProvisionType: z$1.ZodEnum<{ + unmanaged: "unmanaged"; + "managed-worktree": "managed-worktree"; + personal: "personal"; + }>; + }, z$1.core.$strip>; + type: z$1.ZodLiteral<"workspace.pull_request">; + }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"available">; + pullRequest: z$1.ZodObject<{ + number: z$1.ZodNumber; + title: z$1.ZodString; + state: z$1.ZodEnum<{ + OPEN: "OPEN"; + CLOSED: "CLOSED"; + MERGED: "MERGED"; + }>; + url: z$1.ZodString; + isDraft: z$1.ZodBoolean; + baseRefName: z$1.ZodString; + headRefName: z$1.ZodString; + updatedAt: z$1.ZodString; + checks: z$1.ZodArray; + conclusion: z$1.ZodNullable>; + url: z$1.ZodNullable; + }, z$1.core.$strict>>; + reviewDecision: z$1.ZodNullable>; + reviewRequestCount: z$1.ZodNumber; + mergeStateStatus: z$1.ZodNullable>; + mergeable: z$1.ZodNullable>; + }, z$1.core.$strict>; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"absent">; + }, z$1.core.$strict>, z$1.ZodObject<{ + outcome: z$1.ZodLiteral<"unavailable">; + message: z$1.ZodString; + }, z$1.core.$strict>], "outcome">, "onlineRpc", true>; +}; +type HostDaemonCommandRegistry = typeof hostDaemonCommandRegistry; +type AnyHostDaemonCommandDescriptor = HostDaemonCommandRegistry[keyof HostDaemonCommandRegistry]; +type HostDaemonCommandDescriptorForTransport = Extract; +type HostDaemonResultSchemaMapForTransport = { + [Descriptor in HostDaemonCommandDescriptorForTransport as Descriptor["type"]]: Descriptor["resultSchema"]; +}; +type HostDaemonOnlineRpcResultSchemaMap = HostDaemonResultSchemaMapForTransport<"onlineRpc">; +type HostDaemonOnlineRpcResultByType = { + [K in keyof HostDaemonOnlineRpcResultSchemaMap]: z$1.infer; +}; + +declare const pickFolderResponseSchema: z$1.ZodObject<{ + path: z$1.ZodNullable; +}, z$1.core.$strip>; +type PickFolderResponse = z$1.infer; +declare const pathsExistRequestSchema: z$1.ZodObject<{ + paths: z$1.ZodPipe, z$1.ZodTransform>; +}, z$1.core.$strip>; +type PathsExistRequest = z$1.infer; +declare const pathsExistResponseSchema: z$1.ZodObject<{ + existence: z$1.ZodRecord; +}, z$1.core.$strip>; +type PathsExistResponse = z$1.infer; +declare const providerCliStatusResponseSchema: z$1.ZodRecord, z$1.ZodObject<{ + displayName: z$1.ZodString; + executableName: z$1.ZodString; + executablePath: z$1.ZodNullable; + installed: z$1.ZodBoolean; + installSource: z$1.ZodEnum<{ + external: "external"; + notInstalled: "notInstalled"; + npmGlobal: "npmGlobal"; + }>; + currentVersion: z$1.ZodNullable; + latestVersion: z$1.ZodNullable; + minimumSupportedVersion: z$1.ZodNullable; + npmPackageName: z$1.ZodNullable; + npmGlobalPackageVersion: z$1.ZodNullable; + installAction: z$1.ZodNullable; + label: z$1.ZodEnum<{ + Install: "Install"; + Update: "Update"; + }>; + commandKind: z$1.ZodEnum<{ + exec: "exec"; + shell: "shell"; + }>; + command: z$1.ZodString; + }, z$1.core.$strip>>; + needsUpdate: z$1.ZodBoolean; + versionUnsupported: z$1.ZodBoolean; +}, z$1.core.$strip>>; +type ProviderCliStatusResponse = z$1.infer; +declare const providerCliInstallRequestSchema: z$1.ZodObject<{ + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + actionKind: z$1.ZodEnum<{ + install: "install"; + update: "update"; + }>; +}, z$1.core.$strip>; +type ProviderCliInstallRequest = z$1.infer; +declare const providerCliInstallEventSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"started">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + command: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"output">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + stream: z$1.ZodEnum<{ + stdout: "stdout"; + stderr: "stderr"; + }>; + text: z$1.ZodString; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"completed">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + exitCode: z$1.ZodNullable; + signal: z$1.ZodNullable; + success: z$1.ZodBoolean; +}, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"error">; + provider: z$1.ZodEnum<{ + codex: "codex"; + claudeCode: "claudeCode"; + cursor: "cursor"; + }>; + message: z$1.ZodString; +}, z$1.core.$strip>], "type">; +type ProviderCliInstallEvent = z$1.infer; + +interface CreateFilePreviewResponse { + baseUrl: string; + expiresAtMs: number; +} +type HostFileReadResponse = HostDaemonOnlineRpcResultByType["host.read_file"]; +type HostFileWriteResponse = HostDaemonOnlineRpcResultByType["host.write_file"]; +type HostFileListResponse = HostDaemonOnlineRpcResultByType["host.list_files"]; +type HostPathListResponse = HostDaemonOnlineRpcResultByType["host.list_paths"]; +type HostMkdirResponse = HostDaemonOnlineRpcResultByType["host.mkdir"]; +type HostMovePathResponse = HostDaemonOnlineRpcResultByType["host.move_path"]; +type HostRemovePathResponse = HostDaemonOnlineRpcResultByType["host.remove_path"]; + +/** + * Query for `GET /hosts/:id/directory`, the interactive path browser's + * single-level directory read. `path` is an absolute directory on the host; + * omitting it lists the host's home directory (the daemon resolves it, since a + * remote caller cannot know the host's home). + */ +declare const hostDirectoryQuerySchema: z$1.ZodObject<{ + path: z$1.ZodOptional; +}, z$1.core.$strip>; +type HostDirectoryQuery = z$1.infer; +declare const hostDirectoryListingSchema: z$1.ZodObject<{ + directory: z$1.ZodString; + parent: z$1.ZodNullable; + entries: z$1.ZodArray; + name: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type HostDirectoryListing = z$1.infer; +/** Project name is sent so the daemon can derive its host-local checkout path. */ +declare const hostCloneDefaultPathQuerySchema: z$1.ZodObject<{ + projectId: z$1.ZodString; +}, z$1.core.$strip>; +type HostCloneDefaultPathQuery = z$1.infer; +declare const hostCloneDefaultPathResponseSchema: z$1.ZodObject<{ + path: z$1.ZodString; +}, z$1.core.$strict>; +type HostCloneDefaultPathResponse = z$1.infer; +declare const createHostJoinCodeResponseSchema: z$1.ZodObject<{ + joinCode: z$1.ZodString; + hostId: z$1.ZodString; + expiresAt: z$1.ZodNumber; +}, z$1.core.$strip>; +type CreateHostJoinCodeResponse = z$1.infer; +declare const updateHostRequestSchema: z$1.ZodObject<{ + name: z$1.ZodString; +}, z$1.core.$strict>; +type UpdateHostRequest = z$1.infer; +declare const hostRetryUpdateResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; +}, z$1.core.$strict>; +type HostRetryUpdateResponse = z$1.infer; +type HostPathsExistRequest = PathsExistRequest; +type HostPathsExistResponse = PathsExistResponse; +declare const hostPickFolderRequestSchema: z$1.ZodObject<{ + clientHostId: z$1.ZodString; +}, z$1.core.$strict>; +type HostPickFolderRequest = z$1.infer; +type HostPickFolderResponse = PickFolderResponse; +type HostProviderCliStatusResponse = ProviderCliStatusResponse; +type HostProviderCliInstallRequest = ProviderCliInstallRequest; +type HostProviderCliInstallEvent = ProviderCliInstallEvent; + +declare const pluginUpdateCheckEntrySchema: z$1.ZodObject<{ + id: z$1.ZodString; + outcome: z$1.ZodEnum<{ + unavailable: "unavailable"; + incompatible: "incompatible"; + current: "current"; + "update-available": "update-available"; + pinned: "pinned"; + }>; + devMode: z$1.ZodOptional>; + installed: z$1.ZodObject<{ + version: z$1.ZodString; + display: z$1.ZodString; + }, z$1.core.$strip>; + candidate: z$1.ZodOptional>; + blocked: z$1.ZodOptional; + }, z$1.core.$strip>>; + detail: z$1.ZodOptional; +}, z$1.core.$strip>; +type PluginUpdateCheckEntry = z$1.infer; +declare const pluginApplyUpdateResultSchema: z$1.ZodObject<{ + applied: z$1.ZodBoolean; + from: z$1.ZodObject<{ + version: z$1.ZodString; + display: z$1.ZodString; + }, z$1.core.$strip>; + to: z$1.ZodOptional>; + outcome: z$1.ZodEnum<{ + current: "current"; + updated: "updated"; + "rolled-back": "rolled-back"; + }>; + detail: z$1.ZodOptional; +}, z$1.core.$strip>; +type PluginApplyUpdateResult$1 = z$1.infer; +declare const pluginSourceDetailSchema: z$1.ZodObject<{ + requested: z$1.ZodString; + resolved: z$1.ZodString; + integrity: z$1.ZodOptional; + registry: z$1.ZodOptional; + engines: z$1.ZodObject<{ + bb: z$1.ZodOptional; + bbPluginSdk: z$1.ZodOptional; + }, z$1.core.$strip>; + installedAt: z$1.ZodOptional; + history: z$1.ZodArray>; +}, z$1.core.$strip>; +type PluginSourceDetail = z$1.infer; +declare const installedPluginSchema: z$1.ZodObject<{ + id: z$1.ZodString; + source: z$1.ZodString; + rootDir: z$1.ZodString; + version: z$1.ZodString; + provenance: z$1.ZodEnum<{ + builtin: "builtin"; + direct: "direct"; + catalog: "catalog"; + }>; + isOrphanedBuiltin: z$1.ZodBoolean; + catalogEntryId: z$1.ZodOptional; + sourceDisplay: z$1.ZodString; + updateState: z$1.ZodObject<{ + outcome: z$1.ZodOptional>; + availableVersion: z$1.ZodOptional; + blockedVersion: z$1.ZodOptional; + blockedReasons: z$1.ZodOptional>; + lastCheckAt: z$1.ZodOptional; + lastFailure: z$1.ZodOptional>; + }, z$1.core.$strip>; + enabled: z$1.ZodBoolean; + description: z$1.ZodNullable; + name: z$1.ZodNullable; + icon: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + running: "running"; + missing: "missing"; + incompatible: "incompatible"; + disabled: "disabled"; + degraded: "degraded"; + "needs-configuration": "needs-configuration"; + }>; + statusDetail: z$1.ZodNullable; + handlerStats: z$1.ZodObject<{ + count: z$1.ZodNumber; + totalMs: z$1.ZodNumber; + maxMs: z$1.ZodNumber; + errorCount: z$1.ZodNumber; + }, z$1.core.$strip>; + services: z$1.ZodArray; + }, z$1.core.$strip>>; + schedules: z$1.ZodArray; + lastStatus: z$1.ZodNullable>; + lastError: z$1.ZodNullable; + }, z$1.core.$strip>>; + cliCommand: z$1.ZodNullable>; + hasSettings: z$1.ZodBoolean; + app: z$1.ZodObject<{ + hasApp: z$1.ZodBoolean; + bundle: z$1.ZodNullable; + hash: z$1.ZodString; + sdkMajor: z$1.ZodNumber; + sdkVersion: z$1.ZodString; + compatible: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; + logoUrl: z$1.ZodNullable; + logoDarkUrl: z$1.ZodNullable; +}, z$1.core.$strip>; +type InstalledPlugin = z$1.infer; +declare const pluginListResponseSchema: z$1.ZodObject<{ + enabled: z$1.ZodBoolean; + plugins: z$1.ZodArray; + isOrphanedBuiltin: z$1.ZodBoolean; + catalogEntryId: z$1.ZodOptional; + sourceDisplay: z$1.ZodString; + updateState: z$1.ZodObject<{ + outcome: z$1.ZodOptional>; + availableVersion: z$1.ZodOptional; + blockedVersion: z$1.ZodOptional; + blockedReasons: z$1.ZodOptional>; + lastCheckAt: z$1.ZodOptional; + lastFailure: z$1.ZodOptional>; + }, z$1.core.$strip>; + enabled: z$1.ZodBoolean; + description: z$1.ZodNullable; + name: z$1.ZodNullable; + icon: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + running: "running"; + missing: "missing"; + incompatible: "incompatible"; + disabled: "disabled"; + degraded: "degraded"; + "needs-configuration": "needs-configuration"; + }>; + statusDetail: z$1.ZodNullable; + handlerStats: z$1.ZodObject<{ + count: z$1.ZodNumber; + totalMs: z$1.ZodNumber; + maxMs: z$1.ZodNumber; + errorCount: z$1.ZodNumber; + }, z$1.core.$strip>; + services: z$1.ZodArray; + }, z$1.core.$strip>>; + schedules: z$1.ZodArray; + lastStatus: z$1.ZodNullable>; + lastError: z$1.ZodNullable; + }, z$1.core.$strip>>; + cliCommand: z$1.ZodNullable>; + hasSettings: z$1.ZodBoolean; + app: z$1.ZodObject<{ + hasApp: z$1.ZodBoolean; + bundle: z$1.ZodNullable; + hash: z$1.ZodString; + sdkMajor: z$1.ZodNumber; + sdkVersion: z$1.ZodString; + compatible: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; + logoUrl: z$1.ZodNullable; + logoDarkUrl: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type PluginListResponse = z$1.infer; +declare const pluginReloadResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + plugins: z$1.ZodArray; + isOrphanedBuiltin: z$1.ZodBoolean; + catalogEntryId: z$1.ZodOptional; + sourceDisplay: z$1.ZodString; + updateState: z$1.ZodObject<{ + outcome: z$1.ZodOptional>; + availableVersion: z$1.ZodOptional; + blockedVersion: z$1.ZodOptional; + blockedReasons: z$1.ZodOptional>; + lastCheckAt: z$1.ZodOptional; + lastFailure: z$1.ZodOptional>; + }, z$1.core.$strip>; + enabled: z$1.ZodBoolean; + description: z$1.ZodNullable; + name: z$1.ZodNullable; + icon: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + running: "running"; + missing: "missing"; + incompatible: "incompatible"; + disabled: "disabled"; + degraded: "degraded"; + "needs-configuration": "needs-configuration"; + }>; + statusDetail: z$1.ZodNullable; + handlerStats: z$1.ZodObject<{ + count: z$1.ZodNumber; + totalMs: z$1.ZodNumber; + maxMs: z$1.ZodNumber; + errorCount: z$1.ZodNumber; + }, z$1.core.$strip>; + services: z$1.ZodArray; + }, z$1.core.$strip>>; + schedules: z$1.ZodArray; + lastStatus: z$1.ZodNullable>; + lastError: z$1.ZodNullable; + }, z$1.core.$strip>>; + cliCommand: z$1.ZodNullable>; + hasSettings: z$1.ZodBoolean; + app: z$1.ZodObject<{ + hasApp: z$1.ZodBoolean; + bundle: z$1.ZodNullable; + hash: z$1.ZodString; + sdkMajor: z$1.ZodNumber; + sdkVersion: z$1.ZodString; + compatible: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>; + logoUrl: z$1.ZodNullable; + logoDarkUrl: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type PluginReloadResponse = z$1.infer; +declare const pluginRemoveResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; +}, z$1.core.$strip>; +type PluginRemoveResponse = z$1.infer; +declare const pluginSettingsResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + schema: z$1.ZodRecord; + secret: z$1.ZodOptional>; + default: z$1.ZodOptional; + label: z$1.ZodString; + description: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"boolean">; + default: z$1.ZodOptional; + label: z$1.ZodString; + description: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"select">; + options: z$1.ZodArray; + default: z$1.ZodOptional; + label: z$1.ZodString; + description: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"project">; + default: z$1.ZodOptional; + label: z$1.ZodString; + description: z$1.ZodOptional; + }, z$1.core.$strict>], "type">>; + values: z$1.ZodRecord>>; +}, z$1.core.$strip>; +type PluginSettingsResponse = z$1.infer; +declare const pluginTokenResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + token: z$1.ZodString; +}, z$1.core.$strip>; +type PluginTokenResponse = z$1.infer; +declare const pluginCatalogStatusSchema: z$1.ZodObject<{ + pluginCount: z$1.ZodNumber; +}, z$1.core.$strip>; +type PluginCatalogStatus = z$1.infer; +declare const pluginCatalogSearchResultSchema: z$1.ZodObject<{ + entryId: z$1.ZodString; + displayName: z$1.ZodString; + description: z$1.ZodString; + icon: z$1.ZodNullable; + category: z$1.ZodString; + source: z$1.ZodString; + installed: z$1.ZodBoolean; + compatible: z$1.ZodBoolean; + incompatibleReason: z$1.ZodNullable; +}, z$1.core.$strip>; +type PluginCatalogSearchResult$1 = z$1.infer; + +declare const systemExecutionOptionsResponseSchema: z$1.ZodObject<{ + providers: z$1.ZodArray; + capabilities: z$1.ZodObject<{ + supportsArchive: z$1.ZodBoolean; + supportsRename: z$1.ZodBoolean; + supportsServiceTier: z$1.ZodBoolean; + supportsUserQuestion: z$1.ZodBoolean; + supportsFork: z$1.ZodBoolean; + supportedPermissionModes: z$1.ZodArray>; + }, z$1.core.$strip>; + composerActions: z$1.ZodArray; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plan">; + command: z$1.ZodObject<{ + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + trailingText: z$1.ZodString; + }, z$1.core.$strip>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"goal">; + command: z$1.ZodObject<{ + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + trailingText: z$1.ZodString; + }, z$1.core.$strip>; + }, z$1.core.$strip>], "kind">>; + available: z$1.ZodBoolean; + }, z$1.core.$strip>>; + models: z$1.ZodArray; + description: z$1.ZodString; + }, z$1.core.$strip>>; + defaultReasoningEffort: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + isDefault: z$1.ZodBoolean; + }, z$1.core.$strip>>; + selectedOnlyModels: z$1.ZodArray; + description: z$1.ZodString; + }, z$1.core.$strip>>; + defaultReasoningEffort: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + isDefault: z$1.ZodBoolean; + }, z$1.core.$strip>>; + modelLoadError: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type SystemExecutionOptionsResponse = z$1.infer; +declare const systemExecutionOptionsQuerySchema: z$1.ZodObject<{ + providerId: z$1.ZodOptional; + hostId: z$1.ZodOptional; + environmentId: z$1.ZodOptional; +}, z$1.core.$strip>; +type SystemExecutionOptionsQuery = z$1.infer; +/** Omission preserves the existing behavior of reading the primary machine. */ +declare const systemUsageLimitsQuerySchema: z$1.ZodObject<{ + hostId: z$1.ZodOptional; +}, z$1.core.$strip>; +type SystemUsageLimitsQuery = z$1.infer; +declare const systemVoiceTranscriptionResponseSchema: z$1.ZodObject<{ + text: z$1.ZodString; +}, z$1.core.$strip>; +type SystemVoiceTranscriptionResponse = z$1.infer; +declare const systemConfigResponseSchema: z$1.ZodObject<{ + generalSettings: z$1.ZodObject<{ + caffeinate: z$1.ZodBoolean; + showKeyboardHints: z$1.ZodBoolean; + showUnhandledProviderEvents: z$1.ZodBoolean; + codexMemoryEnabled: z$1.ZodBoolean; + claudeCodeMemoryEnabled: z$1.ZodBoolean; + codexSubagentsDisabled: z$1.ZodBoolean; + claudeCodeSubagentsDisabled: z$1.ZodBoolean; + claudeCodeWorkflowsDisabled: z$1.ZodBoolean; + }, z$1.core.$strict>; + keybindings: z$1.ZodArray; + desktopOnly: z$1.ZodBoolean; + shortcut: z$1.ZodObject<{ + key: z$1.ZodString; + mod: z$1.ZodBoolean; + meta: z$1.ZodBoolean; + control: z$1.ZodBoolean; + alt: z$1.ZodBoolean; + shift: z$1.ZodBoolean; + }, z$1.core.$strict>; + when: z$1.ZodObject<{ + all: z$1.ZodArray>; + none: z$1.ZodArray>; + }, z$1.core.$strict>; + }, z$1.core.$strict>>; + defaultKeybindings: z$1.ZodArray; + desktopOnly: z$1.ZodBoolean; + shortcut: z$1.ZodObject<{ + key: z$1.ZodString; + mod: z$1.ZodBoolean; + meta: z$1.ZodBoolean; + control: z$1.ZodBoolean; + alt: z$1.ZodBoolean; + shift: z$1.ZodBoolean; + }, z$1.core.$strict>; + when: z$1.ZodObject<{ + all: z$1.ZodArray>; + none: z$1.ZodArray>; + }, z$1.core.$strict>; + }, z$1.core.$strict>>; + keybindingOverrides: z$1.ZodArray; + shortcut: z$1.ZodNullable>; + }, z$1.core.$strict>>; + experiments: z$1.ZodObject<{ + claudeCodeMockCliTraffic: z$1.ZodBoolean; + plugins: z$1.ZodBoolean; + sideChatPlugin: z$1.ZodBoolean; + }, z$1.core.$strip>; + appearance: z$1.ZodObject<{ + themeId: z$1.ZodString; + customCss: z$1.ZodNullable; + faviconColor: z$1.ZodEnum<{ + default: "default"; + red: "red"; + orange: "orange"; + yellow: "yellow"; + green: "green"; + teal: "teal"; + blue: "blue"; + purple: "purple"; + pink: "pink"; + }>; + }, z$1.core.$strip>; + customThemes: z$1.ZodArray; + pluginThemes: z$1.ZodArray; + }, z$1.core.$strip>>; + featureFlags: z$1.ZodObject<{ + placeholder: z$1.ZodBoolean; + }, z$1.core.$strip>; + hostDaemonPort: z$1.ZodNullable; + primaryHostId: z$1.ZodNullable; + primaryHostPlatform: z$1.ZodNullable>; + voiceTranscriptionEnabled: z$1.ZodBoolean; + dataDir: z$1.ZodString; +}, z$1.core.$strip>; +type SystemConfigResponse = z$1.infer; +declare const systemAttentionResponseSchema: z$1.ZodObject<{ + hasAttention: z$1.ZodBoolean; +}, z$1.core.$strip>; +type SystemAttentionResponse = z$1.infer; +/** + * Theme catalog: the on-disk custom-theme directory plus the discovered custom + * themes and the active palette. Drives `bb theme list` / `bb theme dir`. + */ +declare const themeCatalogResponseSchema: z$1.ZodObject<{ + dir: z$1.ZodString; + custom: z$1.ZodArray; + plugins: z$1.ZodArray; + }, z$1.core.$strip>>; + active: z$1.ZodObject<{ + themeId: z$1.ZodString; + customCss: z$1.ZodNullable; + faviconColor: z$1.ZodEnum<{ + default: "default"; + red: "red"; + orange: "orange"; + yellow: "yellow"; + green: "green"; + teal: "teal"; + blue: "blue"; + purple: "purple"; + pink: "pink"; + }>; + }, z$1.core.$strip>; +}, z$1.core.$strip>; +type ThemeCatalogResponse = z$1.infer; +declare const systemVersionResponseSchema: z$1.ZodObject<{ + currentVersion: z$1.ZodString; + latestVersion: z$1.ZodNullable; + source: z$1.ZodLiteral<"npm">; + updateAvailable: z$1.ZodBoolean; + isDevelopment: z$1.ZodBoolean; + upgradeCommand: z$1.ZodString; +}, z$1.core.$strip>; +type SystemVersionResponse = z$1.infer; +declare const systemConfigReloadResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; +}, z$1.core.$strip>; +type SystemConfigReloadResponse = z$1.infer; + +declare const terminalSessionSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodNullable; + environmentId: z$1.ZodNullable; + hostId: z$1.ZodString; + title: z$1.ZodString; + initialCwd: z$1.ZodString; + cols: z$1.ZodNumber; + rows: z$1.ZodNumber; + status: z$1.ZodEnum<{ + starting: "starting"; + disconnected: "disconnected"; + running: "running"; + exited: "exited"; + }>; + exitCode: z$1.ZodNullable; + closeReason: z$1.ZodNullable>; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + lastUserInputAt: z$1.ZodNullable; +}, z$1.core.$strip>; +type TerminalSession = z$1.infer; +declare const terminalListResponseSchema: z$1.ZodObject<{ + sessions: z$1.ZodArray; + environmentId: z$1.ZodNullable; + hostId: z$1.ZodString; + title: z$1.ZodString; + initialCwd: z$1.ZodString; + cols: z$1.ZodNumber; + rows: z$1.ZodNumber; + status: z$1.ZodEnum<{ + starting: "starting"; + disconnected: "disconnected"; + running: "running"; + exited: "exited"; + }>; + exitCode: z$1.ZodNullable; + closeReason: z$1.ZodNullable>; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + lastUserInputAt: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type TerminalListResponse = z$1.infer; +declare const createTerminalRequestSchema: z$1.ZodObject<{ + cols: z$1.ZodNumber; + rows: z$1.ZodNumber; + start: z$1.ZodOptional; + }, z$1.core.$strict>, z$1.ZodObject<{ + mode: z$1.ZodLiteral<"command">; + command: z$1.ZodString; + }, z$1.core.$strict>], "mode">>; + target: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"environment">; + environmentId: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"host_path">; + hostId: z$1.ZodString; + cwd: z$1.ZodNullable; + }, z$1.core.$strict>], "kind">; + title: z$1.ZodOptional; +}, z$1.core.$strict>; +type CreateTerminalRequest = z$1.infer; +declare const updateTerminalRequestSchema: z$1.ZodObject<{ + title: z$1.ZodString; +}, z$1.core.$strict>; +type UpdateTerminalRequest = z$1.infer; +declare const terminalInputRequestSchema: z$1.ZodObject<{ + dataBase64: z$1.ZodString; +}, z$1.core.$strict>; +type TerminalInputRequest = z$1.infer; +declare const terminalResizeRequestSchema: z$1.ZodObject<{ + cols: z$1.ZodNumber; + rows: z$1.ZodNumber; +}, z$1.core.$strict>; +type TerminalResizeRequest = z$1.infer; +declare const terminalOutputQuerySchema: z$1.ZodObject<{ + sinceSeq: z$1.ZodOptional>; + tailBytes: z$1.ZodOptional>; + limitChunks: z$1.ZodOptional>; +}, z$1.core.$strict>; +type TerminalOutputQuery = z$1.infer; +declare const terminalOutputResponseSchema: z$1.ZodObject<{ + chunks: z$1.ZodArray>; + nextSeq: z$1.ZodNumber; + truncated: z$1.ZodBoolean; +}, z$1.core.$strict>; +type TerminalOutputResponse = z$1.infer; + +declare const timelineRowStatusSchema: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; +}>; +type TimelineRowStatus = z$1.infer; +declare const timelineRowBaseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; +}, z$1.core.$strip>; +type TimelineRowBase = z$1.infer; +declare const timelineConversationRowSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"conversation">; + text: z$1.ZodString; + attachments: z$1.ZodNullable; + localImagePaths: z$1.ZodArray; + localFilePaths: z$1.ZodArray; + }, z$1.core.$strip>>; + role: z$1.ZodLiteral<"user">; + initiator: z$1.ZodEnum<{ + user: "user"; + agent: "agent"; + system: "system"; + }>; + senderThreadId: z$1.ZodNullable; + systemMessageKind: z$1.ZodEnum<{ + "ownership-assigned": "ownership-assigned"; + "ownership-removed": "ownership-removed"; + "child-needs-attention": "child-needs-attention"; + "child-completed": "child-completed"; + "child-failed": "child-failed"; + "child-interrupted": "child-interrupted"; + "child-outcome-batch": "child-outcome-batch"; + unlabeled: "unlabeled"; + }>; + systemMessageSubject: z$1.ZodNullable; + threadId: z$1.ZodString; + threadName: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread-batch">; + count: z$1.ZodNumber; + }, z$1.core.$strip>], "kind">>; + turnRequest: z$1.ZodObject<{ + kind: z$1.ZodEnum<{ + message: "message"; + steer: "steer"; + }>; + status: z$1.ZodEnum<{ + pending: "pending"; + accepted: "accepted"; + }>; + }, z$1.core.$strip>; + mentions: z$1.ZodArray, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"conversation">; + text: z$1.ZodString; + attachments: z$1.ZodNullable; + localImagePaths: z$1.ZodArray; + localFilePaths: z$1.ZodArray; + }, z$1.core.$strip>>; + role: z$1.ZodLiteral<"assistant">; + turnRequest: z$1.ZodNull; +}, z$1.core.$strip>], "role">; +type TimelineConversationRow = z$1.infer; +declare const timelineSystemRowSchema: z$1.ZodUnion; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"system">; + title: z$1.ZodString; + detail: z$1.ZodNullable; + status: z$1.ZodNullable>; + systemKind: z$1.ZodEnum<{ + error: "error"; + debug: "debug"; + reconnect: "reconnect"; + }>; +}, z$1.core.$strip>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"system">; + title: z$1.ZodString; + detail: z$1.ZodNullable; + status: z$1.ZodNullable>; + systemKind: z$1.ZodLiteral<"operation">; + operationKind: z$1.ZodEnum<{ + generic: "generic"; + compaction: "compaction"; + "thread-provisioning": "thread-provisioning"; + "thread-interrupted": "thread-interrupted"; + "provider-unhandled": "provider-unhandled"; + warning: "warning"; + deprecation: "deprecation"; + }>; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>, z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"system">; + title: z$1.ZodString; + detail: z$1.ZodNullable; + systemKind: z$1.ZodLiteral<"operation">; + operationKind: z$1.ZodLiteral<"parent-change">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + parentChange: z$1.ZodObject<{ + action: z$1.ZodEnum<{ + assign: "assign"; + release: "release"; + transfer: "transfer"; + }>; + previousParentThreadId: z$1.ZodNullable; + previousParentThreadTitle: z$1.ZodNullable; + nextParentThreadId: z$1.ZodNullable; + nextParentThreadTitle: z$1.ZodNullable; + }, z$1.core.$strip>; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>], "operationKind">]>; +type TimelineSystemRow = z$1.infer; +interface TimelineWorkRowBase extends TimelineRowBase { + kind: "work"; + status: TimelineRowStatus; +} +declare const timelineCommandWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"command">; + callId: z$1.ZodString; + command: z$1.ZodString; + cwd: z$1.ZodNullable; + source: z$1.ZodNullable; + output: z$1.ZodString; + exitCode: z$1.ZodNullable; + completedAt: z$1.ZodNullable; + approvalStatus: z$1.ZodNullable>; + activityIntents: z$1.ZodArray; + command: z$1.ZodString; + name: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"list_files">; + command: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"search">; + command: z$1.ZodString; + query: z$1.ZodNullable; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"unknown">; + command: z$1.ZodString; + }, z$1.core.$strip>], "type">>; +}, z$1.core.$strip>; +type TimelineCommandWorkRow = z$1.infer; +declare const timelineToolWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"tool">; + callId: z$1.ZodString; + toolName: z$1.ZodString; + toolArgs: z$1.ZodNullable>>>; + output: z$1.ZodString; + completedAt: z$1.ZodNullable; + approvalStatus: z$1.ZodNullable>; + activityIntents: z$1.ZodArray; + command: z$1.ZodString; + name: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"list_files">; + command: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"search">; + command: z$1.ZodString; + query: z$1.ZodNullable; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"unknown">; + command: z$1.ZodString; + }, z$1.core.$strip>], "type">>; +}, z$1.core.$strip>; +type TimelineToolWorkRow = z$1.infer; +declare const timelineFileChangeWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"file-change">; + callId: z$1.ZodString; + change: z$1.ZodObject<{ + path: z$1.ZodString; + kind: z$1.ZodNullable; + movePath: z$1.ZodNullable; + diff: z$1.ZodNullable; + diffStats: z$1.ZodObject<{ + added: z$1.ZodNumber; + removed: z$1.ZodNumber; + }, z$1.core.$strip>; + }, z$1.core.$strip>; + stdout: z$1.ZodNullable; + stderr: z$1.ZodNullable; + approvalStatus: z$1.ZodNullable>; +}, z$1.core.$strip>; +type TimelineFileChangeWorkRow = z$1.infer; +declare const timelineWebSearchWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"web-search">; + callId: z$1.ZodString; + queries: z$1.ZodArray; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>; +type TimelineWebSearchWorkRow = z$1.infer; +declare const timelineWebFetchWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"web-fetch">; + callId: z$1.ZodString; + url: z$1.ZodString; + prompt: z$1.ZodNullable; + pattern: z$1.ZodNullable; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>; +type TimelineWebFetchWorkRow = z$1.infer; +declare const timelineImageViewWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"image-view">; + callId: z$1.ZodString; + path: z$1.ZodString; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>; +type TimelineImageViewWorkRow = z$1.infer; +declare const timelineApprovalWorkRowSchema: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"approval">; + interactionId: z$1.ZodString; + target: z$1.ZodObject<{ + itemId: z$1.ZodString; + toolName: z$1.ZodNullable; + }, z$1.core.$strip>; + approvalKind: z$1.ZodLiteral<"file-edit">; + lifecycle: z$1.ZodEnum<{ + denied: "denied"; + waiting: "waiting"; + }>; +}, z$1.core.$strip>, z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"approval">; + interactionId: z$1.ZodString; + target: z$1.ZodObject<{ + itemId: z$1.ZodString; + toolName: z$1.ZodNullable; + }, z$1.core.$strip>; + approvalKind: z$1.ZodLiteral<"permission-grant">; + lifecycle: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + denied: "denied"; + resolving: "resolving"; + granted: "granted"; + }>; + grantScope: z$1.ZodNullable>; + statusReason: z$1.ZodNullable; +}, z$1.core.$strip>], "approvalKind">; +type TimelineApprovalWorkRow = z$1.infer; +declare const timelineQuestionWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"question">; + interactionId: z$1.ZodString; + lifecycle: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + answered: "answered"; + }>; + questions: z$1.ZodArray; + multiSelect: z$1.ZodBoolean; + options: z$1.ZodOptional; + }, z$1.core.$strip>>>; + allowFreeText: z$1.ZodBoolean; + }, z$1.core.$strip>>; + answers: z$1.ZodNullable; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>>; + statusReason: z$1.ZodNullable; +}, z$1.core.$strip>; +type TimelineQuestionWorkRow = z$1.infer; +interface TimelineDelegationWorkRow extends TimelineWorkRowBase { + workKind: "delegation"; + callId: string; + toolName: string; + subagentType: string | null; + description: string | null; + output: string; + completedAt: number | null; + childRows: TimelineRow[]; +} +/** + * A provider background task — a dynamic workflow (Claude Code Workflow tool) + * or a backgrounded shell command (Bash run_in_background), discriminated by + * `taskType`. The row outlives its spawning turn: progress and terminal state + * arrive via thread-scoped events folded into this single row. `workflow` is + * the merged phase/agent tree, present only for workflows; null for shell + * commands and for workflows the provider reported no progress records for + * (degraded rendering falls back to description + summary). + */ +declare const timelineWorkflowWorkRowSchema: z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + turnId: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"workflow">; + itemId: z$1.ZodString; + taskType: z$1.ZodString; + workflowName: z$1.ZodNullable; + description: z$1.ZodString; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + running: "running"; + paused: "paused"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + workflow: z$1.ZodNullable; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodNullable>; + summary: z$1.ZodNullable; + error: z$1.ZodNullable; + completedAt: z$1.ZodNullable; +}, z$1.core.$strip>; +type TimelineWorkflowWorkRow = z$1.infer; +type TimelineWorkRow = TimelineCommandWorkRow | TimelineToolWorkRow | TimelineFileChangeWorkRow | TimelineWebSearchWorkRow | TimelineWebFetchWorkRow | TimelineImageViewWorkRow | TimelineApprovalWorkRow | TimelineQuestionWorkRow | TimelineDelegationWorkRow | TimelineWorkflowWorkRow; +interface TimelineTurnRow extends TimelineRowBase { + kind: "turn"; + turnId: string; + status: TimelineRowStatus; + summaryCount: number; + completedAt: number | null; + children: TimelineRow[] | null; +} +type TimelineSourceRow = TimelineConversationRow | TimelineWorkRow | TimelineSystemRow; +type TimelineRow = TimelineSourceRow | TimelineTurnRow; + +declare const createThreadRequestSchema: z$1.ZodObject<{ + projectId: z$1.ZodString; + providerId: z$1.ZodOptional; + origin: z$1.ZodEnum<{ + plugin: "plugin"; + app: "app"; + cli: "cli"; + sdk: "sdk"; + }>; + originPluginId: z$1.ZodOptional; + visibility: z$1.ZodOptional>; + title: z$1.ZodOptional; + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodOptional; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>; + executionInputSources: z$1.ZodOptional>; + model: z$1.ZodOptional>; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional>; + }, z$1.core.$strict>>; + environment: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"reuse">; + environmentId: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"host">; + hostId: z$1.ZodOptional; + workspace: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + type: z$1.ZodLiteral<"unmanaged">; + path: z$1.ZodNullable; + branch: z$1.ZodOptional; + name: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"new">; + baseBranch: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"managed-worktree">; + baseBranch: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"named">; + name: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"default">; + }, z$1.core.$strip>], "kind">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"personal">; + }, z$1.core.$strip>], "type">; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"project-default">; + }, z$1.core.$strip>], "type">; + parentThreadId: z$1.ZodOptional; + sectionId: z$1.ZodOptional>; + sourceThreadId: z$1.ZodOptional; + sourceSeqEnd: z$1.ZodOptional; + startedOnBehalfOf: z$1.ZodDefault; + senderThreadId: z$1.ZodString; + }, z$1.core.$strip>>>; + originKind: z$1.ZodDefault>>; + childOrigin: z$1.ZodDefault>>; +}, z$1.core.$strip>; +type CreateThreadRequest = z$1.infer; +declare const forkThreadRequestSchema: z$1.ZodObject<{ + sourceThreadId: z$1.ZodString; + sourceSeqEnd: z$1.ZodOptional; + input: z$1.ZodOptional>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>>; + agentContextSeed: z$1.ZodOptional>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">, z$1.ZodObject<{ + visibility: z$1.ZodLiteral<"agent-only">; + }, z$1.core.$strip>>>>; + title: z$1.ZodOptional; + permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>; + visibility: z$1.ZodDefault>; + workspace: z$1.ZodDefault>; + origin: z$1.ZodDefault>; + originPluginId: z$1.ZodOptional; +}, z$1.core.$strip>; +type ForkThreadRequest = z$1.infer; +declare const sendMessageRequestSchema: z$1.ZodObject<{ + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodOptional; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>; + executionInputSources: z$1.ZodOptional>; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional>; + }, z$1.core.$strict>>; + mode: z$1.ZodEnum<{ + steer: "steer"; + start: "start"; + auto: "auto"; + "queue-if-active": "queue-if-active"; + "steer-if-active": "steer-if-active"; + }>; + senderThreadId: z$1.ZodOptional; +}, z$1.core.$strip>; +type SendMessageRequest = z$1.infer; +declare const createQueuedMessageRequestSchema: z$1.ZodObject<{ + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodOptional; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional, z$1.ZodLiteral<"workspace-write">]>, z$1.ZodTransform<"accept-edits" | "auto" | "full", "accept-edits" | "auto" | "full" | "workspace-write">>>; + executionInputSources: z$1.ZodOptional>; + serviceTier: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>; + permissionMode: z$1.ZodOptional>; + }, z$1.core.$strict>>; + senderThreadId: z$1.ZodOptional; +}, z$1.core.$strip>; +type CreateQueuedMessageRequest = z$1.infer; +declare const updateQueuedMessageRequestSchema: z$1.ZodObject<{ + expectedUpdatedAt: z$1.ZodNumber; + input: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; +}, z$1.core.$strip>; +type UpdateQueuedMessageRequest = z$1.infer; +declare const sendQueuedMessageRequestSchema: z$1.ZodObject<{ + mode: z$1.ZodEnum<{ + steer: "steer"; + auto: "auto"; + }>; +}, z$1.core.$strip>; +type SendQueuedMessageRequest = z$1.infer; +declare const reorderQueuedMessageRequestSchema: z$1.ZodObject<{ + previousQueuedMessageId: z$1.ZodNullable; + nextQueuedMessageId: z$1.ZodNullable; + groupBoundaryQueuedMessageId: z$1.ZodOptional; +}, z$1.core.$strip>; +type ReorderQueuedMessageRequest = z$1.infer; +declare const setQueuedMessageGroupBoundaryRequestSchema: z$1.ZodObject<{ + expectedGroupedPrefixQueuedMessageIds: z$1.ZodArray; + groupBoundaryQueuedMessageId: z$1.ZodString; +}, z$1.core.$strip>; +type SetQueuedMessageGroupBoundaryRequest = z$1.infer; +declare const sendQueuedMessageResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + queuedMessage: z$1.ZodObject<{ + id: z$1.ZodString; + content: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodString; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + "accept-edits": "accept-edits"; + auto: "auto"; + full: "full"; + }>; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + groupWithNext: z$1.ZodBoolean; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + }, z$1.core.$strip>; +}, z$1.core.$strip>; +type SendQueuedMessageResponse = z$1.infer; +declare const threadListResponseSchema: z$1.ZodArray; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + activity: z$1.ZodObject<{ + activeWorkflowCount: z$1.ZodNumber; + activeBackgroundAgentCount: z$1.ZodNumber; + activeBackgroundCommandCount: z$1.ZodNumber; + activePlanModeCount: z$1.ZodNumber; + activeGoalCount: z$1.ZodNumber; + }, z$1.core.$strip>; + pinSortKey: z$1.ZodNullable; + hasPendingInteraction: z$1.ZodBoolean; + environmentHostId: z$1.ZodNullable; + environmentName: z$1.ZodNullable; + environmentBranchName: z$1.ZodNullable; + environmentWorkspaceDisplayKind: z$1.ZodEnum<{ + "managed-worktree": "managed-worktree"; + "unmanaged-worktree": "unmanaged-worktree"; + other: "other"; + }>; +}, z$1.core.$strip>>; +type ThreadListResponse = z$1.infer; +declare const threadSearchResponseSchema: z$1.ZodObject<{ + active: z$1.ZodObject<{ + total: z$1.ZodNumber; + results: z$1.ZodArray; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + activity: z$1.ZodObject<{ + activeWorkflowCount: z$1.ZodNumber; + activeBackgroundAgentCount: z$1.ZodNumber; + activeBackgroundCommandCount: z$1.ZodNumber; + activePlanModeCount: z$1.ZodNumber; + activeGoalCount: z$1.ZodNumber; + }, z$1.core.$strip>; + pinSortKey: z$1.ZodNullable; + hasPendingInteraction: z$1.ZodBoolean; + environmentHostId: z$1.ZodNullable; + environmentName: z$1.ZodNullable; + environmentBranchName: z$1.ZodNullable; + environmentWorkspaceDisplayKind: z$1.ZodEnum<{ + "managed-worktree": "managed-worktree"; + "unmanaged-worktree": "unmanaged-worktree"; + other: "other"; + }>; + }, z$1.core.$strip>; + matches: z$1.ZodArray; + text: z$1.ZodString; + highlightRanges: z$1.ZodArray>; + sourceSeq: z$1.ZodNullable; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + }, z$1.core.$strict>; + archived: z$1.ZodObject<{ + total: z$1.ZodNumber; + results: z$1.ZodArray; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + activity: z$1.ZodObject<{ + activeWorkflowCount: z$1.ZodNumber; + activeBackgroundAgentCount: z$1.ZodNumber; + activeBackgroundCommandCount: z$1.ZodNumber; + activePlanModeCount: z$1.ZodNumber; + activeGoalCount: z$1.ZodNumber; + }, z$1.core.$strip>; + pinSortKey: z$1.ZodNullable; + hasPendingInteraction: z$1.ZodBoolean; + environmentHostId: z$1.ZodNullable; + environmentName: z$1.ZodNullable; + environmentBranchName: z$1.ZodNullable; + environmentWorkspaceDisplayKind: z$1.ZodEnum<{ + "managed-worktree": "managed-worktree"; + "unmanaged-worktree": "unmanaged-worktree"; + other: "other"; + }>; + }, z$1.core.$strip>; + matches: z$1.ZodArray; + text: z$1.ZodString; + highlightRanges: z$1.ZodArray>; + sourceSeq: z$1.ZodNullable; + }, z$1.core.$strict>>; + }, z$1.core.$strict>>; + }, z$1.core.$strict>; +}, z$1.core.$strict>; +type ThreadSearchResponse = z$1.infer; +declare const threadResponseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + projectId: z$1.ZodString; + environmentId: z$1.ZodNullable; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + canSpawnChild: z$1.ZodBoolean; +}, z$1.core.$strip>; +type ThreadResponse = z$1.infer; +declare const threadGetQuerySchema: z$1.ZodObject<{ + include: z$1.ZodOptional; +}, z$1.core.$strip>; +type ThreadGetQuery = z$1.infer; +declare const threadWithIncludesResponseSchema: z$1.ZodObject<{ + id: z$1.ZodString; + projectId: z$1.ZodString; + environmentId: z$1.ZodNullable; + providerId: z$1.ZodString; + title: z$1.ZodNullable; + titleFallback: z$1.ZodNullable; + sectionId: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + }>; + parentThreadId: z$1.ZodNullable; + sourceThreadId: z$1.ZodNullable; + originKind: z$1.ZodNullable>; + childOrigin: z$1.ZodNullable>; + originPluginId: z$1.ZodNullable; + visibility: z$1.ZodEnum<{ + visible: "visible"; + hidden: "hidden"; + }>; + archivedAt: z$1.ZodNullable; + pinnedAt: z$1.ZodNullable; + deletedAt: z$1.ZodNullable; + lastReadAt: z$1.ZodNullable; + latestAttentionAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + runtime: z$1.ZodObject<{ + displayStatus: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + stopping: "stopping"; + idle: "idle"; + starting: "starting"; + active: "active"; + "host-reconnecting": "host-reconnecting"; + "waiting-for-host": "waiting-for-host"; + }>; + hostReconnectGraceExpiresAt: z$1.ZodNullable; + }, z$1.core.$strip>; + canSpawnChild: z$1.ZodBoolean; + environment: z$1.ZodOptional; + projectId: z$1.ZodString; + hostId: z$1.ZodString; + path: z$1.ZodNullable; + managed: z$1.ZodBoolean; + isGitRepo: z$1.ZodBoolean; + isWorktree: z$1.ZodBoolean; + workspaceProvisionType: z$1.ZodEnum<{ + personal: "personal"; + "managed-worktree": "managed-worktree"; + unmanaged: "unmanaged"; + }>; + branchName: z$1.ZodNullable; + baseBranch: z$1.ZodNullable; + defaultBranch: z$1.ZodNullable; + mergeBaseBranch: z$1.ZodNullable; + status: z$1.ZodEnum<{ + error: "error"; + provisioning: "provisioning"; + ready: "ready"; + retiring: "retiring"; + destroying: "destroying"; + destroyed: "destroyed"; + }>; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + }, z$1.core.$strip>>>; + host: z$1.ZodOptional; + status: z$1.ZodEnum<{ + disconnected: "disconnected"; + connected: "connected"; + }>; + lastSeenAt: z$1.ZodNullable; + lastRejectedProtocolVersion: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; + }, z$1.core.$strip>>>; +}, z$1.core.$strip>; +type ThreadWithIncludesResponse = z$1.infer; +declare const threadPendingInteractionsResponseSchema: z$1.ZodArray; + statusReason: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + expiresAt: z$1.ZodOptional>; + resolvedAt: z$1.ZodNullable; + turnId: z$1.ZodString; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerRequestId: z$1.ZodString; + origin: z$1.ZodOptional; + providerId: z$1.ZodString; + providerThreadId: z$1.ZodString; + providerRequestId: z$1.ZodString; + }, z$1.core.$strip>>; + payload: z$1.ZodUnion; + subject: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + itemId: z$1.ZodString; + command: z$1.ZodString; + cwd: z$1.ZodNullable; + actions: z$1.ZodArray; + command: z$1.ZodString; + name: z$1.ZodString; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"listFiles">; + command: z$1.ZodString; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"search">; + command: z$1.ZodString; + query: z$1.ZodNullable; + path: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + type: z$1.ZodLiteral<"unknown">; + command: z$1.ZodString; + }, z$1.core.$strip>], "type">>; + sessionGrant: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"file_change">; + itemId: z$1.ZodString; + writeScope: z$1.ZodNullable; + sessionGrant: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"permission_grant">; + itemId: z$1.ZodString; + toolName: z$1.ZodNullable; + permissions: z$1.ZodObject<{ + network: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>; + }, z$1.core.$strip>], "kind">; + reason: z$1.ZodNullable; + availableDecisions: z$1.ZodArray>; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_question">; + questions: z$1.ZodArray; + multiSelect: z$1.ZodBoolean; + options: z$1.ZodOptional; + }, z$1.core.$strip>>>; + allowFreeText: z$1.ZodBoolean; + }, z$1.core.$strip>>; + }, z$1.core.$strip>]>; + resolution: z$1.ZodNullable; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"allow_for_session">; + grantedPermissions: z$1.ZodNullable; + }, z$1.core.$strip>>; + fileSystem: z$1.ZodNullable; + write: z$1.ZodArray; + }, z$1.core.$strip>>; + }, z$1.core.$strict>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + decision: z$1.ZodLiteral<"deny">; + }, z$1.core.$strip>], "decision">, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"user_answer">; + answers: z$1.ZodRecord; + freeText: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>]>>; +}, z$1.core.$strip>, z$1.ZodObject<{ + id: z$1.ZodString; + threadId: z$1.ZodString; + status: z$1.ZodEnum<{ + pending: "pending"; + interrupted: "interrupted"; + resolving: "resolving"; + resolved: "resolved"; + }>; + statusReason: z$1.ZodNullable; + createdAt: z$1.ZodNumber; + expiresAt: z$1.ZodOptional>; + resolvedAt: z$1.ZodNullable; + turnId: z$1.ZodNullable; + origin: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + rendererId: z$1.ZodString; + }, z$1.core.$strip>; + payload: z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + title: z$1.ZodString; + data: z$1.ZodType>; + }, z$1.core.$strip>; + resolution: z$1.ZodNullable; + }, z$1.core.$strip>>; +}, z$1.core.$strip>]>>; +type ThreadPendingInteractionsResponse = z$1.infer; +declare const threadQueuedMessageListResponseSchema: z$1.ZodArray>; + type: z$1.ZodLiteral<"text">; + text: z$1.ZodString; + mentions: z$1.ZodDefault, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"thread">; + threadId: z$1.ZodString; + projectId: z$1.ZodOptional; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"project">; + projectId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"section">; + sectionId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"path">; + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + entryKind: z$1.ZodEnum<{ + file: "file"; + directory: "directory"; + }>; + path: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"command">; + trigger: z$1.ZodEnum<{ + "/": "/"; + }>; + name: z$1.ZodString; + source: z$1.ZodEnum<{ + command: "command"; + skill: "skill"; + }>; + origin: z$1.ZodEnum<{ + user: "user"; + project: "project"; + builtin: "builtin"; + }>; + label: z$1.ZodString; + argumentHint: z$1.ZodNullable; + }, z$1.core.$strip>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"plugin">; + pluginId: z$1.ZodString; + icon: z$1.ZodOptional>; + itemId: z$1.ZodString; + label: z$1.ZodString; + }, z$1.core.$strip>], "kind">>; + }, z$1.core.$strip>>>; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"image">; + url: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localImage">; + path: z$1.ZodString; + }, z$1.core.$strip>, z$1.ZodObject<{ + visibility: z$1.ZodOptional>; + type: z$1.ZodLiteral<"localFile">; + path: z$1.ZodString; + name: z$1.ZodOptional; + sizeBytes: z$1.ZodOptional; + mimeType: z$1.ZodOptional; + }, z$1.core.$strip>], "type">>; + model: z$1.ZodString; + reasoningLevel: z$1.ZodEnum<{ + none: "none"; + low: "low"; + medium: "medium"; + high: "high"; + xhigh: "xhigh"; + ultracode: "ultracode"; + max: "max"; + ultra: "ultra"; + }>; + permissionMode: z$1.ZodEnum<{ + "accept-edits": "accept-edits"; + auto: "auto"; + full: "full"; + }>; + serviceTier: z$1.ZodEnum<{ + default: "default"; + fast: "fast"; + }>; + groupWithNext: z$1.ZodBoolean; + createdAt: z$1.ZodNumber; + updatedAt: z$1.ZodNumber; +}, z$1.core.$strip>>; +type ThreadQueuedMessageListResponse = z$1.infer; +declare const threadChildSummaryResponseSchema: z$1.ZodObject<{ + nonDeletedChildCount: z$1.ZodNumber; +}, z$1.core.$strip>; +type ThreadChildSummaryResponse = z$1.infer; +declare const deleteThreadRequestSchema: z$1.ZodObject<{ + childThreadsConfirmed: z$1.ZodBoolean; +}, z$1.core.$strip>; +type DeleteThreadRequest = z$1.infer; +declare const updateThreadRequestSchema: z$1.ZodObject<{ + title: z$1.ZodOptional>; + sectionId: z$1.ZodOptional>; + parentThreadId: z$1.ZodOptional>; + model: z$1.ZodOptional>; + reasoningLevel: z$1.ZodOptional>>; + visibility: z$1.ZodOptional>; +}, z$1.core.$strip>; +type UpdateThreadRequest = z$1.infer; +declare const reorderPinnedThreadRequestSchema: z$1.ZodObject<{ + previousThreadId: z$1.ZodNullable; + nextThreadId: z$1.ZodNullable; +}, z$1.core.$strip>; +type ReorderPinnedThreadRequest = z$1.infer; +/** + * Requested placement for a thread opened in the app's split layout. Edge + * placements add panes through the eighth pane; at the cap they replace the + * focused pane. `replace` always replaces the focused pane. + */ +declare const threadOpenSplitSchema: z$1.ZodEnum<{ + right: "right"; + down: "down"; + left: "left"; + top: "top"; + replace: "replace"; +}>; +type ThreadOpenSplit = z$1.infer; +/** Optional secondary-panel file to open with a thread. */ +declare const threadOpenFileSchema: z$1.ZodObject<{ + source: z$1.ZodEnum<{ + workspace: "workspace"; + "thread-storage": "thread-storage"; + }>; + path: z$1.ZodString; + lineNumber: z$1.ZodNullable; +}, z$1.core.$strict>; +type ThreadOpenFile = z$1.infer; +/** Response for POST /threads/:id/open: how many connected clients received it. */ +declare const threadOpenResponseSchema: z$1.ZodObject<{ + delivered: z$1.ZodNumber; +}, z$1.core.$strip>; +type ThreadOpenResponse = z$1.infer; +/** Presentation action for one thread pane in each connected app window. */ +declare const threadPaneActionSchema: z$1.ZodEnum<{ + maximize: "maximize"; + restore: "restore"; + toggle: "toggle"; +}>; +type ThreadPaneAction = z$1.infer; +/** Number of connected app clients that received the pane action. */ +declare const threadPaneActionResponseSchema: z$1.ZodObject<{ + delivered: z$1.ZodNumber; +}, z$1.core.$strip>; +type ThreadPaneActionResponse = z$1.infer; +declare const threadArchiveAllResponseSchema: z$1.ZodObject<{ + ok: z$1.ZodLiteral; + archivedThreadIds: z$1.ZodArray; +}, z$1.core.$strip>; +type ThreadArchiveAllResponse = z$1.infer; +declare const threadListQuerySchema: z$1.ZodObject<{ + projectId: z$1.ZodOptional; + parentThreadId: z$1.ZodOptional; + sourceThreadId: z$1.ZodOptional; + archived: z$1.ZodOptional>; + sectionId: z$1.ZodOptional; + unsectioned: z$1.ZodOptional>; + hasParent: z$1.ZodOptional>; + originKind: z$1.ZodOptional>; + excludeSideChats: z$1.ZodOptional>; + childOrigin: z$1.ZodOptional>; + includeHidden: z$1.ZodOptional>; + limit: z$1.ZodOptional; + offset: z$1.ZodOptional; +}, z$1.core.$strip>; +type ThreadListQuery = z$1.infer; +declare const threadSearchQuerySchema: z$1.ZodObject<{ + query: z$1.ZodString; + limitPerGroup: z$1.ZodOptional; +}, z$1.core.$strip>; +type ThreadSearchQuery = z$1.infer; +declare const threadTimelineQuerySchema: z$1.ZodObject<{ + includeNestedRows: z$1.ZodOptional>; + segmentLimit: z$1.ZodOptional; + beforeAnchorSeq: z$1.ZodOptional; + beforeAnchorId: z$1.ZodOptional; + summaryOnly: z$1.ZodOptional>; + afterSequence: z$1.ZodOptional; +}, z$1.core.$strip>; +type ThreadTimelineQuery = z$1.infer; +declare const timelineTurnSummaryDetailsQuerySchema: z$1.ZodObject<{ + turnId: z$1.ZodString; + sourceSeqStart: z$1.ZodString; + sourceSeqEnd: z$1.ZodString; +}, z$1.core.$strip>; +type TimelineTurnSummaryDetailsQuery = z$1.infer; +declare const threadStorageFilesQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional; + limit: z$1.ZodOptional; +}, z$1.core.$strip>; +type ThreadStorageFilesQuery = z$1.infer; +declare const threadStoragePathsQuerySchema: z$1.ZodObject<{ + query: z$1.ZodOptional; + limit: z$1.ZodOptional; + includeFiles: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; + includeDirectories: z$1.ZodEnum<{ + true: "true"; + false: "false"; + }>; +}, z$1.core.$strip>; +type ThreadStoragePathsQuery = z$1.infer; +declare const timelineTurnSummaryDetailsResponseSchema: z$1.ZodObject<{ + rows: z$1.ZodArray>>; +}, z$1.core.$strip>; +type TimelineTurnSummaryDetailsResponse = z$1.infer; +declare const threadTimelineResponseSchema: z$1.ZodObject<{ + rows: z$1.ZodArray>>; + activePromptMode: z$1.ZodNullable; + providerId: z$1.ZodEnum<{ + codex: "codex"; + "claude-code": "claude-code"; + }>; + prompt: z$1.ZodString; + }, z$1.core.$strict>>; + activeThinking: z$1.ZodNullable>; + activeWorkflow: z$1.ZodNullable; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"workflow">; + itemId: z$1.ZodString; + taskType: z$1.ZodString; + workflowName: z$1.ZodNullable; + description: z$1.ZodString; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + running: "running"; + paused: "paused"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + workflow: z$1.ZodNullable; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodNullable>; + summary: z$1.ZodNullable; + error: z$1.ZodNullable; + completedAt: z$1.ZodNullable; + }, z$1.core.$strip>>; + activeBackgroundCommands: z$1.ZodArray; + sourceSeqStart: z$1.ZodNumber; + sourceSeqEnd: z$1.ZodNumber; + startedAt: z$1.ZodNumber; + createdAt: z$1.ZodNumber; + kind: z$1.ZodLiteral<"work">; + status: z$1.ZodEnum<{ + error: "error"; + pending: "pending"; + completed: "completed"; + interrupted: "interrupted"; + }>; + workKind: z$1.ZodLiteral<"workflow">; + itemId: z$1.ZodString; + taskType: z$1.ZodString; + workflowName: z$1.ZodNullable; + description: z$1.ZodString; + taskStatus: z$1.ZodEnum<{ + pending: "pending"; + completed: "completed"; + running: "running"; + paused: "paused"; + failed: "failed"; + killed: "killed"; + stopped: "stopped"; + }>; + workflow: z$1.ZodNullable; + }, z$1.core.$strip>>; + agents: z$1.ZodArray; + model: z$1.ZodString; + attempt: z$1.ZodNumber; + cached: z$1.ZodBoolean; + lastProgressAt: z$1.ZodNumber; + phaseIndex: z$1.ZodOptional; + phaseTitle: z$1.ZodOptional; + agentType: z$1.ZodOptional; + isolation: z$1.ZodOptional; + queuedAt: z$1.ZodOptional; + startedAt: z$1.ZodOptional; + lastToolName: z$1.ZodOptional; + lastToolSummary: z$1.ZodOptional; + promptPreview: z$1.ZodOptional; + resultPreview: z$1.ZodOptional; + error: z$1.ZodOptional; + tokens: z$1.ZodOptional; + toolCalls: z$1.ZodOptional; + durationMs: z$1.ZodOptional; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + usage: z$1.ZodNullable>; + summary: z$1.ZodNullable; + error: z$1.ZodNullable; + completedAt: z$1.ZodNullable; + }, z$1.core.$strip>>; + pendingTodos: z$1.ZodNullable; + }, z$1.core.$strip>>; + }, z$1.core.$strip>>; + goal: z$1.ZodNullable; + tokenBudget: z$1.ZodNullable; + tokensUsed: z$1.ZodNumber; + timeUsedSeconds: z$1.ZodNumber; + }, z$1.core.$strip>>; + modelFallback: z$1.ZodNullable; + message: z$1.ZodString; + }, z$1.core.$strip>>; + contextWindowUsage: z$1.ZodOptional>; + timelinePage: z$1.ZodObject<{ + kind: z$1.ZodEnum<{ + latest: "latest"; + older: "older"; + }>; + segmentLimit: z$1.ZodNumber; + returnedSegmentCount: z$1.ZodNumber; + hasOlderRows: z$1.ZodBoolean; + olderCursor: z$1.ZodNullable>; + }, z$1.core.$strict>; + maxSeq: z$1.ZodNumber; + delta: z$1.ZodOptional>>; + rowOrder: z$1.ZodOptional>; + }, z$1.core.$strip>>; +}, z$1.core.$strip>; +type ThreadTimelineResponse = z$1.infer; +declare const threadConversationOutlineResponseSchema: z$1.ZodObject<{ + items: z$1.ZodArray; + preview: z$1.ZodString; + attachmentSummary: z$1.ZodNullable>; + }, z$1.core.$strict>>; + maxSeq: z$1.ZodNumber; +}, z$1.core.$strict>; +type ThreadConversationOutlineResponse = z$1.infer; +declare const threadStorageFileListResponseSchema: z$1.ZodObject<{ + files: z$1.ZodArray>; + truncated: z$1.ZodBoolean; + storageRootPath: z$1.ZodString; +}, z$1.core.$strip>; +type ThreadStorageFileListResponse = z$1.infer; +declare const threadStoragePathListResponseSchema: z$1.ZodObject<{ + paths: z$1.ZodArray; + path: z$1.ZodString; + name: z$1.ZodString; + score: z$1.ZodNumber; + positions: z$1.ZodArray; + }, z$1.core.$strip>>; + truncated: z$1.ZodBoolean; + storageRootPath: z$1.ZodString; +}, z$1.core.$strip>; +type ThreadStoragePathListResponse = z$1.infer; + +declare const threadTabsResponseSchema: z$1.ZodObject<{ + revision: z$1.ZodNumber; + tabs: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"git-diff">; + }, z$1.core.$strict>, z$1.ZodObject<{ + actionId: z$1.ZodString; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"plugin-panel">; + paramsJson: z$1.ZodNullable; + pluginId: z$1.ZodString; + title: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"workspace-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + projectId: z$1.ZodNullable; + source: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"working-tree">; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"head">; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"merge-base">; + ref: z$1.ZodString; + }, z$1.core.$strict>], "kind">; + statusLabel: z$1.ZodNullable>; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"host-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + threadId: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + isPinned: z$1.ZodBoolean; + kind: z$1.ZodLiteral<"thread-storage-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + threadId: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"browser">; + title: z$1.ZodNullable; + url: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"new-tab">; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"side-chat">; + sourceMessageText: z$1.ZodString; + sourceSeqEnd: z$1.ZodNullable; + threadId: z$1.ZodNullable; + title: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"terminal">; + terminalId: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; +}, z$1.core.$strict>; +type ThreadTabsResponse = z$1.infer; +declare const updateThreadTabsRequestSchema: z$1.ZodObject<{ + expectedRevision: z$1.ZodNumber; + tabs: z$1.ZodArray; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"git-diff">; + }, z$1.core.$strict>, z$1.ZodObject<{ + actionId: z$1.ZodString; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"plugin-panel">; + paramsJson: z$1.ZodNullable; + pluginId: z$1.ZodString; + title: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"workspace-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + projectId: z$1.ZodNullable; + source: z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{ + kind: z$1.ZodLiteral<"working-tree">; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"head">; + }, z$1.core.$strict>, z$1.ZodObject<{ + kind: z$1.ZodLiteral<"merge-base">; + ref: z$1.ZodString; + }, z$1.core.$strict>], "kind">; + statusLabel: z$1.ZodNullable>; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"host-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + threadId: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + isPinned: z$1.ZodBoolean; + kind: z$1.ZodLiteral<"thread-storage-file-preview">; + lineRange: z$1.ZodNullable>; + path: z$1.ZodString; + threadId: z$1.ZodNullable; + }, z$1.core.$strict>, z$1.ZodObject<{ + environmentId: z$1.ZodNullable; + id: z$1.ZodString; + kind: z$1.ZodLiteral<"browser">; + title: z$1.ZodNullable; + url: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"new-tab">; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"side-chat">; + sourceMessageText: z$1.ZodString; + sourceSeqEnd: z$1.ZodNullable; + threadId: z$1.ZodNullable; + title: z$1.ZodString; + }, z$1.core.$strict>, z$1.ZodObject<{ + id: z$1.ZodString; + kind: z$1.ZodLiteral<"terminal">; + terminalId: z$1.ZodString; + }, z$1.core.$strict>], "kind">>; +}, z$1.core.$strict>; +type UpdateThreadTabsRequest = z$1.infer; + +interface EnvironmentActionArgs { + environmentId: string; +} +interface EnvironmentGetArgs extends EnvironmentActionArgs { + signal?: AbortSignal; +} +type EnvironmentMergeBaseBranchUpdateValue = Exclude; +type EnvironmentNameUpdateValue = Exclude; +interface EnvironmentMergeBaseBranchUpdate { + mergeBaseBranch: EnvironmentMergeBaseBranchUpdateValue; + name?: EnvironmentNameUpdateValue; +} +interface EnvironmentNameUpdate { + mergeBaseBranch?: EnvironmentMergeBaseBranchUpdateValue; + name: EnvironmentNameUpdateValue; +} +type EnvironmentUpdateFields = EnvironmentMergeBaseBranchUpdate | EnvironmentNameUpdate; +type EnvironmentUpdateArgs = EnvironmentUpdateFields & { + environmentId: string; +}; +interface EnvironmentStatusArgs extends EnvironmentStatusQuery { + environmentId: string; + signal?: AbortSignal; +} +type EnvironmentDiffArgs = EnvironmentDiffQuery & { + environmentId: string; + signal?: AbortSignal; +}; +type EnvironmentDiffFileArgs = EnvironmentDiffFileQuery & { + environmentId: string; + signal?: AbortSignal; +}; +interface EnvironmentDiffBranchesArgs extends EnvironmentDiffBranchesQuery { + environmentId: string; + signal?: AbortSignal; +} +interface EnvironmentCommitArgs { + environmentId: string; +} +interface EnvironmentSquashMergeArgs { + environmentId: string; + mergeBaseBranch: string; +} +interface EnvironmentPullRequestMergeArgs { + environmentId: string; + method: PullRequestMergeMethod; +} +type EnvironmentDiffPatchArgs = EnvironmentDiffPatchRequest & { + environmentId: string; + signal?: AbortSignal; +}; +interface EnvironmentPathsArgs extends EnvironmentPathsQuery { + environmentId: string; + signal?: AbortSignal; +} +type EnvironmentArchiveThreadsResult = EnvironmentArchiveThreadsResponse; +type EnvironmentCommitResult = CommitActionResponse; +type EnvironmentDiffResult = EnvironmentDiffResponse; +type EnvironmentDiffBranchesResult = EnvironmentDiffBranchesResponse; +type EnvironmentDiffFileResult = EnvironmentDiffFileResponse; +type EnvironmentDiffFilesResult = EnvironmentDiffFilesResponse; +type EnvironmentDiffPatchResult = EnvironmentDiffPatchResponse; +type EnvironmentGetResult = Environment; +type EnvironmentMarkPullRequestDraftResult = PullRequestDraftActionResponse; +type EnvironmentMarkPullRequestReadyResult = PullRequestReadyActionResponse; +type EnvironmentMergePullRequestResult = PullRequestMergeActionResponse; +type EnvironmentPathsResult = WorkspacePathListResponse; +type EnvironmentPullRequestResult = EnvironmentPullRequestResponse; +type EnvironmentSquashMergeResult = SquashMergeActionResponse; +type EnvironmentStatusResult = EnvironmentStatusResponse; +type EnvironmentUpdateResult = Environment; +interface EnvironmentsArea { + archiveThreads(args: EnvironmentActionArgs): Promise; + commit(args: EnvironmentCommitArgs): Promise; + diff(args: EnvironmentDiffArgs): Promise; + diffBranches(args: EnvironmentDiffBranchesArgs): Promise; + diffFile(args: EnvironmentDiffFileArgs): Promise; + diffFiles(args: EnvironmentDiffArgs): Promise; + diffPatch(args: EnvironmentDiffPatchArgs): Promise; + get(args: EnvironmentGetArgs): Promise; + pullRequest(args: EnvironmentGetArgs): Promise; + markPullRequestDraft(args: EnvironmentActionArgs): Promise; + markPullRequestReady(args: EnvironmentActionArgs): Promise; + mergePullRequest(args: EnvironmentPullRequestMergeArgs): Promise; + paths(args: EnvironmentPathsArgs): Promise; + squashMerge(args: EnvironmentSquashMergeArgs): Promise; + status(args: EnvironmentStatusArgs): Promise; + update(args: EnvironmentUpdateArgs): Promise; +} + +/** + * Host file primitives. `hostId` may be omitted to target the server's + * primary (local) host. `rootPath`, when set, confines the target beneath + * that absolute root on the host (symlink-safe). + */ +interface FileReadArgs { + hostId?: string; + path: string; + rootPath?: string; + signal?: AbortSignal; +} +interface FileWriteArgs { + hostId?: string; + path: string; + rootPath?: string; + content: string; + /** Defaults to "utf8". */ + contentEncoding?: "utf8" | "base64"; + /** Defaults to false. */ + createParents?: boolean; + /** + * Optimistic-concurrency guard: omitted → unconditional write; a hash → + * write only when the current content hashes to it (use `read().sha256`); + * null → create-only. A failed guard resolves to the `conflict` outcome. + */ + expectedSha256?: string | null; + /** POSIX permission bits used when creating a file (for example 0o600). */ + mode?: number; +} +interface FileListArgs { + hostId?: string; + path: string; + query?: string; + limit?: number; + signal?: AbortSignal; +} +interface PathListArgs extends FileListArgs { + includeFiles: boolean; + includeDirectories: boolean; +} +interface FileMkdirArgs { + hostId?: string; + path: string; + rootPath?: string; + recursive?: boolean; +} +interface FileMoveArgs { + hostId?: string; + sourcePath: string; + destinationPath: string; + rootPath?: string; +} +interface FileRemoveArgs { + hostId?: string; + path: string; + rootPath?: string; + recursive?: boolean; +} +interface FilePreviewArgs { + hostId?: string; + rootPath: string; + signal?: AbortSignal; + ttlMs?: number; +} +type FileReadResult = HostFileReadResponse; +type FileWriteResult = HostFileWriteResponse; +type FileListResult = HostFileListResponse; +type PathListResult = HostPathListResponse; +type FileMkdirResult = HostMkdirResponse; +type FileMoveResult = HostMovePathResponse; +type FileRemoveResult = HostRemovePathResponse; +type FilePreviewResult = CreateFilePreviewResponse; +interface FilesArea { + read(args: FileReadArgs): Promise; + write(args: FileWriteArgs): Promise; + list(args: FileListArgs): Promise; + listPaths(args: PathListArgs): Promise; + mkdir(args: FileMkdirArgs): Promise; + move(args: FileMoveArgs): Promise; + remove(args: FileRemoveArgs): Promise; + createPreview(args: FilePreviewArgs): Promise; +} + +interface GuideRenderArgs { + chapter?: string; +} +interface GuideRenderResult { + chapter?: string; + content: string; +} +interface GuideArea { + render(args?: GuideRenderArgs): GuideRenderResult; +} + +interface HostGetArgs { + hostId: string; + signal?: AbortSignal; +} +interface HostDeleteArgs { + hostId: string; +} +interface HostUpdateArgs extends UpdateHostRequest { + hostId: string; +} +interface HostRetryUpdateArgs { + hostId: string; +} +interface HostDirectoryArgs extends HostDirectoryQuery { + hostId: string; + signal?: AbortSignal; +} +interface HostCloneDefaultPathArgs extends HostCloneDefaultPathQuery { + hostId: string; + signal?: AbortSignal; +} +interface HostPathsExistArgs extends HostPathsExistRequest { + hostId: string; + signal?: AbortSignal; +} +interface HostPickFolderArgs extends HostPickFolderRequest { + hostId: string; + signal?: AbortSignal; +} +interface HostProviderCliInstallArgs extends HostProviderCliInstallRequest { + hostId: string; +} +interface HostListArgs { + signal?: AbortSignal; +} +type HostCreateJoinCodeResult = CreateHostJoinCodeResponse; +type HostDeleteResult = { + ok: true; +}; +type HostDirectoryResult = HostDirectoryListing; +type HostGetResult = Host; +type HostCloneDefaultPathResult = HostCloneDefaultPathResponse; +type HostProviderCliInstallResult = HostProviderCliInstallEvent[]; +type HostListResult = Host[]; +type HostPathsExistResult = HostPathsExistResponse; +type HostPickFolderResult = HostPickFolderResponse; +type HostProviderCliStatusResult = HostProviderCliStatusResponse; +type HostRetryUpdateResult = HostRetryUpdateResponse; +type HostUpdateResult = Host; +interface HostsArea { + createJoinCode(): Promise; + delete(args: HostDeleteArgs): Promise; + directory(args: HostDirectoryArgs): Promise; + get(args: HostGetArgs): Promise; + cloneDefaultPath(args: HostCloneDefaultPathArgs): Promise; + installProviderCli(args: HostProviderCliInstallArgs): Promise; + list(args?: HostListArgs): Promise; + pathsExist(args: HostPathsExistArgs): Promise; + pickFolder(args: HostPickFolderArgs): Promise; + providerCliStatus(args: HostGetArgs): Promise; + retryUpdate(args: HostRetryUpdateArgs): Promise; + update(args: HostUpdateArgs): Promise; +} + +interface ProjectListArgs { + include?: ProjectListQuery["include"]; + /** Include the singleton personal project. Defaults to false for compatibility. */ + includePersonal?: boolean; + signal?: AbortSignal; +} +interface ProjectCreateArgs extends CreateProjectRequest { +} +interface ProjectGetArgs { + projectId: string; + signal?: AbortSignal; +} +interface ProjectUpdateArgs extends UpdateProjectRequest { + projectId: string; +} +interface ProjectDeleteArgs { + projectId: string; +} +interface ProjectReorderArgs extends ReorderProjectRequest { + projectId: string; +} +interface ProjectPromptHistoryArgs extends PromptHistoryQuery { + projectId: string; + signal?: AbortSignal; +} +/** Select one project workspace source, or omit both for the primary host. */ +type ProjectWorkspaceRoutingArgs = { + environmentId: string; + hostId?: never; +} | { + environmentId?: never; + hostId: string; +} | { + environmentId?: never; + hostId?: never; +}; +type ProjectFilesArgs = ProjectWorkspaceRoutingArgs & Omit & { + projectId: string; + signal?: AbortSignal; +}; +type ProjectPathsArgs = ProjectWorkspaceRoutingArgs & Omit & { + projectId: string; + signal?: AbortSignal; +}; +type ProjectCommandsArgs = ProjectWorkspaceRoutingArgs & Omit & { + projectId: string; + signal?: AbortSignal; +}; +type ProjectFileContentArgs = ProjectWorkspaceRoutingArgs & Omit & { + projectId: string; + signal?: AbortSignal; +}; +interface ProjectBranchesArgs extends ProjectBranchesQuery { + projectId: string; + signal?: AbortSignal; +} +interface ProjectDefaultExecutionOptionsArgs { + projectId: string; + signal?: AbortSignal; +} +interface ProjectAttachmentFileLike { + arrayBuffer(): Promise; + readonly name: string; + readonly type?: string; +} +interface ProjectAttachmentUploadArgsBase { + /** MIME override. Omit to use the File/Blob type, when available. */ + mimeType?: string; + projectId: string; +} +/** + * Upload bytes owned by this SDK client. A bare Blob/byte buffer needs an + * explicit filename; File-like values can supply their own name. + */ +type ProjectAttachmentUploadArgs = ProjectAttachmentUploadArgsBase & ({ + clientFile: ProjectAttachmentFileLike; + filename?: string; +} | { + clientFile: ArrayBuffer | Blob | Uint8Array; + filename: string; +}); +interface ProjectAttachmentReadArgs { + path: string; + projectId: string; + signal?: AbortSignal; +} +interface ProjectAttachmentCopyArgs extends CopyProjectAttachmentsRequest { + projectId: string; +} +type ProjectSourceAddArgs = CreateProjectSourceRequest & { + projectId: string; +}; +interface ProjectSourceUpdateArgs extends UpdateProjectSourceRequest { + projectId: string; + sourceId: string; +} +interface ProjectSourceDeleteArgs { + projectId: string; + sourceId: string; +} +type ProjectBranchesResult = ProjectBranchesResponse; +interface ProjectAttachmentReadResult { + bytes: Uint8Array; + mimeType: string; + sizeBytes: number; +} +type ProjectAttachmentUploadResult = UploadedPromptAttachment; +type ProjectCommandsResult = CommandListResponse; +type ProjectCreateResult = ProjectResponse; +type ProjectDefaultExecutionOptionsResult = ProjectExecutionDefaults | null; +type ProjectDeleteResult = { + ok: true; +}; +interface ProjectFileContentResult { + /** UTF-8 text or base64, as selected by `contentEncoding`. */ + content: string; + contentEncoding: "utf8" | "base64"; + mimeType: string; + sizeBytes: number; +} +type ProjectFilesResult = WorkspaceFileListResponse; +type ProjectGetResult = ProjectResponse; +type ProjectListResult = ProjectResponse[] | ProjectWithThreadsResponse[]; +type ProjectPathsResult = WorkspacePathListResponse; +type ProjectPromptHistoryResult = PromptHistoryResponse; +type ProjectReorderResult = ProjectResponse[]; +type ProjectSourceAddResult = ProjectSource; +type ProjectSourceDeleteResult = { + ok: true; +}; +type ProjectSourceUpdateResult = ProjectSource; +type ProjectUpdateResult = ProjectResponse; +interface ProjectSourcesArea { + add(args: ProjectSourceAddArgs): Promise; + delete(args: ProjectSourceDeleteArgs): Promise; + update(args: ProjectSourceUpdateArgs): Promise; +} +interface ProjectAttachmentsArea { + copy(args: ProjectAttachmentCopyArgs): Promise; + read(args: ProjectAttachmentReadArgs): Promise; + upload(args: ProjectAttachmentUploadArgs): Promise; +} +interface ProjectsArea { + attachments: ProjectAttachmentsArea; + branches(args: ProjectBranchesArgs): Promise; + commands(args: ProjectCommandsArgs): Promise; + create(args: ProjectCreateArgs): Promise; + defaultExecutionOptions(args: ProjectDefaultExecutionOptionsArgs): Promise; + delete(args: ProjectDeleteArgs): Promise; + fileContent(args: ProjectFileContentArgs): Promise; + files(args: ProjectFilesArgs): Promise; + get(args: ProjectGetArgs): Promise; + list(args?: ProjectListArgs): Promise; + paths(args: ProjectPathsArgs): Promise; + promptHistory(args: ProjectPromptHistoryArgs): Promise; + reorder(args: ProjectReorderArgs): Promise; + sources: ProjectSourcesArea; + update(args: ProjectUpdateArgs): Promise; +} + +/** Select exactly one provider-discovery host source, or omit both for primary. */ +type ProviderHostRoutingArgs = { + environmentId: string; + hostId?: never; +} | { + environmentId?: never; + hostId: string; +} | { + environmentId?: never; + hostId?: never; +}; +type ProviderListArgs = ProviderHostRoutingArgs & { + signal?: AbortSignal; +}; +type ProviderModelsArgs = ProviderHostRoutingArgs & { + providerId?: string; + signal?: AbortSignal; +}; +type ProviderListResult = ProviderInfo[]; +type ProviderModelsResult = SystemExecutionOptionsResponse; +interface ProvidersArea { + /** List providers on the environment host, explicit host, or primary host. */ + list(args?: ProviderListArgs): Promise; + /** List models on the environment host, explicit host, or primary host. */ + models(args?: ProviderModelsArgs): Promise; +} + +interface PluginIdArgs { + pluginId: string; +} +/** Install directly from a path:, git:, npm:, or builtin: source spec. */ +interface PluginInstallArgs { + source: string; +} +/** Install an entry from BB's official catalog. */ +interface PluginCatalogInstallArgs { + entryId: string; +} +interface PluginReloadArgs { + pluginId?: string; +} +interface PluginSettingsUpdateArgs extends PluginIdArgs { + values: Record; +} +interface PluginTokenArgs extends PluginIdArgs { + rotate?: boolean; +} +interface PluginCheckUpdatesArgs { + pluginId?: string; + signal?: AbortSignal; +} +interface PluginRpcArgs extends PluginIdArgs { + input?: JsonValue; + method: string; + outputSchema: z$1.ZodType; +} +interface PluginCatalogSearchArgs { + query: string; + signal?: AbortSignal; +} +interface PluginCatalogStatusArgs { + signal?: AbortSignal; +} +interface PluginGetSettingsArgs extends PluginIdArgs { + signal?: AbortSignal; +} +interface PluginGetSourceArgs extends PluginIdArgs { + signal?: AbortSignal; +} +interface PluginListArgs { + signal?: AbortSignal; +} +interface PluginListUpdateResultsArgs { + signal?: AbortSignal; +} +type PluginDisableResult = InstalledPlugin; +type PluginEnableResult = InstalledPlugin; +type PluginGetSettingsResult = PluginSettingsResponse; +type PluginInstallResult = InstalledPlugin; +type PluginListResult = PluginListResponse; +type PluginReloadResult = PluginReloadResponse; +type PluginRemoveResult = PluginRemoveResponse; +type PluginTokenResult = PluginTokenResponse; +type PluginUpdateSettingsResult = PluginSettingsResponse; +type PluginGetSourceResult = PluginSourceDetail; +type PluginCheckUpdatesResult = PluginUpdateCheckEntry[]; +type PluginApplyUpdateResult = PluginApplyUpdateResult$1; +type PluginCatalogStatusResult = PluginCatalogStatus; +type PluginCatalogSearchResult = PluginCatalogSearchResult$1[]; +interface PluginCatalogArea { + install(args: PluginCatalogInstallArgs): Promise; + search(args: PluginCatalogSearchArgs): Promise; + status(args?: PluginCatalogStatusArgs): Promise; +} +interface PluginsArea { + applyUpdate(args: PluginIdArgs): Promise; + callRpc(args: PluginRpcArgs): Promise; + checkUpdates(args?: PluginCheckUpdatesArgs): Promise; + catalog: PluginCatalogArea; + disable(args: PluginIdArgs): Promise; + enable(args: PluginIdArgs): Promise; + getSettings(args: PluginGetSettingsArgs): Promise; + getSource(args: PluginGetSourceArgs): Promise; + install(args: PluginInstallArgs): Promise; + list(args?: PluginListArgs): Promise; + listUpdateResults(args?: PluginListUpdateResultsArgs): Promise; + reload(args?: PluginReloadArgs): Promise; + remove(args: PluginIdArgs): Promise; + token(args: PluginTokenArgs): Promise; + updateSettings(args: PluginSettingsUpdateArgs): Promise; +} + +type BbRealtimeUnsubscribe = () => void; +type BbRealtimeEventName = "thread:changed" | "project:changed" | "environment:changed" | "host:changed" | "system:changed" | "system:config-changed" | "realtime:connection"; +type ThreadRealtimeEvent = Extract; +type ProjectRealtimeEvent = Extract; +type EnvironmentRealtimeEvent = Extract; +type HostRealtimeEvent = Extract; +type SystemRealtimeEvent = Extract; +type BbRealtimeConnectionState = "connecting" | "connected" | "disconnected"; +interface BbRealtimeConnectionEvent { + reconnectDelayMs: number | null; + reconnected: boolean; + state: BbRealtimeConnectionState; +} +/** + * Entity-changed events are delivered as one shared object to every matching + * listener; their payload types are readonly so a listener cannot mutate what + * the next listener receives. + */ +interface BbRealtimeEventMap { + "thread:changed": ThreadRealtimeEvent; + "project:changed": ProjectRealtimeEvent; + "environment:changed": EnvironmentRealtimeEvent; + "host:changed": HostRealtimeEvent; + "system:changed": SystemRealtimeEvent; + "system:config-changed": SystemRealtimeEvent; + "realtime:connection": BbRealtimeConnectionEvent; +} +type BbRealtimeCallback = (event: BbRealtimeEventMap[TEventName]) => void; +interface ThreadRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"thread:changed">; + event: "thread:changed"; + threadId?: string; +} +interface ProjectRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"project:changed">; + event: "project:changed"; + projectId?: string; +} +interface EnvironmentRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"environment:changed">; + environmentId?: string; + event: "environment:changed"; +} +interface HostRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"host:changed">; + event: "host:changed"; + hostId?: string; +} +interface SystemRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"system:changed">; + event: "system:changed"; +} +interface SystemConfigRealtimeSubscribeArgs { + callback: BbRealtimeCallback<"system:config-changed">; + event: "system:config-changed"; +} +/** + * Connection listeners are pure observers — they never open or hold the + * socket. A listener registered while a socket already exists receives the + * latest connection event as a snapshot on the next microtask, so a status + * UI mounted after connect still learns the current state. + */ +interface RealtimeConnectionSubscribeArgs { + callback: BbRealtimeCallback<"realtime:connection">; + event: "realtime:connection"; +} +type BbRealtimeSubscribeArgsUnion = ThreadRealtimeSubscribeArgs | ProjectRealtimeSubscribeArgs | EnvironmentRealtimeSubscribeArgs | HostRealtimeSubscribeArgs | SystemRealtimeSubscribeArgs | SystemConfigRealtimeSubscribeArgs | RealtimeConnectionSubscribeArgs; +type BbRealtimeSubscribeArgs = Extract; +interface BbRealtime { + subscribe(args: BbRealtimeSubscribeArgs): BbRealtimeUnsubscribe; +} + +interface StatusGetArgs { + projectId?: string; + signal?: AbortSignal; + threadId?: string; +} +interface StatusThreadSummary { + environmentId: string | null; + id: string; + parentThreadId: string | null; + pinnedAt: number | null; + projectId: string; + status: ThreadStatus; + title: string | null; +} +type StatusProject = ProjectResponse; +type StatusChildThreads = ThreadListResponse; +interface StatusResult { + childThreads: StatusChildThreads | null; + pendingTodos: ThreadTimelinePendingTodos | null; + project: StatusProject | null; + thread: StatusThreadSummary | null; +} +interface StatusArea { + get(args?: StatusGetArgs): Promise; +} + +type ThemeGetResult = AppTheme; +type ThemeCatalogResult = ThemeCatalogResponse; +type ThemeSetInput = AppThemeSelection; +type ThemeSetResult = AppTheme; +interface ThemeCatalogArgs { + signal?: AbortSignal; +} +interface ThemeGetArgs { + signal?: AbortSignal; +} +interface ThemeArea { + /** The active app palette, resolved server-side (built-in id or custom CSS). */ + get(args?: ThemeGetArgs): Promise; + /** The custom-theme directory plus discovered themes and the active palette. */ + catalog(args?: ThemeCatalogArgs): Promise; + /** Set the complete app appearance selection in one request. */ + set(selection: ThemeSetInput): Promise; + /** + * Activate a palette by id while preserving the active favicon color. This + * compatibility shorthand reads the active appearance before writing the + * complete selection; prefer the object form when both values are known. + */ + set(themeId: string): Promise; +} + +interface SystemAttentionArgs { + signal?: AbortSignal; +} +interface SystemConfigArgs { + signal?: AbortSignal; +} +interface SystemExecutionOptionsArgs extends SystemExecutionOptionsQuery { + signal?: AbortSignal; +} +interface SystemUsageLimitsArgs extends SystemUsageLimitsQuery { + signal?: AbortSignal; +} +interface SystemVersionArgs { + force?: boolean; + signal?: AbortSignal; +} +interface SystemVoiceTranscriptionArgs { + file: Blob; + prompt?: string; + signal?: AbortSignal; +} +type SystemAttentionResult = SystemAttentionResponse; +type SystemConfigResult = SystemConfigResponse; +type SystemExecutionOptionsResult = SystemExecutionOptionsResponse; +type SystemReloadConfigResult = SystemConfigReloadResponse; +type SystemVoiceTranscriptionResult = SystemVoiceTranscriptionResponse; +type SystemUpdateExperimentsResult = Experiments; +type SystemUpdateGeneralSettingsResult = AppSettings; +type SystemUpdateKeyboardSettingsResult = AppKeybindingOverrides; +type SystemUsageLimitsResult = ProviderUsageResponse; +type SystemVersionResult = SystemVersionResponse; +interface SystemArea { + attention(args?: SystemAttentionArgs): Promise; + config(args?: SystemConfigArgs): Promise; + executionOptions(args?: SystemExecutionOptionsArgs): Promise; + reloadConfig(): Promise; + transcribeVoice(args: SystemVoiceTranscriptionArgs): Promise; + updateExperiments(args: Experiments): Promise; + updateGeneralSettings(args: AppSettings): Promise; + updateKeyboardSettings(args: AppKeybindingOverrides): Promise; + usageLimits(args?: SystemUsageLimitsArgs): Promise; + version(args?: SystemVersionArgs): Promise; +} + +interface TerminalThreadScope { + cwd?: never; + environmentId?: never; + hostId?: never; + kind: "thread"; + threadId: string; +} +interface TerminalEnvironmentScope { + environmentId: string; + cwd?: never; + hostId?: never; + kind: "environment"; + threadId?: never; +} +interface TerminalHostPathListScope { + /** Optional exact initial working-directory filter on the selected host. */ + cwd?: string; + environmentId?: never; + hostId: string; + kind: "host_path"; + threadId?: never; +} +interface TerminalHostPathCreateScope { + /** Null starts in the selected host's home directory. */ + cwd: string | null; + environmentId?: never; + hostId: string; + kind: "host_path"; + threadId?: never; +} +type TerminalListScope = TerminalThreadScope | TerminalEnvironmentScope | TerminalHostPathListScope; +type TerminalCreateScope = TerminalThreadScope | TerminalEnvironmentScope | TerminalHostPathCreateScope; +interface TerminalListArgs { + signal?: AbortSignal; + scope: TerminalListScope; +} +interface TerminalCreateArgs { + cols: number; + rows: number; + scope: TerminalCreateScope; + start?: CreateTerminalRequest["start"]; + title?: string; +} +interface TerminalTargetArgs { + terminalId: string; +} +interface TerminalGetArgs extends TerminalTargetArgs { + signal?: AbortSignal; +} +interface TerminalRenameArgs extends TerminalTargetArgs { + title: UpdateTerminalRequest["title"]; +} +interface TerminalCloseArgs extends TerminalTargetArgs { + mode: "force" | "if-clean"; +} +interface TerminalInputArgs extends TerminalTargetArgs { + dataBase64: TerminalInputRequest["dataBase64"]; +} +interface TerminalResizeArgs extends TerminalTargetArgs { + cols: TerminalResizeRequest["cols"]; + rows: TerminalResizeRequest["rows"]; +} +interface TerminalOutputArgs extends TerminalTargetArgs { + limitChunks?: TerminalOutputQuery["limitChunks"]; + signal?: AbortSignal; + sinceSeq?: TerminalOutputQuery["sinceSeq"]; + tailBytes?: TerminalOutputQuery["tailBytes"]; +} +type TerminalRestartArgs = TerminalTargetArgs; +type TerminalListResult = TerminalListResponse; +type TerminalCreateResult = TerminalSession; +type TerminalGetResult = TerminalSession; +type TerminalRenameResult = TerminalSession; +type TerminalCloseResult = TerminalSession; +type TerminalInputResult = TerminalSession; +type TerminalResizeResult = TerminalSession; +type TerminalOutputResult = TerminalOutputResponse; +type TerminalRestartResult = TerminalSession; +interface TerminalsArea { + close(args: TerminalCloseArgs): Promise; + create(args: TerminalCreateArgs): Promise; + get(args: TerminalGetArgs): Promise; + input(args: TerminalInputArgs): Promise; + list(args: TerminalListArgs): Promise; + output(args: TerminalOutputArgs): Promise; + rename(args: TerminalRenameArgs): Promise; + /** + * Replace a terminal with a shell at the same scope, size, and title. + * The original command is not replayed because terminal sessions do not + * persist launch commands. The replacement has a new terminal ID. + */ + restart(args: TerminalRestartArgs): Promise; + resize(args: TerminalResizeArgs): Promise; +} + +interface ThreadListArgs { + archived?: boolean; + excludeSideChats?: boolean; + sectionId?: string; + hasParent?: boolean; + includeHidden?: boolean; + limit?: number; + offset?: number; + originKind?: ThreadListQuery["originKind"]; + parentThreadId?: string; + projectId?: string; + signal?: AbortSignal; + sourceThreadId?: string; + unsectioned?: boolean; +} +interface ThreadSearchArgs extends ThreadSearchQuery { + signal?: AbortSignal; +} +interface ThreadGetArgs { + include?: ThreadGetQuery["include"]; + signal?: AbortSignal; + threadId: string; +} +type ThreadGetResult = ThreadResponse | ThreadWithIncludesResponse; +type ThreadListResult = ThreadListResponse; +type ThreadSearchResult = ThreadSearchResponse; +interface ThreadOutputResponse { + output: string | null; +} +type ThreadMutationResult = ThreadResponse; +type ThreadSpawnResult = ThreadResponse; +type ThreadForkResult = ThreadResponse; +type ThreadInteractionGetResult = PendingInteraction; +type ThreadInteractionListResult = ThreadPendingInteractionsResponse; +type ThreadInteractionResolveResult = PendingInteraction; +type ThreadInteractionRespondResult = PendingInteraction; +type ThreadInteractionCancelResult = PendingInteraction; +type ThreadEventsListResult = ThreadEventRow[]; +type ThreadEventWaitResult = ThreadEventRow | null; +type ThreadTimelineResult = ThreadTimelineResponse; +type ThreadArchiveResult = ThreadArchiveAllResponse; +type ThreadOpenResult = ThreadOpenResponse; +type ThreadPaneActionResult = ThreadPaneActionResponse; +type ThreadDeleteResult = { + ok: true; +}; +type ThreadSendResult = { + ok: true; +}; +type ThreadStopResult = { + ok: true; +}; +type ThreadBannerActionResult = { + ok: true; +}; +type ThreadUnarchiveResult = { + ok: true; +}; +type ThreadArchiveAllResult = ThreadArchiveAllResponse; +type ThreadReadStateResult = ThreadResponse; +type ThreadPinOrderResult = ThreadListResponse; +type ThreadPromptHistoryResult = PromptHistoryResponse; +type ThreadQueuedMessagesResult = ThreadQueuedMessageListResponse; +type ThreadQueuedMessageCreateResult = ThreadQueuedMessage; +type ThreadQueuedMessageUpdateResult = ThreadQueuedMessage; +type ThreadQueuedMessageDeleteResult = { + ok: true; +}; +type ThreadQueuedMessageReorderResult = ThreadQueuedMessageListResponse; +type ThreadQueuedMessageSendResult = SendQueuedMessageResponse; +type ThreadQueuedMessageGroupBoundaryResult = ThreadQueuedMessageListResponse; +type ThreadTabsResult = ThreadTabsResponse; +type ThreadTabsUpdateResult = ThreadTabsResponse; +type ThreadStorageFilesResult = ThreadStorageFileListResponse; +type ThreadStoragePathsResult = ThreadStoragePathListResponse; +type ThreadChildSummaryResult = ThreadChildSummaryResponse; +type ThreadDefaultExecutionOptionsResult = ResolvedThreadExecutionOptions | null; +type ThreadConversationOutlineResult = ThreadConversationOutlineResponse; +type ThreadTimelineTurnSummaryDetailsResult = TimelineTurnSummaryDetailsResponse; +interface ThreadSpawnBaseArgs extends Omit { + childOrigin?: CreateThreadRequest["childOrigin"]; + origin?: CreateThreadRequest["origin"]; + originKind?: CreateThreadRequest["originKind"]; + startedOnBehalfOf?: CreateThreadRequest["startedOnBehalfOf"]; +} +type ThreadSpawnArgs = ThreadSpawnBaseArgs & ({ + input: CreateThreadRequest["input"]; + prompt?: never; +} | { + input?: never; + prompt: string; +}); +interface ThreadForkArgs extends Omit { + origin?: ForkThreadRequest["origin"]; + visibility?: ForkThreadRequest["visibility"]; + workspace?: ForkThreadRequest["workspace"]; +} +interface ThreadUpdateArgs extends UpdateThreadRequest { + threadId: string; +} +interface ThreadDeleteArgs extends DeleteThreadRequest { + threadId: string; +} +interface ThreadSendArgs extends SendMessageRequest { + threadId: string; +} +interface ThreadActionArgs { + threadId: string; +} +interface ThreadStatusArgs extends ThreadActionArgs { + signal?: AbortSignal; +} +interface ThreadPromptHistoryArgs extends PromptHistoryQuery { + signal?: AbortSignal; + threadId: string; +} +interface ThreadPinOrderArgs extends ReorderPinnedThreadRequest { + threadId: string; +} +interface ThreadQueuedMessageArgs { + signal?: AbortSignal; + threadId: string; +} +interface ThreadQueuedMessageCreateArgs extends CreateQueuedMessageRequest { + threadId: string; +} +interface ThreadQueuedMessageUpdateArgs extends ThreadQueuedMessageTargetArgs, UpdateQueuedMessageRequest { +} +interface ThreadQueuedMessageTargetArgs { + queuedMessageId: string; + threadId: string; +} +interface ThreadQueuedMessageSendArgs extends ThreadQueuedMessageTargetArgs, SendQueuedMessageRequest { +} +interface ThreadQueuedMessageReorderArgs extends ThreadQueuedMessageTargetArgs, ReorderQueuedMessageRequest { +} +interface ThreadQueuedMessageGroupBoundaryArgs extends SetQueuedMessageGroupBoundaryRequest { + threadId: string; +} +interface ThreadStorageFilesArgs extends ThreadStorageFilesQuery { + signal?: AbortSignal; + threadId: string; +} +interface ThreadStoragePathsArgs extends ThreadStoragePathsQuery { + signal?: AbortSignal; + threadId: string; +} +interface ThreadTimelineTurnSummaryDetailsArgs extends TimelineTurnSummaryDetailsQuery { + signal?: AbortSignal; + threadId: string; +} +interface ThreadTabsUpdateArgs extends UpdateThreadTabsRequest { + threadId: string; +} +interface ThreadOpenArgs { + threadId: string; + split?: ThreadOpenSplit; + file: ThreadOpenFile | null; +} +interface ThreadPaneActionArgs { + action: ThreadPaneAction; + threadId: string; +} +interface ThreadEventsListArgs { + afterSeq?: string; + limit?: string; + signal?: AbortSignal; + threadId: string; +} +interface ThreadEventWaitArgs { + afterSeq?: string; + signal?: AbortSignal; + threadId: string; + type: string; + waitMs: string; +} +interface ThreadTimelineArgs extends ThreadTimelineQuery { + signal?: AbortSignal; + threadId: string; +} +interface ThreadOutputArgs { + signal?: AbortSignal; + threadId: string; +} +interface ThreadInteractionListArgs { + signal?: AbortSignal; + threadId: string; +} +interface ThreadInteractionTargetArgs { + interactionId: string; + threadId: string; +} +interface ThreadInteractionGetArgs extends ThreadInteractionTargetArgs { + signal?: AbortSignal; +} +interface ThreadInteractionResolveArgs extends ThreadInteractionTargetArgs { + resolution: PendingInteractionResolution; +} +interface ThreadInteractionRespondArgs extends ThreadInteractionTargetArgs { + value: JsonValue; +} +type ThreadWaitTarget = { + kind: "status"; + status: ThreadStatus; +} | { + kind: "event"; + eventType: string; +}; +interface ThreadWaitArgs { + event?: string; + pollIntervalMs?: number; + signal?: AbortSignal; + status?: ThreadStatus; + threadId: string; + timeoutMs?: number; +} +type ThreadWaitResult = { + event: NonNullable; + matched: true; + target: Extract; + threadId: string; +} | { + matched: true; + target: Extract; + thread: ThreadGetResult; + threadId: string; +}; +interface ThreadInteractionsArea { + cancel(args: ThreadInteractionTargetArgs): Promise; + get(args: ThreadInteractionGetArgs): Promise; + list(args: ThreadInteractionListArgs): Promise; + resolve(args: ThreadInteractionResolveArgs): Promise; + respond(args: ThreadInteractionRespondArgs): Promise; +} +interface ThreadEventsArea { + list(args: ThreadEventsListArgs): Promise; + wait(args: ThreadEventWaitArgs): Promise; +} +interface ThreadQueuedMessagesArea { + create(args: ThreadQueuedMessageCreateArgs): Promise; + delete(args: ThreadQueuedMessageTargetArgs): Promise; + list(args: ThreadQueuedMessageArgs): Promise; + reorder(args: ThreadQueuedMessageReorderArgs): Promise; + send(args: ThreadQueuedMessageSendArgs): Promise; + setGroupBoundary(args: ThreadQueuedMessageGroupBoundaryArgs): Promise; + update(args: ThreadQueuedMessageUpdateArgs): Promise; +} +interface ThreadTabsArea { + get(args: ThreadStatusArgs): Promise; + update(args: ThreadTabsUpdateArgs): Promise; +} +interface ThreadsArea { + archive(args: ThreadActionArgs): Promise; + archiveAll(args: ThreadActionArgs): Promise; + childSummary(args: ThreadStatusArgs): Promise; + cancelPlan(args: ThreadActionArgs): Promise; + clearGoal(args: ThreadActionArgs): Promise; + conversationOutline(args: ThreadStatusArgs): Promise; + defaultExecutionOptions(args: ThreadStatusArgs): Promise; + delete(args: ThreadDeleteArgs): Promise; + events: ThreadEventsArea; + fork(args: ThreadForkArgs): Promise; + get(args: ThreadGetArgs): Promise; + interactions: ThreadInteractionsArea; + list(args?: ThreadListArgs): Promise; + markRead(args: ThreadActionArgs): Promise; + markUnread(args: ThreadActionArgs): Promise; + open(args: ThreadOpenArgs): Promise; + paneAction(args: ThreadPaneActionArgs): Promise; + output(args: ThreadOutputArgs): Promise; + pin(args: ThreadActionArgs): Promise; + promptHistory(args: ThreadPromptHistoryArgs): Promise; + queuedMessages: ThreadQueuedMessagesArea; + reorderPinned(args: ThreadPinOrderArgs): Promise; + search(args: ThreadSearchArgs): Promise; + send(args: ThreadSendArgs): Promise; + spawn(args: ThreadSpawnArgs): Promise; + stop(args: ThreadActionArgs): Promise; + tabs: ThreadTabsArea; + timeline(args: ThreadTimelineArgs): Promise; + timelineTurnSummaryDetails(args: ThreadTimelineTurnSummaryDetailsArgs): Promise; + storageFiles(args: ThreadStorageFilesArgs): Promise; + storagePaths(args: ThreadStoragePathsArgs): Promise; + unarchive(args: ThreadActionArgs): Promise; + unpin(args: ThreadActionArgs): Promise; + update(args: ThreadUpdateArgs): Promise; + wait(args: ThreadWaitArgs): Promise; +} + +type ThreadSectionCreateResult = ThreadSectionResponse; +type ThreadSectionUpdateResult = ThreadSectionMutationResponse; +type ThreadSectionDeleteResult = ThreadSectionMutationResponse; +type ThreadSectionListResult = ThreadSectionResponse[]; +interface ThreadSectionListArgs { + signal?: AbortSignal; +} +interface ThreadSectionsArea { + create(args: CreateThreadSectionRequest): Promise; + delete(args: DeleteThreadSectionRequest): Promise; + list(args?: ThreadSectionListArgs): Promise; + update(args: UpdateThreadSectionRequest): Promise; +} + +interface BbSdk extends BbRealtime { + environments: EnvironmentsArea; + files: FilesArea; + guide: GuideArea; + hosts: HostsArea; + projects: ProjectsArea; + plugins: PluginsArea; + providers: ProvidersArea; + status: StatusArea; + system: SystemArea; + terminals: TerminalsArea; + theme: ThemeArea; + threadSections: ThreadSectionsArea; + threads: ThreadsArea; +} + +/** + * The backend plugin API contract — the `bb` object handed to a plugin's + * `server.ts` factory (`export default function plugin(bb: BbPluginApi)`). + * + * Types only: the implementation lives in the BB server + * (apps/server/src/services/plugins/plugin-api.ts), which imports these + * shapes so the contract and the implementation cannot drift. Plugin authors + * import them type-only (`import type { BbPluginApi } from + * "@bb/plugin-sdk"`); the import is erased when BB loads the file. + * + * Runtime classes stay host-side. NeedsConfigurationError in particular is + * matched by NAME, so plugin code needs no runtime import: + * `throw Object.assign(new Error(msg), { name: "NeedsConfigurationError" })`. + */ +interface PluginLogger { + debug(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} +/** + * Declarative settings descriptors (`bb.settings.define`). Deliberately plain + * data — not zod — so the host can render settings forms and the CLI can + * parse values without executing plugin code. + */ +type PluginSettingDescriptor = { + type: "string"; + label: string; + description?: string; + /** Stored in a 0600 file under /plugins//secrets/, never in the db or sent to the frontend. */ + secret?: true; + default?: string; +} | { + type: "boolean"; + label: string; + description?: string; + default?: boolean; +} | { + type: "select"; + label: string; + description?: string; + options: string[]; + default?: string; +} | { + type: "project"; + label: string; + description?: string; + default?: string; +}; +type PluginSettingDescriptors = Record; +type PluginSettingValue = string | boolean; +/** `default` present → non-optional value; absent → `T | undefined`. */ +type PluginSettingsValues> = { + [K in keyof Ds]: Ds[K] extends { + default: string | boolean; + } ? PluginSettingValueOf : PluginSettingValueOf | undefined; +}; +type PluginSettingValueOf = D extends { + type: "boolean"; +} ? boolean : string; +interface PluginSettingsHandle> { + /** Load-safe: callable inside the factory. */ + get(): Promise>; + /** Fires after values change through the settings route/CLI. */ + onChange(listener: (next: PluginSettingsValues, prev: PluginSettingsValues) => void): void; +} +interface PluginSettings { + define>(descriptors: Ds): PluginSettingsHandle; +} +interface PluginKvStorage { + get(key: string): Promise; + set(key: string, value: unknown): Promise; + delete(key: string): Promise; + list(prefix?: string): Promise; +} +interface PluginStorage { + /** Namespaced JSON key-value rows in bb.db; values ≤256KB each. */ + kv: PluginKvStorage; + /** + * Open (or reuse the path of) the plugin's own SQLite database at + * /plugins//data.db — the server's better-sqlite3, WAL mode, + * busy_timeout 5000. Handles are host-tracked and closed on + * dispose/reload; a closed handle throws on use. + */ + database(): Database.Database; + /** + * Ordered-statement migration helper: statement index = migration id in a + * `_bb_migrations` table; unapplied statements run in one transaction. + * Append-only — never reorder or edit shipped statements. + */ + migrate(db: Database.Database, statements: string[]): void; +} +/** + * Thread lifecycle events a plugin can observe (design §4.5). Observe-only: + * handlers run fire-and-forget after the transition is applied and can never + * block or veto it. `thread` is the same public DTO GET /threads/:id serves. + */ +interface PluginThreadEventPayloads { + /** Fired after a thread row is created. */ + "thread.created": { + thread: ThreadResponse; + }; + /** Fired when a thread transitions into `active`. */ + "thread.active": { + thread: ThreadResponse; + }; + /** Fired when a thread transitions into `idle`. `lastAssistantText` is + * assembled the same way GET /threads/:id/output is. */ + "thread.idle": { + thread: ThreadResponse; + lastAssistantText: string | null; + }; + /** Fired when a thread transitions into `error`. `error` is the latest + * system/error event message, when one exists. */ + "thread.failed": { + thread: ThreadResponse; + error: string | null; + }; + /** Fired after a thread is archived (including cascade archives). */ + "thread.archived": { + thread: ThreadResponse; + }; + /** Fired after a thread is soft-deleted. */ + "thread.deleted": { + thread: ThreadResponse; + }; +} +type PluginThreadEventName = keyof PluginThreadEventPayloads; +type PluginThreadEventHandler = (payload: PluginThreadEventPayloads[E]) => void | Promise; +type PluginHttpAuthMode = "local" | "token" | "none"; +type PluginHttpHandler = (context: Context) => Response | Promise; +interface PluginHttp { + /** + * Register an HTTP route, mounted at + * `/api/v1/plugins//http/`. Auth modes (default "local"): + * - "local": Origin/Host must be a local BB app origin; non-GET requires + * content-type application/json (forces a CORS preflight). + * - "token": requires the per-plugin token (`bb plugin token `) via + * the x-bb-plugin-token header or ?token=. + * - "none": no checks — only for signature-verified webhooks. + */ + route(method: string, path: string, handler: PluginHttpHandler, opts?: { + auth?: PluginHttpAuthMode; + }): void; +} +interface PluginRpc { + /** + * Register a Standard Schema-driven rpc contract and its inferred handlers, + * served at POST + * `/api/v1/plugins//rpc/` with "local" auth semantics. The + * host validates input before invocation and output before strict JSON + * serialization. The response is `{ ok: true, result }` or + * `{ ok: false, error: { code, message, issues? } }`. + */ + register(contract: Contract, handlers: PluginRpcHandlers): void; +} +interface PluginRealtime { + /** + * Broadcast an ephemeral `plugin-signal` WS message + * `{ pluginId, channel, payload }` to every connected client (V1 has no + * per-channel subscriptions). `payload` must be JSON-serializable; + * `undefined` is normalized to `null`. Nothing is persisted. + */ + publish(channel: string, payload: unknown): void; +} +interface PluginBackground { + /** + * Register a long-lived background service. `start` runs after the + * factory completes and should resolve when `signal` aborts + * (dispose/reload/disable/shutdown). A crash restarts it with capped + * exponential backoff; throwing NeedsConfigurationError marks the plugin + * `needs-configuration` and stops restarting until the next load. + */ + service(name: string, service: { + start(signal: AbortSignal): void | Promise; + }): void; + /** + * Register a cron schedule (5-field expression, server-local time). The + * durable row keyed (pluginId, name) is upserted at load; the periodic + * sweep claims due rows with a CAS on next_run_at, but only while this + * plugin is loaded. Failures land in last_status/last_error, visible in + * `bb plugin list`. + */ + schedule(name: string, cron: string, fn: () => void | Promise): void; +} +interface PluginCliCommandInfo { + name: string; + summary: string; + usage: string; +} +/** Context forwarded from the invoking CLI when known; all fields optional. */ +interface PluginCliContext { + cwd?: string; + threadId?: string; + projectId?: string; + /** Aborted when the invoking CLI HTTP request disconnects. */ + signal?: AbortSignal; +} +type PluginInteractionCancelReason = "user" | "request-aborted" | "thread-stopped" | "thread-deleted" | "plugin-disposed" | "server-restarted" | "timeout"; +type PluginInteractionResult = { + outcome: "submitted"; + value: JsonValue$1; +} | { + outcome: "cancelled"; + reason: PluginInteractionCancelReason; +}; +interface PluginInteractionRequest { + threadId: string; + rendererId: string; + title: string; + payload: JsonValue$1; + /** Defaults to ten minutes; capped at one hour. */ + timeoutMs?: number; +} +interface PluginCliResult { + exitCode: number; + stdout?: string; + stderr?: string; +} +/** + * Maximum combined UTF-8 bytes accepted from plugin CLI stdout and stderr. + * This is the shared source of truth for production and the testing harness. + */ +declare const PLUGIN_CLI_OUTPUT_MAX_BYTES: number; +interface PluginCliOutputLimitError { + code: "plugin_cli_output_too_large"; + message: string; + maxBytes: number; + stdoutBytes: number; + stderrBytes: number; + totalBytes: number; +} +/** Normalized host result returned by the plugin CLI HTTP/testing boundary. */ +interface PluginCliExecutionResult { + exitCode: number; + stdout: string; + stderr: string; + error?: PluginCliOutputLimitError; +} +interface PluginCliRegistration { + /** Top-level command name (`bb …`): lowercase [a-z0-9-]+, and not + * a core bb command (see RESERVED_BB_CLI_COMMANDS in the server). */ + name: string; + summary: string; + /** Subcommand metadata rendered in help and the plugin-commands skill + * without executing plugin code. Parsing argv is plugin-owned. */ + commands?: PluginCliCommandInfo[]; + run(argv: string[], ctx: PluginCliContext): PluginCliResult | Promise; +} +interface PluginCli { + /** + * Register this plugin's `bb` subcommand. One registration per factory + * execution; a repeated call is rejected. Core bb commands always win + * name collisions; reserved names are rejected at registration. + */ + register(registration: PluginCliRegistration): void; +} +/** Per-turn context handed to bb.agents context providers (design §4.4). */ +/** MCP-style content parts a native tool may return (design §4.4). */ +type PluginAgentToolContentPart = { + type: "text"; + text: string; +} | { + type: "image"; + data: string; + mimeType: string; +}; +type PluginAgentToolResult = string | { + content: PluginAgentToolContentPart[]; + isError?: boolean; +}; +/** Per-call context handed to a native tool's execute (design §4.4). */ +interface PluginAgentToolContext { + threadId: string; + projectId: string; + /** The tool-call request's abort signal (aborts if the daemon round-trip + * is torn down mid-call). */ + signal: AbortSignal; +} +interface PluginAgentToolRegistrationBase { + /** Tool name shown to the model: [a-zA-Z0-9_-]+, unique across plugins, + * and not a built-in dynamic tool (see RESERVED_AGENT_TOOL_NAMES in the + * server). */ + name: string; + description: string; + /** + * Optional usage snippet appended to the thread instructions whenever + * this tool is in the session's tool set (mirrors the built-in + * update_environment_directory guidance). Limited to 4096 characters. + */ + instructions?: string; +} +/** Stable, plain-data context resolved by the server for one agent session. */ +interface PluginAgentConfigurationContext { + thread: { + id: string; + title: string | null; + parentThreadId: string | null; + sourceThreadId: string | null; + }; + project: { + id: string; + kind: "standard" | "personal"; + name: string; + gitRemoteUrl: string | null; + }; + environment: { + id: string; + name: string | null; + path: string | null; + workspaceProvisionType: "unmanaged" | "managed-worktree" | "personal"; + branchName: string | null; + }; + host: { + id: string; + name: string; + }; + provider: { + id: string; + model: string; + }; + sideChat: boolean; + origin: { + kind: "fork" | "side-chat" | null; + pluginId: string | null; + }; +} +/** Object form of a {@link PluginAgentConfiguration} tools entry: selects a + * registered tool and overrides the parameter schema advertised to the + * provider for this resolution only. */ +interface PluginAgentToolSelection { + /** Name of a tool registered by this plugin via `registerTool`. */ + name: string; + /** JSON-schema object (root `type: "object"`, JSON-serializable, at most + * 128 KiB serialized) sent to the provider in place of the registered + * parameter schema. Execution-side validation still runs the registered + * parameters, so the override must only narrow what the registered schema + * already accepts. */ + parameters: Record; +} +/** Per-resolution selection returned by {@link PluginAgents.configure}. */ +interface PluginAgentConfiguration { + /** Tool names registered by this plugin, or {@link PluginAgentToolSelection} + * entries to also override a tool's advertised parameter schema for this + * resolution. Duplicate or unknown names, or an invalid override, reject + * this plugin's complete selection for the resolution. */ + tools: Array; + /** Skill frontmatter names from this plugin's manifest skill roots. + * Duplicate or unknown names reject this plugin's complete selection. */ + skills: string[]; + /** Optional dynamic instructions. Output is truncated to 4096 characters. */ + instructions?: string; +} +interface PluginAgents { + /** + * Select this plugin's statically registered tools and manifest skills for + * each thread/session resolution, with optional dynamic instructions. The + * callback is synchronous and runs at `thread.start` / `turn.submit`; it + * never rebuilds registrations. Exactly one callback may be registered per + * factory execution. A throw, malformed result, duplicate id, unknown id, + * or more than 256 tool/skill ids fails closed for this plugin only. + * + * Tools take effect when the provider session is next started or resumed; + * an already-running session is not hot-mutated. Instructions are resolved + * for the next turn. Skill changes follow BB's environment runtime policy: + * a busy runtime keeps its current catalog until a safe relaunch. Side-chat + * threads receive `sideChat: true`, and their returned tool, skill, and + * dynamic-instruction selections apply at the same boundaries. Independent + * side-chat safety policy (such as permission escalation) is unchanged. + */ + configure(provider: (context: PluginAgentConfigurationContext) => PluginAgentConfiguration): void; + /** + * Register a native dynamic tool (design §4.4). `parameters` is either a + * zod schema (validated per call; execute receives the parsed value) or a + * plain JSON-schema object (no validation; execute receives the raw + * arguments as `unknown`). Tool-set changes apply on the NEXT session + * start — a tool registered mid-session is not hot-added to running + * provider sessions. A second registration of the same name within this + * plugin is rejected; a name already registered by another plugin is + * rejected and surfaced as this plugin's status detail. + */ + registerTool(tool: PluginAgentToolRegistrationBase & { + parameters: Schema; + execute(params: z.output, ctx: PluginAgentToolContext): PluginAgentToolResult | Promise; + }): void; + registerTool(tool: PluginAgentToolRegistrationBase & { + /** Raw JSON-schema escape hatch; params arrive unvalidated. */ + parameters: Record; + execute(params: unknown, ctx: PluginAgentToolContext): PluginAgentToolResult | Promise; + }): void; + /** + * Contribute a dynamic section appended to thread instructions. The + * provider runs when a thread's runtime command config is resolved + * (thread.start / turn.submit); return null to contribute nothing for + * that resolution. Must be synchronous and fast — it sits on the + * thread-start path. Output longer than 4096 characters is truncated; a + * throwing provider is logged against the plugin and contributes nothing. + * A repeated registration within one factory execution is rejected. + * This legacy contribution is not applied to side-chat threads; use + * configure() when sideChat-aware dynamic instructions are required. + */ + contributeInstructions(provider: (ctx: { + threadId: string; + projectId: string; + }) => string | null): void; +} +interface PluginThreadActionContext { + threadId: string; + projectId: string; +} +interface PluginThreadActionToast { + kind: "success" | "error" | "info"; + message: string; +} +type PluginThreadActionResult = void | { + toast?: PluginThreadActionToast; +}; +interface PluginThreadActionRegistration { + /** Unique within this plugin: [a-zA-Z0-9_-]+ (becomes a URL segment). */ + id: string; + /** Button label rendered in the thread header. */ + title: string; + /** Optional icon name; the host falls back to a generic icon. */ + icon?: string; + /** Optional confirmation prompt the host shows before running. */ + confirm?: string; + /** + * Runs server-side when the user clicks the action. The host shows a + * pending state while in flight, the returned toast on completion, and an + * automatic error toast when this throws. + */ + run(ctx: PluginThreadActionContext): PluginThreadActionResult | Promise; +} +type PluginMentionTrigger = "@" | "#" | "$" | "!" | "~"; +/** Search context handed to a mention provider (design §4.9). `projectId`/ + * `threadId` are null when the composer has not committed one yet. */ +interface PluginMentionSearchContext { + trigger: PluginMentionTrigger; + query: string; + projectId: string | null; + threadId: string | null; +} +/** One row a mention provider returns from `search`. `id` is the provider's + * own item id — the host namespaces it before it reaches the wire. */ +interface PluginMentionItem { + id: string; + title: string; + subtitle?: string; + icon?: string; +} +interface PluginMentionProviderRegistration { + /** Unique within this plugin: [a-zA-Z0-9_-]+ (no ":" — the host composes + * wire item ids as ":"). */ + id: string; + /** Section label shown above this provider's rows in the mention menu. */ + label: string; + /** + * Composer trigger characters this provider should answer. Omit to use the + * default `@` mention trigger. Valid triggers are `@`, `#`, `$`, `!`, and `~`. + */ + triggers?: readonly PluginMentionTrigger[]; + /** + * Runs server-side as the user types after one of this provider's triggers + * in the composer. Each call is time-boxed (2s) and failure-isolated: a slow + * or throwing provider contributes an empty list — it can never break the + * mention menu. + */ + search(ctx: PluginMentionSearchContext): PluginMentionItem[] | Promise; + /** + * Resolves one picked item into agent context, called once per unique + * item at message send time. The returned `context` is attached to the + * message as an agent-visible (user-hidden) prompt input. Throwing blocks + * the send with a visible error. + */ + resolve(itemId: string): { + context: string; + } | Promise<{ + context: string; + }>; +} +interface PluginUi { + /** Block until the app submits or cancels a plugin-owned composer form. */ + requestInput(request: PluginInteractionRequest, options?: { + signal?: AbortSignal; + }): Promise; + /** + * Register a thread action rendered in the shipped app's thread header + * (design §4.9). Multiple actions per plugin; ids must be unique within + * the plugin. Invoked via POST /plugins/:id/actions/:actionId. + */ + registerThreadAction(action: PluginThreadActionRegistration): void; + /** + * Register a mention provider for the shipped app's composer (design §4.9). + * Providers default to the `@` trigger and may opt into `#`, `$`, `!`, or + * `~` with `triggers`. Items group under `label` in the mention menu; a + * picked item becomes a `{ kind: "plugin" }` mention resource whose context + * is resolved once at send time. Multiple providers per plugin; ids must be + * unique within the plugin. + */ + registerMentionProvider(provider: PluginMentionProviderRegistration): void; +} +interface PluginEvents { + /** + * Add a thread lifecycle listener. Multiple listeners for the same event are + * additive and run independently in registration order. + */ + on(event: E, handler: PluginThreadEventHandler): void; +} +interface PluginServerApi { + /** + * This BB server's own loopback base URL (e.g. "http://127.0.0.1:38886"), + * which serves the SPA + /api + /ws. For plugins that proxy or relay + * traffic back to the server itself (e.g. a tunnel). Bind-gated like + * `bb.sdk`: reading it before the server is listening throws, so prefer + * reading it from handlers, services, and timers. + */ + readonly loopbackBaseUrl: string; +} +interface PluginSharedPortTunnelIdentity { + /** Gate routing label assigned to this machine. */ + label: string; + /** Gate apex without a scheme, e.g. "getbb.app". */ + baseDomain: string; +} +interface PluginHosts { + /** + * Ensure this enrolled host has a gate label and return its read-only public + * identity. The daemon chooses the trusted gate and desired label; plugins + * cannot influence either credential-bearing destination. + */ + ensureSharedPortTunnel(hostId: string): Promise; + /** + * Replace this plugin's desired shared-loopback ports for one host. The + * server aggregates declarations, owns generations, and delivers the + * resulting set to that host's daemon. Tunnel identity is deliberately not + * accepted here: it is owned by the daemon's trusted enrollment. + */ + declareSharedPorts(hostId: string, ports: readonly number[]): void; +} +interface PluginStatusApi { + /** + * Mark this plugin `needs-configuration` (with a message shown in + * `bb plugin list` and the UI) instead of failing — e.g. a factory or + * service that finds no API key configured. Cleared on the next load; + * saving settings does not auto-reload in V1, so ask the user to + * `bb plugin reload ` after configuring. + */ + needsConfiguration(message: string): void; +} +/** + * The API object handed to a plugin's factory (design §4). Implemented by + * the BB server; this contract is what plugin `server.ts` files compile + * against. + */ +interface BbPluginApi { + /** The plugin's own id (namespaces storage, routes, commands). */ + readonly pluginId: string; + /** Leveled, plugin-scoped logger. */ + readonly log: PluginLogger; + /** Declarative settings (design §4.2). */ + readonly settings: PluginSettings; + /** Namespaced KV + per-plugin database (design §4.3). */ + readonly storage: PluginStorage; + /** HTTP routes under /api/v1/plugins//http/* (design §4.6). */ + readonly http: PluginHttp; + /** RPC methods under /api/v1/plugins//rpc/ (design §4.6). */ + readonly rpc: PluginRpc; + /** Ephemeral push to connected frontends (design §4.7). */ + readonly realtime: PluginRealtime; + /** Long-lived services + cron schedules (design §4.8). */ + readonly background: PluginBackground; + /** Agent-facing `bb` CLI subcommand (design §4.4). */ + readonly cli: PluginCli; + /** Per-turn agent context contributions (design §4.4). */ + readonly agents: PluginAgents; + /** Host-rendered UI contributions (design §4.9). */ + readonly ui: PluginUi; + /** Additive plugin lifecycle listeners (design §4.5). */ + readonly events: PluginEvents; + /** Plugin-reported status (needs-configuration). */ + readonly status: PluginStatusApi; + /** Read-only facts about the running server (loopback base URL). */ + readonly server: PluginServerApi; + /** Server-to-daemon host control-plane declarations. */ + readonly hosts: PluginHosts; + /** + * The full BB SDK, bound to this server over loopback (design §4.1). + * Bind-gated: reading this before the host binds the SDK throws. The real + * server binds it before loading plugins, so it is available from the + * moment factories run there — but isolated harnesses may not, so prefer + * using it from handlers, services, and timers for portability. + * `threads.spawn` defaults `origin` to "plugin" and `originPluginId` to + * this plugin's id so spawned threads are attributed automatically. + */ + readonly sdk: BbSdk; + /** + * Register cleanup to run on reload/disable/shutdown. Hooks run LIFO. + * The sanctioned place to clear timers and close connections. + */ + onDispose(hook: () => void | Promise): void; +} + +export { PLUGIN_CLI_OUTPUT_MAX_BYTES, defineRpcContract }; +export type { BbContext, BbNavigate, BbPluginApi, ComposerCustomization, ComposerPlusMenuItem, ComposerRichTextSpec, ComposerStructuredDraft, ComposerView, JsonValue$1 as JsonValue, MarkdownProps, PluginAgentConfiguration, PluginAgentConfigurationContext, PluginAgentToolContentPart, PluginAgentToolContext, PluginAgentToolRegistrationBase, PluginAgentToolResult, PluginAgentToolSelection, PluginAgents, PluginAppBuilder, PluginAppComposer, PluginAppContentScripts, PluginAppDefinition, PluginAppSetup, PluginAppSlots, PluginBackground, PluginCli, PluginCliCommandInfo, PluginCliContext, PluginCliExecutionResult, PluginCliOutputLimitError, PluginCliRegistration, PluginCliResult, PluginComposerApi, PluginComposerMention, PluginComposerScope, PluginComposerTextEffect, PluginComposerThreadRowStatus, PluginContentScriptContext, PluginContentScriptDisposer, PluginContentScriptRegistration, PluginEvents, PluginFileOpenerProps, PluginFileOpenerRegistration, PluginFileOpenerSource, PluginHomepageSectionProps, PluginHomepageSectionRegistration, PluginHosts, PluginHttp, PluginHttpAuthMode, PluginHttpHandler, PluginInteractionCancelReason, PluginInteractionRequest, PluginInteractionResult, PluginKvStorage, PluginLogger, PluginMentionItem, PluginMentionProviderRegistration, PluginMentionSearchContext, PluginMentionTrigger, PluginMessageActionContext, PluginMessageActionRegistration, PluginMessageActionThreadPanelOptions, PluginMessageDirectiveMessage, PluginMessageDirectiveOpenWorkspaceFile, PluginMessageDirectiveProps, PluginMessageDirectiveRegistration, PluginNavPanelProps, PluginNavPanelRegistration, PluginPendingInteractionProps, PluginPendingInteractionRegistration, PluginPendingInteractionView, PluginRealtime, PluginRealtimeConnectionState, PluginRpc, PluginRpcCallArgs, PluginRpcClient, PluginRpcContract, PluginRpcError, PluginRpcErrorCode, PluginRpcHandlers, PluginRpcIssuePathSegment, PluginRpcMethodContract, PluginRpcResult, PluginRpcValidationIssue, PluginSdkApp, PluginServerApi, PluginSettingDescriptor, PluginSettingDescriptors, PluginSettingValue, PluginSettings, PluginSettingsHandle, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginSettingsState, PluginSettingsValues, PluginSharedPortTunnelIdentity, PluginSidebarFooterActionContext, PluginSidebarFooterActionProps, PluginSidebarFooterActionRegistration, PluginStatusApi, PluginStorage, PluginThreadActionContext, PluginThreadActionRegistration, PluginThreadActionResult, PluginThreadActionToast, PluginThreadEventHandler, PluginThreadEventName, PluginThreadEventPayloads, PluginThreadPanelActionContext, PluginThreadPanelActionRegistration, PluginThreadPanelProps, PluginUi, StandardSchemaV1, StandardSchemaV1InferInput, StandardSchemaV1InferOutput, StandardSchemaV1Issue, StandardSchemaV1Result, ThreadChatMessageAction, ThreadChatMessageReference, ThreadChatProps }; diff --git a/plugins/timeline-comments/vitest.config.ts b/plugins/timeline-comments/vitest.config.ts new file mode 100644 index 0000000..3722de9 --- /dev/null +++ b/plugins/timeline-comments/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["*.test.ts", "*.test.tsx"], + testTimeout: 15_000, + }, +}); diff --git a/plugins/ui-patterns/dist/app.js b/plugins/ui-patterns/dist/app.js index 16b9143..56efc8f 100644 --- a/plugins/ui-patterns/dist/app.js +++ b/plugins/ui-patterns/dist/app.js @@ -8612,10 +8612,10 @@ var init_accordion_demo = __esm({ } }); -// node_modules/lucide-react/dist/esm/shared/src/utils.js +// ../../node_modules/lucide-react/dist/esm/shared/src/utils.js var toKebabCase, mergeClasses; var init_utils3 = __esm({ - "node_modules/lucide-react/dist/esm/shared/src/utils.js"() { + "../../node_modules/lucide-react/dist/esm/shared/src/utils.js"() { toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); mergeClasses = (...classes) => classes.filter((className, index4, array) => { return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index4; @@ -8623,10 +8623,10 @@ var init_utils3 = __esm({ } }); -// node_modules/lucide-react/dist/esm/defaultAttributes.js +// ../../node_modules/lucide-react/dist/esm/defaultAttributes.js var defaultAttributes2; var init_defaultAttributes = __esm({ - "node_modules/lucide-react/dist/esm/defaultAttributes.js"() { + "../../node_modules/lucide-react/dist/esm/defaultAttributes.js"() { defaultAttributes2 = { xmlns: "http://www.w3.org/2000/svg", width: 24, @@ -8641,10 +8641,10 @@ var init_defaultAttributes = __esm({ } }); -// node_modules/lucide-react/dist/esm/Icon.js +// ../../node_modules/lucide-react/dist/esm/Icon.js var Icon2; var init_Icon = __esm({ - "node_modules/lucide-react/dist/esm/Icon.js"() { + "../../node_modules/lucide-react/dist/esm/Icon.js"() { init_react(); init_defaultAttributes(); init_utils3(); @@ -8681,10 +8681,10 @@ var init_Icon = __esm({ } }); -// node_modules/lucide-react/dist/esm/createLucideIcon.js +// ../../node_modules/lucide-react/dist/esm/createLucideIcon.js var createLucideIcon; var init_createLucideIcon = __esm({ - "node_modules/lucide-react/dist/esm/createLucideIcon.js"() { + "../../node_modules/lucide-react/dist/esm/createLucideIcon.js"() { init_react(); init_utils3(); init_Icon(); @@ -8703,10 +8703,10 @@ var init_createLucideIcon = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/archive.js +// ../../node_modules/lucide-react/dist/esm/icons/archive.js var __iconNode, Archive; var init_archive = __esm({ - "node_modules/lucide-react/dist/esm/icons/archive.js"() { + "../../node_modules/lucide-react/dist/esm/icons/archive.js"() { init_createLucideIcon(); __iconNode = [ ["rect", { width: "20", height: "5", x: "2", y: "3", rx: "1", key: "1wp1u1" }], @@ -8717,10 +8717,10 @@ var init_archive = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/arrow-left.js +// ../../node_modules/lucide-react/dist/esm/icons/arrow-left.js var __iconNode2, ArrowLeft; var init_arrow_left = __esm({ - "node_modules/lucide-react/dist/esm/icons/arrow-left.js"() { + "../../node_modules/lucide-react/dist/esm/icons/arrow-left.js"() { init_createLucideIcon(); __iconNode2 = [ ["path", { d: "m12 19-7-7 7-7", key: "1l729n" }], @@ -8730,10 +8730,10 @@ var init_arrow_left = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/arrow-up-down.js +// ../../node_modules/lucide-react/dist/esm/icons/arrow-up-down.js var __iconNode3, ArrowUpDown; var init_arrow_up_down = __esm({ - "node_modules/lucide-react/dist/esm/icons/arrow-up-down.js"() { + "../../node_modules/lucide-react/dist/esm/icons/arrow-up-down.js"() { init_createLucideIcon(); __iconNode3 = [ ["path", { d: "m21 16-4 4-4-4", key: "f6ql7i" }], @@ -8745,10 +8745,10 @@ var init_arrow_up_down = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/arrow-up-right.js +// ../../node_modules/lucide-react/dist/esm/icons/arrow-up-right.js var __iconNode4, ArrowUpRight; var init_arrow_up_right = __esm({ - "node_modules/lucide-react/dist/esm/icons/arrow-up-right.js"() { + "../../node_modules/lucide-react/dist/esm/icons/arrow-up-right.js"() { init_createLucideIcon(); __iconNode4 = [ ["path", { d: "M7 7h10v10", key: "1tivn9" }], @@ -8758,10 +8758,10 @@ var init_arrow_up_right = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/arrow-up.js +// ../../node_modules/lucide-react/dist/esm/icons/arrow-up.js var __iconNode5, ArrowUp; var init_arrow_up = __esm({ - "node_modules/lucide-react/dist/esm/icons/arrow-up.js"() { + "../../node_modules/lucide-react/dist/esm/icons/arrow-up.js"() { init_createLucideIcon(); __iconNode5 = [ ["path", { d: "m5 12 7-7 7 7", key: "hav0vg" }], @@ -8771,10 +8771,10 @@ var init_arrow_up = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/audio-waveform.js +// ../../node_modules/lucide-react/dist/esm/icons/audio-waveform.js var __iconNode6, AudioWaveform; var init_audio_waveform = __esm({ - "node_modules/lucide-react/dist/esm/icons/audio-waveform.js"() { + "../../node_modules/lucide-react/dist/esm/icons/audio-waveform.js"() { init_createLucideIcon(); __iconNode6 = [ [ @@ -8789,10 +8789,10 @@ var init_audio_waveform = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/badge-check.js +// ../../node_modules/lucide-react/dist/esm/icons/badge-check.js var __iconNode7, BadgeCheck; var init_badge_check = __esm({ - "node_modules/lucide-react/dist/esm/icons/badge-check.js"() { + "../../node_modules/lucide-react/dist/esm/icons/badge-check.js"() { init_createLucideIcon(); __iconNode7 = [ [ @@ -8808,10 +8808,10 @@ var init_badge_check = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/bell.js +// ../../node_modules/lucide-react/dist/esm/icons/bell.js var __iconNode8, Bell; var init_bell = __esm({ - "node_modules/lucide-react/dist/esm/icons/bell.js"() { + "../../node_modules/lucide-react/dist/esm/icons/bell.js"() { init_createLucideIcon(); __iconNode8 = [ ["path", { d: "M10.268 21a2 2 0 0 0 3.464 0", key: "vwvbt9" }], @@ -8827,10 +8827,10 @@ var init_bell = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/bold.js +// ../../node_modules/lucide-react/dist/esm/icons/bold.js var __iconNode9, Bold; var init_bold = __esm({ - "node_modules/lucide-react/dist/esm/icons/bold.js"() { + "../../node_modules/lucide-react/dist/esm/icons/bold.js"() { init_createLucideIcon(); __iconNode9 = [ [ @@ -8842,10 +8842,10 @@ var init_bold = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/book-open.js +// ../../node_modules/lucide-react/dist/esm/icons/book-open.js var __iconNode10, BookOpen; var init_book_open = __esm({ - "node_modules/lucide-react/dist/esm/icons/book-open.js"() { + "../../node_modules/lucide-react/dist/esm/icons/book-open.js"() { init_createLucideIcon(); __iconNode10 = [ ["path", { d: "M12 7v14", key: "1akyts" }], @@ -8861,10 +8861,10 @@ var init_book_open = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/bookmark.js +// ../../node_modules/lucide-react/dist/esm/icons/bookmark.js var __iconNode11, Bookmark; var init_bookmark = __esm({ - "node_modules/lucide-react/dist/esm/icons/bookmark.js"() { + "../../node_modules/lucide-react/dist/esm/icons/bookmark.js"() { init_createLucideIcon(); __iconNode11 = [ ["path", { d: "m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z", key: "1fy3hk" }] @@ -8873,10 +8873,10 @@ var init_bookmark = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/bot.js +// ../../node_modules/lucide-react/dist/esm/icons/bot.js var __iconNode12, Bot; var init_bot = __esm({ - "node_modules/lucide-react/dist/esm/icons/bot.js"() { + "../../node_modules/lucide-react/dist/esm/icons/bot.js"() { init_createLucideIcon(); __iconNode12 = [ ["path", { d: "M12 8V4H8", key: "hb8ula" }], @@ -8890,10 +8890,10 @@ var init_bot = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/calculator.js +// ../../node_modules/lucide-react/dist/esm/icons/calculator.js var __iconNode13, Calculator; var init_calculator = __esm({ - "node_modules/lucide-react/dist/esm/icons/calculator.js"() { + "../../node_modules/lucide-react/dist/esm/icons/calculator.js"() { init_createLucideIcon(); __iconNode13 = [ ["rect", { width: "16", height: "20", x: "4", y: "2", rx: "2", key: "1nb95v" }], @@ -8911,10 +8911,10 @@ var init_calculator = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/calendar-plus.js +// ../../node_modules/lucide-react/dist/esm/icons/calendar-plus.js var __iconNode14, CalendarPlus; var init_calendar_plus = __esm({ - "node_modules/lucide-react/dist/esm/icons/calendar-plus.js"() { + "../../node_modules/lucide-react/dist/esm/icons/calendar-plus.js"() { init_createLucideIcon(); __iconNode14 = [ ["path", { d: "M8 2v4", key: "1cmpym" }], @@ -8928,10 +8928,10 @@ var init_calendar_plus = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/calendar.js +// ../../node_modules/lucide-react/dist/esm/icons/calendar.js var __iconNode15, Calendar; var init_calendar = __esm({ - "node_modules/lucide-react/dist/esm/icons/calendar.js"() { + "../../node_modules/lucide-react/dist/esm/icons/calendar.js"() { init_createLucideIcon(); __iconNode15 = [ ["path", { d: "M8 2v4", key: "1cmpym" }], @@ -8943,10 +8943,10 @@ var init_calendar = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/chart-pie.js +// ../../node_modules/lucide-react/dist/esm/icons/chart-pie.js var __iconNode16, ChartPie; var init_chart_pie = __esm({ - "node_modules/lucide-react/dist/esm/icons/chart-pie.js"() { + "../../node_modules/lucide-react/dist/esm/icons/chart-pie.js"() { init_createLucideIcon(); __iconNode16 = [ [ @@ -8962,30 +8962,30 @@ var init_chart_pie = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/chevron-down.js +// ../../node_modules/lucide-react/dist/esm/icons/chevron-down.js var __iconNode17, ChevronDown; var init_chevron_down = __esm({ - "node_modules/lucide-react/dist/esm/icons/chevron-down.js"() { + "../../node_modules/lucide-react/dist/esm/icons/chevron-down.js"() { init_createLucideIcon(); __iconNode17 = [["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]]; ChevronDown = createLucideIcon("ChevronDown", __iconNode17); } }); -// node_modules/lucide-react/dist/esm/icons/chevron-right.js +// ../../node_modules/lucide-react/dist/esm/icons/chevron-right.js var __iconNode18, ChevronRight; var init_chevron_right = __esm({ - "node_modules/lucide-react/dist/esm/icons/chevron-right.js"() { + "../../node_modules/lucide-react/dist/esm/icons/chevron-right.js"() { init_createLucideIcon(); __iconNode18 = [["path", { d: "m9 18 6-6-6-6", key: "mthhwq" }]]; ChevronRight = createLucideIcon("ChevronRight", __iconNode18); } }); -// node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js +// ../../node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js var __iconNode19, ChevronsUpDown; var init_chevrons_up_down = __esm({ - "node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js"() { + "../../node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js"() { init_createLucideIcon(); __iconNode19 = [ ["path", { d: "m7 15 5 5 5-5", key: "1hf1tw" }], @@ -8995,10 +8995,10 @@ var init_chevrons_up_down = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/circle-alert.js +// ../../node_modules/lucide-react/dist/esm/icons/circle-alert.js var __iconNode20, CircleAlert; var init_circle_alert = __esm({ - "node_modules/lucide-react/dist/esm/icons/circle-alert.js"() { + "../../node_modules/lucide-react/dist/esm/icons/circle-alert.js"() { init_createLucideIcon(); __iconNode20 = [ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }], @@ -9009,10 +9009,10 @@ var init_circle_alert = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/circle-check.js +// ../../node_modules/lucide-react/dist/esm/icons/circle-check.js var __iconNode21, CircleCheck; var init_circle_check = __esm({ - "node_modules/lucide-react/dist/esm/icons/circle-check.js"() { + "../../node_modules/lucide-react/dist/esm/icons/circle-check.js"() { init_createLucideIcon(); __iconNode21 = [ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }], @@ -9022,10 +9022,10 @@ var init_circle_check = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/circle-dashed.js +// ../../node_modules/lucide-react/dist/esm/icons/circle-dashed.js var __iconNode22, CircleDashed; var init_circle_dashed = __esm({ - "node_modules/lucide-react/dist/esm/icons/circle-dashed.js"() { + "../../node_modules/lucide-react/dist/esm/icons/circle-dashed.js"() { init_createLucideIcon(); __iconNode22 = [ ["path", { d: "M10.1 2.182a10 10 0 0 1 3.8 0", key: "5ilxe3" }], @@ -9041,10 +9041,10 @@ var init_circle_dashed = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/clock.js +// ../../node_modules/lucide-react/dist/esm/icons/clock.js var __iconNode23, Clock; var init_clock = __esm({ - "node_modules/lucide-react/dist/esm/icons/clock.js"() { + "../../node_modules/lucide-react/dist/esm/icons/clock.js"() { init_createLucideIcon(); __iconNode23 = [ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }], @@ -9054,10 +9054,10 @@ var init_clock = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/command.js +// ../../node_modules/lucide-react/dist/esm/icons/command.js var __iconNode24, Command; var init_command = __esm({ - "node_modules/lucide-react/dist/esm/icons/command.js"() { + "../../node_modules/lucide-react/dist/esm/icons/command.js"() { init_createLucideIcon(); __iconNode24 = [ [ @@ -9069,10 +9069,10 @@ var init_command = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/credit-card.js +// ../../node_modules/lucide-react/dist/esm/icons/credit-card.js var __iconNode25, CreditCard; var init_credit_card = __esm({ - "node_modules/lucide-react/dist/esm/icons/credit-card.js"() { + "../../node_modules/lucide-react/dist/esm/icons/credit-card.js"() { init_createLucideIcon(); __iconNode25 = [ ["rect", { width: "20", height: "14", x: "2", y: "5", rx: "2", key: "ynyp8z" }], @@ -9082,10 +9082,10 @@ var init_credit_card = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/ellipsis.js +// ../../node_modules/lucide-react/dist/esm/icons/ellipsis.js var __iconNode26, Ellipsis; var init_ellipsis = __esm({ - "node_modules/lucide-react/dist/esm/icons/ellipsis.js"() { + "../../node_modules/lucide-react/dist/esm/icons/ellipsis.js"() { init_createLucideIcon(); __iconNode26 = [ ["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }], @@ -9096,10 +9096,10 @@ var init_ellipsis = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/file-code.js +// ../../node_modules/lucide-react/dist/esm/icons/file-code.js var __iconNode27, FileCode; var init_file_code = __esm({ - "node_modules/lucide-react/dist/esm/icons/file-code.js"() { + "../../node_modules/lucide-react/dist/esm/icons/file-code.js"() { init_createLucideIcon(); __iconNode27 = [ ["path", { d: "M10 12.5 8 15l2 2.5", key: "1tg20x" }], @@ -9111,10 +9111,10 @@ var init_file_code = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/folder.js +// ../../node_modules/lucide-react/dist/esm/icons/folder.js var __iconNode28, Folder; var init_folder = __esm({ - "node_modules/lucide-react/dist/esm/icons/folder.js"() { + "../../node_modules/lucide-react/dist/esm/icons/folder.js"() { init_createLucideIcon(); __iconNode28 = [ [ @@ -9129,10 +9129,10 @@ var init_folder = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/forward.js +// ../../node_modules/lucide-react/dist/esm/icons/forward.js var __iconNode29, Forward; var init_forward = __esm({ - "node_modules/lucide-react/dist/esm/icons/forward.js"() { + "../../node_modules/lucide-react/dist/esm/icons/forward.js"() { init_createLucideIcon(); __iconNode29 = [ ["polyline", { points: "15 17 20 12 15 7", key: "1w3sku" }], @@ -9142,10 +9142,10 @@ var init_forward = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/frame.js +// ../../node_modules/lucide-react/dist/esm/icons/frame.js var __iconNode30, Frame; var init_frame = __esm({ - "node_modules/lucide-react/dist/esm/icons/frame.js"() { + "../../node_modules/lucide-react/dist/esm/icons/frame.js"() { init_createLucideIcon(); __iconNode30 = [ ["line", { x1: "22", x2: "2", y1: "6", y2: "6", key: "15w7dq" }], @@ -9157,10 +9157,10 @@ var init_frame = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/gallery-vertical-end.js +// ../../node_modules/lucide-react/dist/esm/icons/gallery-vertical-end.js var __iconNode31, GalleryVerticalEnd; var init_gallery_vertical_end = __esm({ - "node_modules/lucide-react/dist/esm/icons/gallery-vertical-end.js"() { + "../../node_modules/lucide-react/dist/esm/icons/gallery-vertical-end.js"() { init_createLucideIcon(); __iconNode31 = [ ["path", { d: "M7 2h10", key: "nczekb" }], @@ -9171,10 +9171,10 @@ var init_gallery_vertical_end = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/git-branch.js +// ../../node_modules/lucide-react/dist/esm/icons/git-branch.js var __iconNode32, GitBranch; var init_git_branch = __esm({ - "node_modules/lucide-react/dist/esm/icons/git-branch.js"() { + "../../node_modules/lucide-react/dist/esm/icons/git-branch.js"() { init_createLucideIcon(); __iconNode32 = [ ["line", { x1: "6", x2: "6", y1: "3", y2: "15", key: "17qcm7" }], @@ -9186,10 +9186,10 @@ var init_git_branch = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/info.js +// ../../node_modules/lucide-react/dist/esm/icons/info.js var __iconNode33, Info; var init_info = __esm({ - "node_modules/lucide-react/dist/esm/icons/info.js"() { + "../../node_modules/lucide-react/dist/esm/icons/info.js"() { init_createLucideIcon(); __iconNode33 = [ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }], @@ -9200,10 +9200,10 @@ var init_info = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/italic.js +// ../../node_modules/lucide-react/dist/esm/icons/italic.js var __iconNode34, Italic; var init_italic = __esm({ - "node_modules/lucide-react/dist/esm/icons/italic.js"() { + "../../node_modules/lucide-react/dist/esm/icons/italic.js"() { init_createLucideIcon(); __iconNode34 = [ ["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }], @@ -9214,10 +9214,10 @@ var init_italic = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/list-filter.js +// ../../node_modules/lucide-react/dist/esm/icons/list-filter.js var __iconNode35, ListFilter; var init_list_filter = __esm({ - "node_modules/lucide-react/dist/esm/icons/list-filter.js"() { + "../../node_modules/lucide-react/dist/esm/icons/list-filter.js"() { init_createLucideIcon(); __iconNode35 = [ ["path", { d: "M3 6h18", key: "d0wm0j" }], @@ -9228,10 +9228,10 @@ var init_list_filter = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/log-out.js +// ../../node_modules/lucide-react/dist/esm/icons/log-out.js var __iconNode36, LogOut; var init_log_out = __esm({ - "node_modules/lucide-react/dist/esm/icons/log-out.js"() { + "../../node_modules/lucide-react/dist/esm/icons/log-out.js"() { init_createLucideIcon(); __iconNode36 = [ ["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", key: "1uf3rs" }], @@ -9242,10 +9242,10 @@ var init_log_out = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/mail-check.js +// ../../node_modules/lucide-react/dist/esm/icons/mail-check.js var __iconNode37, MailCheck; var init_mail_check = __esm({ - "node_modules/lucide-react/dist/esm/icons/mail-check.js"() { + "../../node_modules/lucide-react/dist/esm/icons/mail-check.js"() { init_createLucideIcon(); __iconNode37 = [ ["path", { d: "M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8", key: "12jkf8" }], @@ -9256,10 +9256,10 @@ var init_mail_check = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/map.js +// ../../node_modules/lucide-react/dist/esm/icons/map.js var __iconNode38, Map2; var init_map = __esm({ - "node_modules/lucide-react/dist/esm/icons/map.js"() { + "../../node_modules/lucide-react/dist/esm/icons/map.js"() { init_createLucideIcon(); __iconNode38 = [ [ @@ -9276,10 +9276,10 @@ var init_map = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/plus.js +// ../../node_modules/lucide-react/dist/esm/icons/plus.js var __iconNode39, Plus; var init_plus = __esm({ - "node_modules/lucide-react/dist/esm/icons/plus.js"() { + "../../node_modules/lucide-react/dist/esm/icons/plus.js"() { init_createLucideIcon(); __iconNode39 = [ ["path", { d: "M5 12h14", key: "1ays0h" }], @@ -9289,10 +9289,10 @@ var init_plus = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/search.js +// ../../node_modules/lucide-react/dist/esm/icons/search.js var __iconNode40, Search; var init_search = __esm({ - "node_modules/lucide-react/dist/esm/icons/search.js"() { + "../../node_modules/lucide-react/dist/esm/icons/search.js"() { init_createLucideIcon(); __iconNode40 = [ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }], @@ -9302,10 +9302,10 @@ var init_search = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/settings-2.js +// ../../node_modules/lucide-react/dist/esm/icons/settings-2.js var __iconNode41, Settings2; var init_settings_2 = __esm({ - "node_modules/lucide-react/dist/esm/icons/settings-2.js"() { + "../../node_modules/lucide-react/dist/esm/icons/settings-2.js"() { init_createLucideIcon(); __iconNode41 = [ ["path", { d: "M20 7h-9", key: "3s1dr2" }], @@ -9317,10 +9317,10 @@ var init_settings_2 = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/settings.js +// ../../node_modules/lucide-react/dist/esm/icons/settings.js var __iconNode42, Settings; var init_settings = __esm({ - "node_modules/lucide-react/dist/esm/icons/settings.js"() { + "../../node_modules/lucide-react/dist/esm/icons/settings.js"() { init_createLucideIcon(); __iconNode42 = [ [ @@ -9336,10 +9336,10 @@ var init_settings = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/smile.js +// ../../node_modules/lucide-react/dist/esm/icons/smile.js var __iconNode43, Smile; var init_smile = __esm({ - "node_modules/lucide-react/dist/esm/icons/smile.js"() { + "../../node_modules/lucide-react/dist/esm/icons/smile.js"() { init_createLucideIcon(); __iconNode43 = [ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }], @@ -9351,10 +9351,10 @@ var init_smile = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/sparkles.js +// ../../node_modules/lucide-react/dist/esm/icons/sparkles.js var __iconNode44, Sparkles; var init_sparkles = __esm({ - "node_modules/lucide-react/dist/esm/icons/sparkles.js"() { + "../../node_modules/lucide-react/dist/esm/icons/sparkles.js"() { init_createLucideIcon(); __iconNode44 = [ [ @@ -9373,10 +9373,10 @@ var init_sparkles = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/square-terminal.js +// ../../node_modules/lucide-react/dist/esm/icons/square-terminal.js var __iconNode45, SquareTerminal; var init_square_terminal = __esm({ - "node_modules/lucide-react/dist/esm/icons/square-terminal.js"() { + "../../node_modules/lucide-react/dist/esm/icons/square-terminal.js"() { init_createLucideIcon(); __iconNode45 = [ ["path", { d: "m7 11 2-2-2-2", key: "1lz0vl" }], @@ -9387,10 +9387,10 @@ var init_square_terminal = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/tag.js +// ../../node_modules/lucide-react/dist/esm/icons/tag.js var __iconNode46, Tag; var init_tag = __esm({ - "node_modules/lucide-react/dist/esm/icons/tag.js"() { + "../../node_modules/lucide-react/dist/esm/icons/tag.js"() { init_createLucideIcon(); __iconNode46 = [ [ @@ -9406,10 +9406,10 @@ var init_tag = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/trash-2.js +// ../../node_modules/lucide-react/dist/esm/icons/trash-2.js var __iconNode47, Trash2; var init_trash_2 = __esm({ - "node_modules/lucide-react/dist/esm/icons/trash-2.js"() { + "../../node_modules/lucide-react/dist/esm/icons/trash-2.js"() { init_createLucideIcon(); __iconNode47 = [ ["path", { d: "M3 6h18", key: "d0wm0j" }], @@ -9422,10 +9422,10 @@ var init_trash_2 = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/underline.js +// ../../node_modules/lucide-react/dist/esm/icons/underline.js var __iconNode48, Underline; var init_underline = __esm({ - "node_modules/lucide-react/dist/esm/icons/underline.js"() { + "../../node_modules/lucide-react/dist/esm/icons/underline.js"() { init_createLucideIcon(); __iconNode48 = [ ["path", { d: "M6 4v6a6 6 0 0 0 12 0V4", key: "9kb039" }], @@ -9435,10 +9435,10 @@ var init_underline = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/user.js +// ../../node_modules/lucide-react/dist/esm/icons/user.js var __iconNode49, User; var init_user = __esm({ - "node_modules/lucide-react/dist/esm/icons/user.js"() { + "../../node_modules/lucide-react/dist/esm/icons/user.js"() { init_createLucideIcon(); __iconNode49 = [ ["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }], @@ -9448,10 +9448,10 @@ var init_user = __esm({ } }); -// node_modules/lucide-react/dist/esm/icons/x.js +// ../../node_modules/lucide-react/dist/esm/icons/x.js var __iconNode50, X; var init_x = __esm({ - "node_modules/lucide-react/dist/esm/icons/x.js"() { + "../../node_modules/lucide-react/dist/esm/icons/x.js"() { init_createLucideIcon(); __iconNode50 = [ ["path", { d: "M18 6 6 18", key: "1bl5f8" }], @@ -9461,9 +9461,9 @@ var init_x = __esm({ } }); -// node_modules/lucide-react/dist/esm/lucide-react.js +// ../../node_modules/lucide-react/dist/esm/lucide-react.js var init_lucide_react = __esm({ - "node_modules/lucide-react/dist/esm/lucide-react.js"() { + "../../node_modules/lucide-react/dist/esm/lucide-react.js"() { init_badge_check(); init_chart_pie(); init_circle_alert();