@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- 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
|
+
})
|