@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,210 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Consensus session window — the dedicated, read-only surface for any step that ran the
|
|
3
|
+
// multi-model consensus mechanism (specialist panel / debate / ranked voting). Opened via
|
|
4
|
+
// the universal result-view host (routed in `ui.dispatchStepView` when a step's
|
|
5
|
+
// `consensus.enabled`). Visualizes the process for observability: the participants + their
|
|
6
|
+
// models, the round-by-round contributions (anonymized as the models saw each other),
|
|
7
|
+
// per-candidate votes/scores, and the synthesized result + confidence/dissent. Updates live
|
|
8
|
+
// as `consensus` stream events arrive.
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
import type { ConsensusContribution, ConsensusSession } from '~/types/consensus'
|
|
11
|
+
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const consensus = useConsensusStore()
|
|
14
|
+
|
|
15
|
+
const { open, blockId, close } = useResultView('consensus-session', {
|
|
16
|
+
onOpen: (id) => {
|
|
17
|
+
void consensus.load(id)
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
22
|
+
const session = computed<ConsensusSession | null>(() =>
|
|
23
|
+
blockId.value ? consensus.sessionFor(blockId.value) : null,
|
|
24
|
+
)
|
|
25
|
+
const loading = computed(() => (blockId.value ? consensus.isLoading(blockId.value) : false))
|
|
26
|
+
|
|
27
|
+
const STRATEGY_LABEL: Record<string, string> = {
|
|
28
|
+
'specialist-panel': 'Specialist panel',
|
|
29
|
+
debate: 'Debate',
|
|
30
|
+
'ranked-voting': 'Ranked voting',
|
|
31
|
+
}
|
|
32
|
+
const ROUND_LABEL: Record<string, string> = {
|
|
33
|
+
draft: 'Independent drafts',
|
|
34
|
+
critique: 'Critique & revision',
|
|
35
|
+
score: 'Scoring',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STATUS_META: Record<string, { label: string; class: string }> = {
|
|
39
|
+
running: { label: 'Running', class: 'bg-sky-500/15 text-sky-300' },
|
|
40
|
+
synthesizing: { label: 'Synthesizing', class: 'bg-indigo-500/15 text-indigo-300' },
|
|
41
|
+
done: { label: 'Done', class: 'bg-emerald-500/15 text-emerald-300' },
|
|
42
|
+
failed: { label: 'Failed', class: 'bg-rose-500/15 text-rose-300' },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Anonymous label (Expert A/B/…) for a participant, matching the backend's ordering. */
|
|
46
|
+
function anonLabel(participantId: string): string {
|
|
47
|
+
const idx = session.value?.participants.findIndex((p) => p.id === participantId) ?? -1
|
|
48
|
+
return `Expert ${String.fromCharCode(65 + (idx < 0 ? 0 : idx % 26))}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function roleFor(participantId: string): string {
|
|
52
|
+
return session.value?.participants.find((p) => p.id === participantId)?.role ?? 'Participant'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pct(n: number | null | undefined): string {
|
|
56
|
+
return n == null ? '—' : `${Math.round(n * 100)}%`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function topScore(c: ConsensusContribution): { label: string; value: number } | null {
|
|
60
|
+
if (!c.scores?.length) return null
|
|
61
|
+
const best = [...c.scores].sort((a, b) => b.value - a.value)[0]!
|
|
62
|
+
return { label: best.dimension, value: best.value }
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<Teleport to="body">
|
|
68
|
+
<div
|
|
69
|
+
v-if="open"
|
|
70
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/70 p-4 backdrop-blur-sm"
|
|
71
|
+
@click.self="close"
|
|
72
|
+
>
|
|
73
|
+
<div
|
|
74
|
+
class="flex h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
|
|
75
|
+
>
|
|
76
|
+
<!-- header -->
|
|
77
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
|
|
78
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-amber-500/15">
|
|
79
|
+
<UIcon name="i-lucide-users-round" class="h-5 w-5 text-amber-300" />
|
|
80
|
+
</div>
|
|
81
|
+
<div class="min-w-0 flex-1">
|
|
82
|
+
<h2 class="truncate text-sm font-semibold text-slate-100">
|
|
83
|
+
Consensus ·
|
|
84
|
+
{{ session ? (STRATEGY_LABEL[session.strategy] ?? session.strategy) : '' }}
|
|
85
|
+
<span v-if="block" class="font-normal text-slate-400">— {{ block.title }}</span>
|
|
86
|
+
</h2>
|
|
87
|
+
<p v-if="session" class="text-xs text-slate-500">
|
|
88
|
+
{{ session.agentKind }} · {{ session.participants.length }} participants
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<span
|
|
92
|
+
v-if="session"
|
|
93
|
+
class="rounded-full px-2.5 py-1 text-xs font-medium"
|
|
94
|
+
:class="STATUS_META[session.status]?.class ?? 'bg-slate-700 text-slate-300'"
|
|
95
|
+
>
|
|
96
|
+
{{ STATUS_META[session.status]?.label ?? session.status }}
|
|
97
|
+
</span>
|
|
98
|
+
<button
|
|
99
|
+
class="rounded-lg p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
100
|
+
@click="close"
|
|
101
|
+
>
|
|
102
|
+
<UIcon name="i-lucide-x" class="h-5 w-5" />
|
|
103
|
+
</button>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<div class="flex-1 overflow-y-auto px-6 py-5">
|
|
107
|
+
<div v-if="loading && !session" class="py-16 text-center text-sm text-slate-500">
|
|
108
|
+
Loading consensus session…
|
|
109
|
+
</div>
|
|
110
|
+
<div v-else-if="!session" class="py-16 text-center text-sm text-slate-500">
|
|
111
|
+
No consensus session has run for this step yet.
|
|
112
|
+
</div>
|
|
113
|
+
<template v-else>
|
|
114
|
+
<!-- failure -->
|
|
115
|
+
<div
|
|
116
|
+
v-if="session.status === 'failed'"
|
|
117
|
+
class="mb-5 rounded-lg border border-rose-800/60 bg-rose-950/40 px-4 py-3 text-sm text-rose-200"
|
|
118
|
+
>
|
|
119
|
+
Consensus failed: {{ session.error ?? 'unknown error' }}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- synthesized result -->
|
|
123
|
+
<section v-if="session.synthesis" class="mb-6">
|
|
124
|
+
<div class="mb-2 flex items-center gap-2">
|
|
125
|
+
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
126
|
+
Synthesized result
|
|
127
|
+
</h3>
|
|
128
|
+
<span
|
|
129
|
+
v-if="session.confidence != null"
|
|
130
|
+
class="rounded bg-emerald-500/15 px-1.5 py-0.5 text-xs text-emerald-300"
|
|
131
|
+
>confidence {{ pct(session.confidence) }}</span
|
|
132
|
+
>
|
|
133
|
+
</div>
|
|
134
|
+
<pre
|
|
135
|
+
class="whitespace-pre-wrap rounded-lg border border-slate-800 bg-slate-950/60 px-4 py-3 text-sm text-slate-200"
|
|
136
|
+
>{{ session.synthesis }}</pre
|
|
137
|
+
>
|
|
138
|
+
<ul v-if="session.dissent?.length" class="mt-2 space-y-1">
|
|
139
|
+
<li
|
|
140
|
+
v-for="(d, i) in session.dissent"
|
|
141
|
+
:key="i"
|
|
142
|
+
class="flex items-start gap-2 text-xs text-amber-300/90"
|
|
143
|
+
>
|
|
144
|
+
<UIcon name="i-lucide-triangle-alert" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
145
|
+
<span>{{ d }}</span>
|
|
146
|
+
</li>
|
|
147
|
+
</ul>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<!-- participants -->
|
|
151
|
+
<section class="mb-6">
|
|
152
|
+
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
153
|
+
Panel
|
|
154
|
+
</h3>
|
|
155
|
+
<div class="flex flex-wrap gap-2">
|
|
156
|
+
<div
|
|
157
|
+
v-for="(p, i) in session.participants"
|
|
158
|
+
:key="p.id"
|
|
159
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-1.5 text-xs"
|
|
160
|
+
>
|
|
161
|
+
<span class="font-medium text-slate-200"
|
|
162
|
+
>Expert {{ String.fromCharCode(65 + i) }}</span
|
|
163
|
+
>
|
|
164
|
+
<span class="text-slate-400"> · {{ p.role }}</span>
|
|
165
|
+
<span v-if="p.modelId" class="ml-1 text-slate-500">({{ p.modelId }})</span>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</section>
|
|
169
|
+
|
|
170
|
+
<!-- rounds -->
|
|
171
|
+
<section v-for="round in session.rounds" :key="round.index" class="mb-5">
|
|
172
|
+
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
173
|
+
Round {{ round.index + 1 }} ·
|
|
174
|
+
{{ round.kind ? (ROUND_LABEL[round.kind] ?? round.kind) : 'Contributions' }}
|
|
175
|
+
</h3>
|
|
176
|
+
<div class="space-y-3">
|
|
177
|
+
<div
|
|
178
|
+
v-for="c in round.contributions"
|
|
179
|
+
:key="c.participantId"
|
|
180
|
+
class="rounded-lg border border-slate-800 bg-slate-950/40 px-4 py-3"
|
|
181
|
+
>
|
|
182
|
+
<div class="mb-1 flex items-center gap-2">
|
|
183
|
+
<span class="text-xs font-semibold text-slate-200">{{
|
|
184
|
+
anonLabel(c.participantId)
|
|
185
|
+
}}</span>
|
|
186
|
+
<span class="text-xs text-slate-500">{{ roleFor(c.participantId) }}</span>
|
|
187
|
+
<span
|
|
188
|
+
v-if="topScore(c)"
|
|
189
|
+
class="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
|
|
190
|
+
>top {{ topScore(c)!.label }} {{ pct(topScore(c)!.value) }}</span
|
|
191
|
+
>
|
|
192
|
+
</div>
|
|
193
|
+
<pre class="whitespace-pre-wrap text-sm text-slate-300">{{ c.text }}</pre>
|
|
194
|
+
<div v-if="c.scores?.length" class="mt-2 flex flex-wrap gap-1.5">
|
|
195
|
+
<span
|
|
196
|
+
v-for="s in c.scores"
|
|
197
|
+
:key="s.dimension"
|
|
198
|
+
class="rounded bg-slate-800/80 px-1.5 py-0.5 text-xs text-slate-400"
|
|
199
|
+
>{{ s.dimension }}: {{ pct(s.value) }}</span
|
|
200
|
+
>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
</template>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</Teleport>
|
|
210
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocumentSourceKind } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
// Import pages from a connected document source and pick one to expand into
|
|
5
|
+
// board structure. A source selector lets the user choose which connected source
|
|
6
|
+
// to import from (Confluence, Notion, …). Carries an optional target frame from
|
|
7
|
+
// the inspector so "Preview & spawn" lands the structure inside that frame.
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const documents = useDocumentsStore()
|
|
10
|
+
const board = useBoardStore()
|
|
11
|
+
const toast = useToast()
|
|
12
|
+
|
|
13
|
+
const open = computed({
|
|
14
|
+
get: () => ui.documentImport !== null,
|
|
15
|
+
set: (v: boolean) => {
|
|
16
|
+
if (!v) ui.closeDocumentImport()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const targetFrameId = computed(() => ui.documentImport?.targetFrameId ?? null)
|
|
21
|
+
const targetFrameTitle = computed(() =>
|
|
22
|
+
targetFrameId.value ? board.getBlock(targetFrameId.value)?.title : null,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/** Which connected source we're importing from (defaults to the first). */
|
|
26
|
+
const source = ref<DocumentSourceKind | undefined>(undefined)
|
|
27
|
+
const ref_ = ref('')
|
|
28
|
+
const importing = ref(false)
|
|
29
|
+
|
|
30
|
+
const sourceItems = computed(() =>
|
|
31
|
+
documents.connectedSources.map((s) => ({ label: s.label, value: s.source })),
|
|
32
|
+
)
|
|
33
|
+
const descriptor = computed(() =>
|
|
34
|
+
source.value ? documents.descriptorFor(source.value) : undefined,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/** Documents imported from the currently selected source. */
|
|
38
|
+
const sourceDocs = computed(() =>
|
|
39
|
+
source.value ? documents.documents.filter((d) => d.source === source.value) : [],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
watch(open, (isOpen) => {
|
|
43
|
+
if (isOpen) {
|
|
44
|
+
ref_.value = ''
|
|
45
|
+
source.value = ui.documentImport?.source ?? documents.connectedSources[0]?.source ?? undefined
|
|
46
|
+
documents.loadDocuments().catch(() => {})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
async function doImport() {
|
|
51
|
+
const value = ref_.value.trim()
|
|
52
|
+
if (!value || !source.value) return
|
|
53
|
+
importing.value = true
|
|
54
|
+
try {
|
|
55
|
+
const doc = await documents.importDocument(source.value, value)
|
|
56
|
+
ref_.value = ''
|
|
57
|
+
toast.add({ title: `Imported "${doc.title}"`, icon: 'i-lucide-file-down' })
|
|
58
|
+
} catch (e) {
|
|
59
|
+
toast.add({
|
|
60
|
+
title: 'Import failed',
|
|
61
|
+
description: e instanceof Error ? e.message : String(e),
|
|
62
|
+
icon: 'i-lucide-triangle-alert',
|
|
63
|
+
color: 'error',
|
|
64
|
+
})
|
|
65
|
+
} finally {
|
|
66
|
+
importing.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function preview(externalId: string) {
|
|
71
|
+
if (source.value) ui.openSpawnPreview(source.value, externalId, targetFrameId.value)
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<UModal v-model:open="open" title="Import from a document source">
|
|
77
|
+
<template #body>
|
|
78
|
+
<div v-if="!documents.anyConnected" class="space-y-3 text-center">
|
|
79
|
+
<UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
|
|
80
|
+
<p class="text-sm text-slate-400">Connect a document source first.</p>
|
|
81
|
+
<div class="flex justify-center gap-2">
|
|
82
|
+
<UButton
|
|
83
|
+
v-for="s in documents.sources"
|
|
84
|
+
:key="s.source"
|
|
85
|
+
color="primary"
|
|
86
|
+
variant="soft"
|
|
87
|
+
:icon="s.icon"
|
|
88
|
+
@click="ui.openDocumentConnect(s.source)"
|
|
89
|
+
>
|
|
90
|
+
Connect {{ s.label }}
|
|
91
|
+
</UButton>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div v-else class="space-y-4">
|
|
96
|
+
<p v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
97
|
+
Spawning into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
98
|
+
</p>
|
|
99
|
+
|
|
100
|
+
<UFormField v-if="sourceItems.length > 1" label="Source">
|
|
101
|
+
<USelect v-model="source" :items="sourceItems" class="w-full" />
|
|
102
|
+
</UFormField>
|
|
103
|
+
|
|
104
|
+
<div class="flex items-end gap-2">
|
|
105
|
+
<UFormField :label="descriptor?.refLabel ?? 'Page URL or ID'" class="flex-1">
|
|
106
|
+
<UInput
|
|
107
|
+
v-model="ref_"
|
|
108
|
+
:placeholder="descriptor?.refPlaceholder"
|
|
109
|
+
class="w-full"
|
|
110
|
+
@keydown.enter="doImport"
|
|
111
|
+
/>
|
|
112
|
+
</UFormField>
|
|
113
|
+
<UButton
|
|
114
|
+
color="primary"
|
|
115
|
+
icon="i-lucide-file-down"
|
|
116
|
+
:loading="importing"
|
|
117
|
+
:disabled="!ref_.trim()"
|
|
118
|
+
@click="doImport"
|
|
119
|
+
>
|
|
120
|
+
Import
|
|
121
|
+
</UButton>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div v-if="sourceDocs.length" class="space-y-2">
|
|
125
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
126
|
+
Imported documents
|
|
127
|
+
</h3>
|
|
128
|
+
<div
|
|
129
|
+
v-for="doc in sourceDocs"
|
|
130
|
+
:key="`${doc.source}:${doc.externalId}`"
|
|
131
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
132
|
+
>
|
|
133
|
+
<div class="flex items-start justify-between gap-2">
|
|
134
|
+
<div class="min-w-0">
|
|
135
|
+
<a
|
|
136
|
+
:href="doc.url"
|
|
137
|
+
target="_blank"
|
|
138
|
+
rel="noopener"
|
|
139
|
+
class="truncate text-sm font-medium text-white hover:underline"
|
|
140
|
+
>
|
|
141
|
+
{{ doc.title }}
|
|
142
|
+
</a>
|
|
143
|
+
<p class="mt-0.5 line-clamp-2 text-xs text-slate-500">{{ doc.excerpt }}</p>
|
|
144
|
+
</div>
|
|
145
|
+
<UButton
|
|
146
|
+
color="primary"
|
|
147
|
+
variant="soft"
|
|
148
|
+
size="xs"
|
|
149
|
+
icon="i-lucide-wand-sparkles"
|
|
150
|
+
@click="preview(doc.externalId)"
|
|
151
|
+
>
|
|
152
|
+
Preview & spawn
|
|
153
|
+
</UButton>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<p v-else class="text-center text-xs text-slate-500">No documents imported yet.</p>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</UModal>
|
|
161
|
+
</template>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Connect (or disconnect) the workspace to a document source. The form is
|
|
3
|
+
// rendered generically from the source's descriptor (credential fields), so the
|
|
4
|
+
// same modal serves Confluence, Notion and any future source. Secret credentials
|
|
5
|
+
// are write-only — the backend never returns them, so on reload we show
|
|
6
|
+
// "Connected" with empty fields.
|
|
7
|
+
const ui = useUiStore()
|
|
8
|
+
const documents = useDocumentsStore()
|
|
9
|
+
const toast = useToast()
|
|
10
|
+
|
|
11
|
+
const source = computed(() => ui.documentConnect?.source ?? null)
|
|
12
|
+
const descriptor = computed(() =>
|
|
13
|
+
source.value ? documents.descriptorFor(source.value) : undefined,
|
|
14
|
+
)
|
|
15
|
+
const connection = computed(() =>
|
|
16
|
+
source.value ? documents.connectionFor(source.value) : undefined,
|
|
17
|
+
)
|
|
18
|
+
const connected = computed(() => connection.value !== undefined)
|
|
19
|
+
|
|
20
|
+
const open = computed({
|
|
21
|
+
get: () => ui.documentConnect !== null,
|
|
22
|
+
set: (v: boolean) => {
|
|
23
|
+
if (!v) ui.closeDocumentConnect()
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/** One value per credential field, reset whenever the modal (re)opens. */
|
|
28
|
+
const values = ref<Record<string, string>>({})
|
|
29
|
+
const saving = ref(false)
|
|
30
|
+
|
|
31
|
+
watch(open, (isOpen) => {
|
|
32
|
+
if (isOpen) values.value = {}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const canSubmit = computed(() => {
|
|
36
|
+
const fields = descriptor.value?.credentialFields ?? []
|
|
37
|
+
return fields.length > 0 && fields.every((f) => (values.value[f.key] ?? '').trim())
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async function submit() {
|
|
41
|
+
if (!canSubmit.value || !source.value) return
|
|
42
|
+
const credentials: Record<string, string> = {}
|
|
43
|
+
for (const f of descriptor.value!.credentialFields) {
|
|
44
|
+
credentials[f.key] = values.value[f.key]!.trim()
|
|
45
|
+
}
|
|
46
|
+
saving.value = true
|
|
47
|
+
try {
|
|
48
|
+
await documents.connect(source.value, credentials)
|
|
49
|
+
toast.add({
|
|
50
|
+
title: `${descriptor.value!.label} connected`,
|
|
51
|
+
icon: 'i-lucide-check',
|
|
52
|
+
color: 'success',
|
|
53
|
+
})
|
|
54
|
+
ui.closeDocumentConnect()
|
|
55
|
+
} catch (e) {
|
|
56
|
+
toast.add({
|
|
57
|
+
title: 'Could not connect',
|
|
58
|
+
description: e instanceof Error ? e.message : String(e),
|
|
59
|
+
icon: 'i-lucide-triangle-alert',
|
|
60
|
+
color: 'error',
|
|
61
|
+
})
|
|
62
|
+
} finally {
|
|
63
|
+
saving.value = false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function disconnect() {
|
|
68
|
+
if (!source.value) return
|
|
69
|
+
await documents.disconnect(source.value)
|
|
70
|
+
toast.add({
|
|
71
|
+
title: `${descriptor.value?.label ?? 'Source'} disconnected`,
|
|
72
|
+
icon: 'i-lucide-unplug',
|
|
73
|
+
})
|
|
74
|
+
ui.closeDocumentConnect()
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<UModal v-model:open="open" :title="descriptor?.label ?? 'Connect source'">
|
|
80
|
+
<template #body>
|
|
81
|
+
<div v-if="descriptor" class="space-y-4">
|
|
82
|
+
<p class="text-sm text-slate-400">
|
|
83
|
+
Connect {{ descriptor.label }} to import requirements, RFCs and PRDs, then spawn board
|
|
84
|
+
structure or attach them to tasks as agent context.
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<div class="space-y-3">
|
|
88
|
+
<UFormField
|
|
89
|
+
v-for="field in descriptor.credentialFields"
|
|
90
|
+
:key="field.key"
|
|
91
|
+
:label="field.label"
|
|
92
|
+
:help="field.help"
|
|
93
|
+
>
|
|
94
|
+
<UInput
|
|
95
|
+
v-model="values[field.key]"
|
|
96
|
+
:type="field.secret ? 'password' : 'text'"
|
|
97
|
+
:placeholder="field.placeholder"
|
|
98
|
+
class="w-full"
|
|
99
|
+
/>
|
|
100
|
+
</UFormField>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="flex items-center justify-between gap-2 pt-1">
|
|
104
|
+
<UButton
|
|
105
|
+
v-if="connected"
|
|
106
|
+
color="error"
|
|
107
|
+
variant="ghost"
|
|
108
|
+
icon="i-lucide-unplug"
|
|
109
|
+
@click="disconnect"
|
|
110
|
+
>
|
|
111
|
+
Disconnect
|
|
112
|
+
</UButton>
|
|
113
|
+
<div v-else />
|
|
114
|
+
<UButton
|
|
115
|
+
color="primary"
|
|
116
|
+
icon="i-lucide-plug"
|
|
117
|
+
:loading="saving"
|
|
118
|
+
:disabled="!canSubmit"
|
|
119
|
+
@click="submit"
|
|
120
|
+
>
|
|
121
|
+
{{ connected ? 'Update connection' : 'Connect' }}
|
|
122
|
+
</UButton>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</template>
|
|
126
|
+
</UModal>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DocumentBoardPlan } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
// Preview the structure an imported document expands into, then spawn it. The
|
|
5
|
+
// plan is fetched fresh on open; a badge makes clear whether an LLM or the
|
|
6
|
+
// deterministic heading parser produced it.
|
|
7
|
+
const ui = useUiStore()
|
|
8
|
+
const documents = useDocumentsStore()
|
|
9
|
+
const board = useBoardStore()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
const open = computed({
|
|
13
|
+
get: () => ui.spawnPreview !== null,
|
|
14
|
+
set: (v: boolean) => {
|
|
15
|
+
if (!v) ui.closeSpawnPreview()
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const targetFrameId = computed(() => ui.spawnPreview?.targetFrameId ?? null)
|
|
20
|
+
const targetFrameTitle = computed(() =>
|
|
21
|
+
targetFrameId.value ? board.getBlock(targetFrameId.value)?.title : null,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const plan = ref<DocumentBoardPlan | null>(null)
|
|
25
|
+
const loadingPlan = ref(false)
|
|
26
|
+
const spawning = ref(false)
|
|
27
|
+
|
|
28
|
+
watch(
|
|
29
|
+
() => ui.spawnPreview?.externalId,
|
|
30
|
+
async (externalId) => {
|
|
31
|
+
plan.value = null
|
|
32
|
+
const preview = ui.spawnPreview
|
|
33
|
+
if (!externalId || !preview) return
|
|
34
|
+
loadingPlan.value = true
|
|
35
|
+
try {
|
|
36
|
+
plan.value = await documents.plan(preview.source, externalId)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
toast.add({
|
|
39
|
+
title: 'Could not build a plan',
|
|
40
|
+
description: e instanceof Error ? e.message : String(e),
|
|
41
|
+
icon: 'i-lucide-triangle-alert',
|
|
42
|
+
color: 'error',
|
|
43
|
+
})
|
|
44
|
+
} finally {
|
|
45
|
+
loadingPlan.value = false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ immediate: true },
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async function spawn() {
|
|
52
|
+
const preview = ui.spawnPreview
|
|
53
|
+
if (!preview) return
|
|
54
|
+
spawning.value = true
|
|
55
|
+
try {
|
|
56
|
+
const result = await documents.spawn(
|
|
57
|
+
preview.source,
|
|
58
|
+
preview.externalId,
|
|
59
|
+
targetFrameId.value ?? undefined,
|
|
60
|
+
)
|
|
61
|
+
toast.add({
|
|
62
|
+
title: 'Structure spawned',
|
|
63
|
+
description: `${result.frames} frames · ${result.modules} modules · ${result.tasks} tasks`,
|
|
64
|
+
icon: 'i-lucide-check',
|
|
65
|
+
color: 'success',
|
|
66
|
+
})
|
|
67
|
+
ui.closeSpawnPreview()
|
|
68
|
+
ui.closeDocumentImport()
|
|
69
|
+
} catch (e) {
|
|
70
|
+
toast.add({
|
|
71
|
+
title: 'Spawn failed',
|
|
72
|
+
description: e instanceof Error ? e.message : String(e),
|
|
73
|
+
icon: 'i-lucide-triangle-alert',
|
|
74
|
+
color: 'error',
|
|
75
|
+
})
|
|
76
|
+
} finally {
|
|
77
|
+
spawning.value = false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<UModal v-model:open="open" title="Preview structure">
|
|
84
|
+
<template #body>
|
|
85
|
+
<div class="space-y-4">
|
|
86
|
+
<div v-if="plan" class="flex items-center justify-between gap-2">
|
|
87
|
+
<UBadge
|
|
88
|
+
:color="plan.planner === 'llm' ? 'primary' : 'neutral'"
|
|
89
|
+
variant="subtle"
|
|
90
|
+
size="sm"
|
|
91
|
+
>
|
|
92
|
+
{{ plan.planner === 'llm' ? 'AI-generated' : 'From headings' }}
|
|
93
|
+
</UBadge>
|
|
94
|
+
<span v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
95
|
+
into <span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
96
|
+
</span>
|
|
97
|
+
<span v-else class="text-xs text-slate-400">as new top-level frames</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-slate-400">
|
|
101
|
+
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" /> Building plan…
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pr-1">
|
|
105
|
+
<div
|
|
106
|
+
v-for="(frame, fi) in plan.frames"
|
|
107
|
+
:key="fi"
|
|
108
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
|
|
109
|
+
>
|
|
110
|
+
<div class="flex items-center gap-2">
|
|
111
|
+
<UIcon name="i-lucide-box" class="h-4 w-4 text-indigo-400" />
|
|
112
|
+
<span class="text-sm font-semibold text-white">{{ frame.title }}</span>
|
|
113
|
+
<UBadge variant="subtle" size="sm" color="neutral">{{ frame.type }}</UBadge>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<ul v-if="frame.tasks.length" class="mt-2 space-y-1 pl-6">
|
|
117
|
+
<li
|
|
118
|
+
v-for="(task, ti) in frame.tasks"
|
|
119
|
+
:key="`t-${ti}`"
|
|
120
|
+
class="flex items-center gap-1.5 text-xs text-slate-300"
|
|
121
|
+
>
|
|
122
|
+
<UIcon name="i-lucide-square-check-big" class="h-3 w-3 text-slate-500" />
|
|
123
|
+
{{ task.title }}
|
|
124
|
+
</li>
|
|
125
|
+
</ul>
|
|
126
|
+
|
|
127
|
+
<div v-for="(mod, mi) in frame.modules" :key="`m-${mi}`" class="mt-2 pl-4">
|
|
128
|
+
<div class="flex items-center gap-1.5 text-xs font-medium text-slate-200">
|
|
129
|
+
<UIcon name="i-lucide-folder" class="h-3.5 w-3.5 text-amber-400" />
|
|
130
|
+
{{ mod.name }}
|
|
131
|
+
</div>
|
|
132
|
+
<ul class="mt-1 space-y-1 pl-5">
|
|
133
|
+
<li
|
|
134
|
+
v-for="(task, ti) in mod.tasks"
|
|
135
|
+
:key="`mt-${ti}`"
|
|
136
|
+
class="flex items-center gap-1.5 text-xs text-slate-300"
|
|
137
|
+
>
|
|
138
|
+
<UIcon name="i-lucide-square-check-big" class="h-3 w-3 text-slate-500" />
|
|
139
|
+
{{ task.title }}
|
|
140
|
+
</li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
147
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">Cancel</UButton>
|
|
148
|
+
<UButton
|
|
149
|
+
color="primary"
|
|
150
|
+
icon="i-lucide-wand-sparkles"
|
|
151
|
+
:loading="spawning"
|
|
152
|
+
:disabled="!plan || loadingPlan"
|
|
153
|
+
@click="spawn"
|
|
154
|
+
>
|
|
155
|
+
Spawn onto board
|
|
156
|
+
</UButton>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</UModal>
|
|
161
|
+
</template>
|