@cat-factory/app 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,196 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { ClarityReview, ResolveClarityExceededChoice, ReviewItemStatus } from '~/types/clarity'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Clarity-review state. On the pipeline path the reviewer runs as the first gate
8
+ * step: the run parks while the human answers/dismisses findings about the bug report,
9
+ * then asks to incorporate. Incorporation + the re-review run ASYNCHRONOUSLY in the
10
+ * durable driver — the call returns at once (status `incorporating`) and the user goes
11
+ * back to the board; they are summoned again (a notification) only if the re-review
12
+ * yields findings or hits the cap. The store is patched both from call responses and
13
+ * from live `clarity` stream events (see `upsert`). `available` mirrors the backend's
14
+ * opt-in gate (a 503 hides the UI). Per-workspace; nothing is persisted client-side.
15
+ */
16
+ export const useClarityStore = defineStore('clarity', () => {
17
+ const api = useApi()
18
+ const workspace = useWorkspaceStore()
19
+
20
+ /** null = unknown (not probed), true/false = feature on/off. */
21
+ const available = ref<boolean | null>(null)
22
+ /** The current review per block id (null = fetched, none exists). */
23
+ const reviews = ref<Record<string, ClarityReview | null>>({})
24
+ /** Block ids whose reviewer is currently running (review / re-review). */
25
+ const reviewing = ref<Set<string>>(new Set())
26
+ /** Review ids currently incorporating their answers. */
27
+ const incorporating = ref<Set<string>>(new Set())
28
+ /** Block ids whose current review is being fetched (the initial `load`). */
29
+ const loadingByBlock = ref<Set<string>>(new Set())
30
+ /**
31
+ * In-flight `load()` promises keyed by block id, so concurrent callers for the same
32
+ * block (the inspector's badge watch + the review window opening together) share ONE
33
+ * request instead of each firing its own. Plain Map — internal bookkeeping, not
34
+ * reactive. Cleared once the request settles.
35
+ */
36
+ const inFlight = new Map<string, Promise<void>>()
37
+
38
+ function reviewFor(blockId: string): ClarityReview | null {
39
+ return reviews.value[blockId] ?? null
40
+ }
41
+ /**
42
+ * The async background stage a block's review is in, or null. While the driver folds the
43
+ * answers (`incorporating`) then re-reviews the document (`reviewing`), NO human action is
44
+ * needed — so the board suppresses the "Approval needed" gate and shows this working state
45
+ * instead, with copy that names which of the two stages is running.
46
+ */
47
+ function backgroundStage(blockId: string): 'incorporating' | 'reviewing' | null {
48
+ const status = reviews.value[blockId]?.status
49
+ return status === 'incorporating' || status === 'reviewing' ? status : null
50
+ }
51
+ function isReviewing(blockId: string): boolean {
52
+ return reviewing.value.has(blockId)
53
+ }
54
+ function isLoading(blockId: string): boolean {
55
+ return loadingByBlock.value.has(blockId)
56
+ }
57
+ function isIncorporating(reviewId: string): boolean {
58
+ return incorporating.value.has(reviewId)
59
+ }
60
+
61
+ /** Findings still needing a human (status `open`). */
62
+ function openCount(review: ClarityReview): number {
63
+ return review.items.filter((i) => i.status === 'open').length
64
+ }
65
+ /** Findings the human answered (a reply recorded), which the companion folds in. */
66
+ function answeredCount(review: ClarityReview): number {
67
+ return review.items.filter((i) => i.status === 'answered' || i.status === 'resolved').length
68
+ }
69
+ /** Every finding is settled (answered or dismissed) — none still open. */
70
+ function allSettled(review: ClarityReview): boolean {
71
+ return openCount(review) === 0
72
+ }
73
+ /** Incorporation is possible: all findings settled AND at least one was answered. */
74
+ function canIncorporate(review: ClarityReview): boolean {
75
+ return allSettled(review) && answeredCount(review) > 0
76
+ }
77
+ /** Proceed (skip the companion) is possible: all findings settled but none answered. */
78
+ function canProceed(review: ClarityReview): boolean {
79
+ return allSettled(review) && answeredCount(review) === 0
80
+ }
81
+
82
+ function store(review: ClarityReview) {
83
+ reviews.value = { ...reviews.value, [review.blockId]: review }
84
+ }
85
+
86
+ function withFlag(set: typeof reviewing, key: string, on: boolean) {
87
+ const next = new Set(set.value)
88
+ if (on) next.add(key)
89
+ else next.delete(key)
90
+ set.value = next
91
+ }
92
+
93
+ /** Fetch the current review for a block (probing the feature's availability). */
94
+ async function load(blockId: string) {
95
+ if (!workspace.workspaceId) return
96
+ // Coalesce overlapping loads of the same block onto a single request.
97
+ const pending = inFlight.get(blockId)
98
+ if (pending) return pending
99
+ const promise = (async () => {
100
+ withFlag(loadingByBlock, blockId, true)
101
+ try {
102
+ const review = await api.getClarityReview(workspace.requireId(), blockId)
103
+ available.value = true
104
+ reviews.value = { ...reviews.value, [blockId]: review }
105
+ } catch {
106
+ // 503 (feature off) or any error → hide the UI entry points.
107
+ available.value = false
108
+ } finally {
109
+ withFlag(loadingByBlock, blockId, false)
110
+ inFlight.delete(blockId)
111
+ }
112
+ })()
113
+ inFlight.set(blockId, promise)
114
+ return promise
115
+ }
116
+
117
+ /** Record a human's answer to one item. */
118
+ async function reply(review: ClarityReview, itemId: string, text: string) {
119
+ store(await api.replyClarityItem(workspace.requireId(), review.id, itemId, text))
120
+ }
121
+
122
+ /** Set an item's status (dismiss / reopen). */
123
+ async function setItemStatus(review: ClarityReview, itemId: string, status: ReviewItemStatus) {
124
+ store(await api.setClarityItemStatus(workspace.requireId(), review.id, itemId, status))
125
+ }
126
+
127
+ /**
128
+ * Ask the driver to incorporate the answers ASYNCHRONOUSLY. Optional `feedback` is the "do
129
+ * it differently" direction when redoing a merge. Returns at once with the `incorporating`
130
+ * review (the fold + re-review run in the background); the caller returns the user to the
131
+ * board. A live `clarity` event / a notification reflects the outcome later.
132
+ */
133
+ async function incorporate(review: ClarityReview, feedback?: string) {
134
+ withFlag(incorporating, review.id, true)
135
+ try {
136
+ const updated = await api.incorporateClarity(workspace.requireId(), review.blockId, feedback)
137
+ store(updated)
138
+ return updated
139
+ } finally {
140
+ withFlag(incorporating, review.id, false)
141
+ }
142
+ }
143
+
144
+ /** Re-review the clarified report (one more reviewer pass; may converge/advance). */
145
+ async function reReview(blockId: string): Promise<ClarityReview> {
146
+ withFlag(reviewing, blockId, true)
147
+ try {
148
+ const updated = await api.reReviewClarity(workspace.requireId(), blockId)
149
+ store(updated)
150
+ return updated
151
+ } finally {
152
+ withFlag(reviewing, blockId, false)
153
+ }
154
+ }
155
+
156
+ /** Proceed: settle the clarity review and advance the parked run. */
157
+ async function proceed(blockId: string): Promise<ClarityReview> {
158
+ const updated = await api.proceedClarity(workspace.requireId(), blockId)
159
+ store(updated)
160
+ return updated
161
+ }
162
+
163
+ /** Resolve a capped review: extra-round / proceed / stop-reset. */
164
+ async function resolveExceeded(
165
+ blockId: string,
166
+ choice: ResolveClarityExceededChoice,
167
+ ): Promise<ClarityReview> {
168
+ const updated = await api.resolveClarityExceeded(workspace.requireId(), blockId, choice)
169
+ store(updated)
170
+ return updated
171
+ }
172
+
173
+ return {
174
+ available,
175
+ reviews,
176
+ reviewFor,
177
+ backgroundStage,
178
+ isReviewing,
179
+ isLoading,
180
+ isIncorporating,
181
+ openCount,
182
+ answeredCount,
183
+ allSettled,
184
+ canIncorporate,
185
+ canProceed,
186
+ load,
187
+ reply,
188
+ setItemStatus,
189
+ incorporate,
190
+ reReview,
191
+ proceed,
192
+ resolveExceeded,
193
+ // Patch the cache from a live `clarity` stream event.
194
+ upsert: store,
195
+ }
196
+ })
@@ -0,0 +1,60 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import type { ConsensusSession } from '~/types/consensus'
4
+ import { useWorkspaceStore } from '~/stores/workspace'
5
+
6
+ /**
7
+ * Consensus session state. A consensus-enabled step runs a multi-model process (panel /
8
+ * debate / ranked voting); its transcript is pushed live via the `consensus` stream event
9
+ * and patched here by `upsert`. The dedicated window loads the latest session for a block
10
+ * on open (`load`) so a reload shows a completed session. Per-workspace; nothing persisted
11
+ * client-side.
12
+ */
13
+ export const useConsensusStore = defineStore('consensus', () => {
14
+ const api = useApi()
15
+ const workspace = useWorkspaceStore()
16
+
17
+ /** The latest session per block id (null = fetched, none exists / consensus off). */
18
+ const sessions = ref<Record<string, ConsensusSession | null>>({})
19
+ /** Block ids whose session is currently being fetched. */
20
+ const loading = ref<Set<string>>(new Set())
21
+
22
+ function sessionFor(blockId: string): ConsensusSession | null {
23
+ return sessions.value[blockId] ?? null
24
+ }
25
+
26
+ function isLoading(blockId: string): boolean {
27
+ return loading.value.has(blockId)
28
+ }
29
+
30
+ function store(session: ConsensusSession) {
31
+ sessions.value = { ...sessions.value, [session.blockId]: session }
32
+ }
33
+
34
+ /** Patch the cache from a live `consensus` stream event (newest wins per block). */
35
+ function upsert(session: ConsensusSession) {
36
+ const existing = sessions.value[session.blockId]
37
+ // Keep the freshest by updatedAt so out-of-order pushes don't regress the transcript.
38
+ if (existing && existing.id === session.id && existing.updatedAt > session.updatedAt) return
39
+ store(session)
40
+ }
41
+
42
+ /** Load the latest session for a block (window open / reload). Best-effort. */
43
+ async function load(blockId: string): Promise<void> {
44
+ const wsId = workspace.workspaceId
45
+ if (!wsId) return
46
+ loading.value = new Set(loading.value).add(blockId)
47
+ try {
48
+ const { session } = await api.getConsensusSession(wsId, blockId)
49
+ sessions.value = { ...sessions.value, [blockId]: session }
50
+ } catch {
51
+ // Consensus off / no session — leave the cache as-is; the window shows its empty state.
52
+ } finally {
53
+ const next = new Set(loading.value)
54
+ next.delete(blockId)
55
+ loading.value = next
56
+ }
57
+ }
58
+
59
+ return { sessions, sessionFor, isLoading, load, upsert }
60
+ })
@@ -0,0 +1,176 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, ref } from 'vue'
3
+ import type {
4
+ DocumentBoardPlan,
5
+ DocumentConnection,
6
+ DocumentSearchResult,
7
+ DocumentSourceDescriptor,
8
+ DocumentSourceKind,
9
+ SourceDocument,
10
+ } from '~/types/domain'
11
+ import { useWorkspaceStore } from '~/stores/workspace'
12
+
13
+ /**
14
+ * Document-source integration state: the sources the backend offers (and their
15
+ * connect metadata), the workspace's per-source connections, and the pages it
16
+ * has imported — plus the actions that connect/import/plan/spawn/link against the
17
+ * backend. `available` mirrors the backend's opt-in gate: a 503 from the source
18
+ * probe means the integration is off, and the UI hides its entry points (just as
19
+ * `auth.required` gates the login UI). The abstraction is source-agnostic; every
20
+ * action is keyed by a `DocumentSourceKind`. Per-workspace, like the board
21
+ * itself; nothing is persisted client-side.
22
+ */
23
+ export const useDocumentsStore = defineStore('documents', () => {
24
+ const api = useApi()
25
+ const workspace = useWorkspaceStore()
26
+
27
+ /** null = unknown (not probed yet), true/false = integration on/off. */
28
+ const available = ref<boolean | null>(null)
29
+ /** The configured sources and their connect/import descriptors. */
30
+ const sources = ref<DocumentSourceDescriptor[]>([])
31
+ /** Live connections, one per connected source. */
32
+ const connections = ref<DocumentConnection[]>([])
33
+ const documents = ref<SourceDocument[]>([])
34
+ const loading = ref(false)
35
+
36
+ /** Sources the workspace currently has a live connection to. */
37
+ const connectedSources = computed(() =>
38
+ sources.value.filter((s) => connections.value.some((c) => c.source === s.source)),
39
+ )
40
+ const anyConnected = computed(() => connections.value.length > 0)
41
+
42
+ function descriptorFor(source: DocumentSourceKind): DocumentSourceDescriptor | undefined {
43
+ return sources.value.find((s) => s.source === source)
44
+ }
45
+
46
+ function connectionFor(source: DocumentSourceKind): DocumentConnection | undefined {
47
+ return connections.value.find((c) => c.source === source)
48
+ }
49
+
50
+ function isConnected(source: DocumentSourceKind): boolean {
51
+ return connectionFor(source) !== undefined
52
+ }
53
+
54
+ /** Imported documents currently attached to a given block. */
55
+ function docsForBlock(blockId: string): SourceDocument[] {
56
+ return documents.value.filter((d) => d.linkedBlockId === blockId)
57
+ }
58
+
59
+ /** Merge a document returned by the backend into the local cache. */
60
+ function upsertDoc(doc: SourceDocument) {
61
+ const i = documents.value.findIndex(
62
+ (d) => d.source === doc.source && d.externalId === doc.externalId,
63
+ )
64
+ if (i >= 0) documents.value[i] = doc
65
+ else documents.value.unshift(doc)
66
+ }
67
+
68
+ function upsertConnection(conn: DocumentConnection) {
69
+ const i = connections.value.findIndex((c) => c.source === conn.source)
70
+ if (i >= 0) connections.value[i] = conn
71
+ else connections.value.push(conn)
72
+ }
73
+
74
+ /** Probe the integration: resolves `available`, the sources and connections. */
75
+ async function probe() {
76
+ if (!workspace.workspaceId) return
77
+ try {
78
+ const [{ sources: srcs }, { connections: conns }] = await Promise.all([
79
+ api.listDocumentSources(workspace.requireId()),
80
+ api.listDocumentConnections(workspace.requireId()),
81
+ ])
82
+ available.value = true
83
+ sources.value = srcs
84
+ connections.value = conns
85
+ } catch {
86
+ // 503 (integration disabled) or any error → hide the UI entry points.
87
+ available.value = false
88
+ sources.value = []
89
+ connections.value = []
90
+ }
91
+ }
92
+
93
+ /** Connect the workspace to a source with its credential bag. */
94
+ async function connect(source: DocumentSourceKind, credentials: Record<string, string>) {
95
+ const conn = await api.connectDocumentSource(workspace.requireId(), source, credentials)
96
+ upsertConnection(conn)
97
+ available.value = true
98
+ }
99
+
100
+ /** Disconnect the workspace from a source. */
101
+ async function disconnect(source: DocumentSourceKind) {
102
+ await api.disconnectDocumentSource(workspace.requireId(), source)
103
+ connections.value = connections.value.filter((c) => c.source !== source)
104
+ }
105
+
106
+ /** Load the imported documents for the workspace (across sources). */
107
+ async function loadDocuments() {
108
+ documents.value = await api.listDocuments(workspace.requireId())
109
+ }
110
+
111
+ /** Import (fetch + persist) a page by id or URL from a source. */
112
+ async function importDocument(source: DocumentSourceKind, ref: string): Promise<SourceDocument> {
113
+ loading.value = true
114
+ try {
115
+ const doc = await api.importDocument(workspace.requireId(), source, { ref })
116
+ upsertDoc(doc)
117
+ return doc
118
+ } finally {
119
+ loading.value = false
120
+ }
121
+ }
122
+
123
+ /** Search a connected source's catalogue by free text (title/content). */
124
+ async function search(
125
+ source: DocumentSourceKind,
126
+ query: string,
127
+ ): Promise<DocumentSearchResult[]> {
128
+ const { results } = await api.searchDocumentSource(workspace.requireId(), source, query)
129
+ return results
130
+ }
131
+
132
+ /** Preview the board structure a page would expand into (no writes). */
133
+ function plan(source: DocumentSourceKind, externalId: string): Promise<DocumentBoardPlan> {
134
+ return api.planDocument(workspace.requireId(), source, externalId)
135
+ }
136
+
137
+ /** Apply a page's structure to the board, then refresh the board snapshot. */
138
+ async function spawn(source: DocumentSourceKind, externalId: string, frameId?: string) {
139
+ const { result } = await api.spawnDocument(workspace.requireId(), source, {
140
+ externalId,
141
+ frameId,
142
+ })
143
+ await workspace.refresh()
144
+ return result
145
+ }
146
+
147
+ /** Attach an imported page to a block as agent context. */
148
+ async function linkToBlock(blockId: string, source: DocumentSourceKind, externalId: string) {
149
+ const doc = await api.linkDocument(workspace.requireId(), { source, externalId, blockId })
150
+ upsertDoc(doc)
151
+ return doc
152
+ }
153
+
154
+ return {
155
+ available,
156
+ sources,
157
+ connections,
158
+ documents,
159
+ loading,
160
+ connectedSources,
161
+ anyConnected,
162
+ descriptorFor,
163
+ connectionFor,
164
+ isConnected,
165
+ docsForBlock,
166
+ probe,
167
+ connect,
168
+ disconnect,
169
+ loadDocuments,
170
+ importDocument,
171
+ search,
172
+ plan,
173
+ spawn,
174
+ linkToBlock,
175
+ }
176
+ })