@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,152 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
Account,
|
|
5
|
+
AccountInvitation,
|
|
6
|
+
AccountMember,
|
|
7
|
+
AccountRole,
|
|
8
|
+
CloudProvider,
|
|
9
|
+
EmailConnection,
|
|
10
|
+
} from '~/types/domain'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Account tenancy on the client: the accounts the signed-in user can switch
|
|
14
|
+
* between (their personal account plus any orgs they belong to) and which one is
|
|
15
|
+
* active. The active account scopes the board switcher and stamps new boards, so
|
|
16
|
+
* a team can keep org boards separate from personal ones.
|
|
17
|
+
*
|
|
18
|
+
* Empty when auth is disabled (the backend returns no accounts in dev), in which
|
|
19
|
+
* case the UI simply hides the account switcher and boards stay unscoped.
|
|
20
|
+
*/
|
|
21
|
+
export const useAccountsStore = defineStore(
|
|
22
|
+
'accounts',
|
|
23
|
+
() => {
|
|
24
|
+
const api = useApi()
|
|
25
|
+
|
|
26
|
+
const accounts = ref<Account[]>([])
|
|
27
|
+
/** Active account id (persisted so a reload keeps the same context). */
|
|
28
|
+
const activeAccountId = ref<string | null>(null)
|
|
29
|
+
const ready = ref(false)
|
|
30
|
+
|
|
31
|
+
const activeAccount = computed(
|
|
32
|
+
() => accounts.value.find((a) => a.id === activeAccountId.value) ?? null,
|
|
33
|
+
)
|
|
34
|
+
/** Whether accounts exist (auth on); gates the switcher UI. */
|
|
35
|
+
const enabled = computed(() => accounts.value.length > 0)
|
|
36
|
+
|
|
37
|
+
/** Load the user's accounts and resolve the active one (persisted or first). */
|
|
38
|
+
async function load() {
|
|
39
|
+
accounts.value = await api.listAccounts()
|
|
40
|
+
if (!activeAccountId.value || !accounts.value.some((a) => a.id === activeAccountId.value)) {
|
|
41
|
+
activeAccountId.value = accounts.value[0]?.id ?? null
|
|
42
|
+
}
|
|
43
|
+
ready.value = true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Create a shared org account and make it active. */
|
|
47
|
+
async function createOrg(name: string) {
|
|
48
|
+
const account = await api.createAccount({ name })
|
|
49
|
+
accounts.value.push(account)
|
|
50
|
+
activeAccountId.value = account.id
|
|
51
|
+
return account
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Switch the active account (the caller re-scopes the board list). */
|
|
55
|
+
function switchTo(id: string) {
|
|
56
|
+
activeAccountId.value = id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set an account's default cloud provider (the provider new services inherit).
|
|
61
|
+
* Owner-only on the backend; patches the loaded account in place on success.
|
|
62
|
+
*/
|
|
63
|
+
async function setDefaultCloudProvider(id: string, provider: CloudProvider) {
|
|
64
|
+
const updated = await api.updateAccount(id, { defaultCloudProvider: provider })
|
|
65
|
+
const i = accounts.value.findIndex((a) => a.id === id)
|
|
66
|
+
if (i >= 0) accounts.value[i] = updated
|
|
67
|
+
return updated
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- members + invitations -------------------------------------------
|
|
71
|
+
|
|
72
|
+
const members = ref<AccountMember[]>([])
|
|
73
|
+
const invitations = ref<AccountInvitation[]>([])
|
|
74
|
+
|
|
75
|
+
/** Load the active account's member roster + pending invitations. */
|
|
76
|
+
async function loadRoster(accountId: string) {
|
|
77
|
+
const [m, inv] = await Promise.all([
|
|
78
|
+
api.listAccountMembers(accountId),
|
|
79
|
+
api.listInvitations(accountId),
|
|
80
|
+
])
|
|
81
|
+
members.value = m
|
|
82
|
+
invitations.value = inv
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Invite a teammate by email; returns the accept link (for manual sharing). */
|
|
86
|
+
async function invite(accountId: string, email: string, roles: AccountRole[] = ['developer']) {
|
|
87
|
+
const { invitation, acceptUrl } = await api.createInvitation(accountId, { email, roles })
|
|
88
|
+
invitations.value = [invitation, ...invitations.value]
|
|
89
|
+
return acceptUrl
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function revokeInvite(accountId: string, invitationId: string) {
|
|
93
|
+
await api.revokeInvitation(accountId, invitationId)
|
|
94
|
+
invitations.value = invitations.value.filter((i) => i.id !== invitationId)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Set a member's role set (admin-only); patches the loaded roster in place. */
|
|
98
|
+
async function setMemberRoles(accountId: string, userId: string, roles: AccountRole[]) {
|
|
99
|
+
const updated = await api.setMemberRoles(accountId, userId, roles)
|
|
100
|
+
const i = members.value.findIndex((m) => m.userId === userId)
|
|
101
|
+
if (i >= 0) members.value[i] = updated
|
|
102
|
+
return updated
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- email sender connection -----------------------------------------
|
|
106
|
+
|
|
107
|
+
const emailConnection = ref<EmailConnection | null>(null)
|
|
108
|
+
const emailConfigured = ref(false)
|
|
109
|
+
|
|
110
|
+
async function loadEmailConnection(accountId: string) {
|
|
111
|
+
const res = await api.getEmailConnection(accountId)
|
|
112
|
+
emailConnection.value = res.connection
|
|
113
|
+
emailConfigured.value = res.configured
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function connectEmail(
|
|
117
|
+
accountId: string,
|
|
118
|
+
body: { provider: 'sendgrid' | 'resend'; apiKey: string; fromAddress: string },
|
|
119
|
+
) {
|
|
120
|
+
emailConnection.value = await api.connectEmail(accountId, body)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function disconnectEmail(accountId: string) {
|
|
124
|
+
await api.disconnectEmail(accountId)
|
|
125
|
+
emailConnection.value = null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
accounts,
|
|
130
|
+
activeAccountId,
|
|
131
|
+
activeAccount,
|
|
132
|
+
enabled,
|
|
133
|
+
ready,
|
|
134
|
+
members,
|
|
135
|
+
invitations,
|
|
136
|
+
emailConnection,
|
|
137
|
+
emailConfigured,
|
|
138
|
+
load,
|
|
139
|
+
createOrg,
|
|
140
|
+
switchTo,
|
|
141
|
+
setDefaultCloudProvider,
|
|
142
|
+
loadRoster,
|
|
143
|
+
invite,
|
|
144
|
+
revokeInvite,
|
|
145
|
+
setMemberRoles,
|
|
146
|
+
loadEmailConnection,
|
|
147
|
+
connectEmail,
|
|
148
|
+
disconnectEmail,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{ persist: { pick: ['activeAccountId'] } },
|
|
152
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { AgentConfigDescriptor } from '~/types/domain'
|
|
4
|
+
import { usePipelinesStore } from '~/stores/pipelines'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The agent config-contribution catalog: the task-level parameters the registered
|
|
8
|
+
* agent kinds surface (e.g. the Tester's environment). Static metadata hydrated from
|
|
9
|
+
* the workspace snapshot. The task-creation form and inspector render the subset
|
|
10
|
+
* whose owning agent kind appears in the task's selected pipeline.
|
|
11
|
+
*/
|
|
12
|
+
export const useAgentConfigStore = defineStore('agentConfig', () => {
|
|
13
|
+
const descriptors = ref<AgentConfigDescriptor[]>([])
|
|
14
|
+
|
|
15
|
+
function hydrate(list: AgentConfigDescriptor[]) {
|
|
16
|
+
descriptors.value = [...list]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The descriptors contributed by the agent kinds of the given pipeline (by id). */
|
|
20
|
+
function forPipeline(pipelineId: string | undefined): AgentConfigDescriptor[] {
|
|
21
|
+
if (!pipelineId) return []
|
|
22
|
+
const pipeline = usePipelinesStore().getPipeline(pipelineId)
|
|
23
|
+
if (!pipeline) return []
|
|
24
|
+
const kinds = new Set<string>(pipeline.agentKinds)
|
|
25
|
+
return descriptors.value.filter((d) => kinds.has(d.agentKind))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The descriptors contributed across a set of agent kinds (by id). */
|
|
29
|
+
function forKinds(kinds: Iterable<string>): AgentConfigDescriptor[] {
|
|
30
|
+
const set = new Set(kinds)
|
|
31
|
+
return descriptors.value.filter((d) => set.has(d.agentKind))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { descriptors, hydrate, forPipeline, forKinds }
|
|
35
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { AgentFailure, AgentRunKind, BootstrapJob, StepSubtasks } from '~/types/domain'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A coarse, per-block view of the current "agent run" against a block, regardless
|
|
9
|
+
* of which flow produced it — enough for the board to render a failure banner +
|
|
10
|
+
* retry and a "working…" progress badge uniformly. The rich step-level UI still
|
|
11
|
+
* reads the full {@link ExecutionInstance} from the execution store.
|
|
12
|
+
*/
|
|
13
|
+
export interface AgentRunSummary {
|
|
14
|
+
blockId: string
|
|
15
|
+
kind: AgentRunKind
|
|
16
|
+
/** The run's own status: execution running|blocked|done|paused|failed,
|
|
17
|
+
* bootstrap pending|running|succeeded|failed. */
|
|
18
|
+
status: string
|
|
19
|
+
/** Id of the run, for the unified retry endpoint. */
|
|
20
|
+
runId: string
|
|
21
|
+
/** Structured failure when `status` is `failed`; null otherwise. */
|
|
22
|
+
failure: AgentFailure | null
|
|
23
|
+
/** Latest subtask counts for a live progress bar (null until reported). */
|
|
24
|
+
subtasks: StepSubtasks | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Unified failure/retry surface over BOTH agent flows. Bootstrap runs are held
|
|
29
|
+
* here (they have no other home on the client); executions are read from the
|
|
30
|
+
* execution store so they're never duplicated. `byBlock` merges the two so a
|
|
31
|
+
* board card / inspector can look itself up and show the same failure banner +
|
|
32
|
+
* retry whether the block was made by a bootstrap or is running a task pipeline.
|
|
33
|
+
*
|
|
34
|
+
* This replaces the old bootstrap-only `bootstrap.byBlock`/`retry`, whose retry
|
|
35
|
+
* could silently vanish when the separate jobs projection failed to resolve.
|
|
36
|
+
*/
|
|
37
|
+
export const useAgentRunsStore = defineStore('agentRuns', () => {
|
|
38
|
+
const api = useApi()
|
|
39
|
+
const execution = useExecutionStore()
|
|
40
|
+
|
|
41
|
+
/** Bootstrap runs for this workspace, newest-first. */
|
|
42
|
+
const bootstrapJobs = ref<BootstrapJob[]>([])
|
|
43
|
+
|
|
44
|
+
/** Replace the cached bootstrap runs with a server snapshot. */
|
|
45
|
+
function hydrate(jobs: BootstrapJob[]) {
|
|
46
|
+
bootstrapJobs.value = [...jobs].sort((a, b) => b.createdAt - a.createdAt)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Patch a bootstrap run from a real-time `bootstrap` event (or after launching
|
|
51
|
+
* one): replace it in place by id, else prepend it. Keeps the service card
|
|
52
|
+
* reactive to live progress without a refetch.
|
|
53
|
+
*/
|
|
54
|
+
function upsertBootstrap(job: BootstrapJob) {
|
|
55
|
+
const i = bootstrapJobs.value.findIndex((j) => j.id === job.id)
|
|
56
|
+
if (i >= 0) bootstrapJobs.value[i] = job
|
|
57
|
+
else bootstrapJobs.value.unshift(job)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The current run summary per block, merged from execution instances (task
|
|
62
|
+
* runs) and bootstrap runs (service frames). Executions take a block first;
|
|
63
|
+
* a frame block has no execution, so the newest bootstrap run wins there.
|
|
64
|
+
*/
|
|
65
|
+
const byBlock = computed<Record<string, AgentRunSummary>>(() => {
|
|
66
|
+
const map: Record<string, AgentRunSummary> = {}
|
|
67
|
+
for (const e of execution.instances) {
|
|
68
|
+
map[e.blockId] = {
|
|
69
|
+
blockId: e.blockId,
|
|
70
|
+
kind: 'execution',
|
|
71
|
+
status: e.status,
|
|
72
|
+
runId: e.id,
|
|
73
|
+
failure: e.failure ?? null,
|
|
74
|
+
subtasks: e.steps[e.currentStep]?.subtasks ?? null,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// `bootstrapJobs` is newest-first; keep the first (newest) seen per block.
|
|
78
|
+
for (const job of bootstrapJobs.value) {
|
|
79
|
+
if (!job.blockId || map[job.blockId]) continue
|
|
80
|
+
map[job.blockId] = {
|
|
81
|
+
blockId: job.blockId,
|
|
82
|
+
kind: 'bootstrap',
|
|
83
|
+
status: job.status,
|
|
84
|
+
runId: job.id,
|
|
85
|
+
failure: job.failure,
|
|
86
|
+
subtasks: job.subtasks,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return map
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Retry a failed run (bootstrap or execution) via the unified endpoint, then
|
|
94
|
+
* refresh the snapshot so both stores rehydrate — the card flips from failed
|
|
95
|
+
* back to "working…" as a fresh run is dispatched server-side.
|
|
96
|
+
*/
|
|
97
|
+
async function retry(runId: string) {
|
|
98
|
+
const ws = useWorkspaceStore()
|
|
99
|
+
const personal = usePersonalSubscriptionsStore()
|
|
100
|
+
// A failed run on a Claude-pinned block needs the retrying user's personal password;
|
|
101
|
+
// supplied from cache and prompted (then retried) on a 428, exactly like start.
|
|
102
|
+
await personal.withCredential(async (password) => {
|
|
103
|
+
await api.retryAgentRun(ws.requireId(), runId, password)
|
|
104
|
+
await ws.refresh()
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Explicitly stop a running run (bootstrap or execution) via the unified endpoint:
|
|
110
|
+
* the backend kills the per-run container + tears down the durable driver, then
|
|
111
|
+
* marks the run cancelled. Refresh so both stores rehydrate and the card flips out
|
|
112
|
+
* of its "running" state. Returns the resolved kind so the caller can word a toast.
|
|
113
|
+
*/
|
|
114
|
+
async function stop(runId: string): Promise<AgentRunKind> {
|
|
115
|
+
const ws = useWorkspaceStore()
|
|
116
|
+
const { kind } = await api.stopAgentRun(ws.requireId(), runId)
|
|
117
|
+
await ws.refresh()
|
|
118
|
+
return kind
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { bootstrapJobs, hydrate, upsertBootstrap, byBlock, retry, stop }
|
|
122
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { AGENT_ARCHETYPES, AGENT_BY_KIND, uid } from '~/utils/catalog'
|
|
4
|
+
import type { AgentArchetype, AgentKind } from '~/types/domain'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The agent palette. Seeded from the static catalog, but custom agents can be
|
|
8
|
+
* added at runtime (they show up in the pipeline builder). Newly created agents
|
|
9
|
+
* are also registered into AGENT_BY_KIND so the many components that look an
|
|
10
|
+
* agent up by kind keep rendering it correctly.
|
|
11
|
+
*/
|
|
12
|
+
export const useAgentsStore = defineStore('agents', () => {
|
|
13
|
+
const archetypes = ref<AgentArchetype[]>([...AGENT_ARCHETYPES])
|
|
14
|
+
|
|
15
|
+
function get(kind: AgentKind) {
|
|
16
|
+
return AGENT_BY_KIND[kind]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function addAgent(input: {
|
|
20
|
+
label: string
|
|
21
|
+
description?: string
|
|
22
|
+
icon?: string
|
|
23
|
+
color?: string
|
|
24
|
+
}): AgentArchetype {
|
|
25
|
+
const archetype: AgentArchetype = {
|
|
26
|
+
// custom kinds are free-form ids; cast keeps the existing AgentKind typing happy
|
|
27
|
+
kind: uid('agent') as AgentKind,
|
|
28
|
+
label: input.label.trim() || 'Custom Agent',
|
|
29
|
+
description: input.description?.trim() || 'Custom agent.',
|
|
30
|
+
icon: input.icon || 'i-lucide-sparkles',
|
|
31
|
+
color: input.color || '#22d3ee',
|
|
32
|
+
}
|
|
33
|
+
// register for kind-based lookups across the app, then surface in the palette
|
|
34
|
+
AGENT_BY_KIND[archetype.kind] = archetype
|
|
35
|
+
archetypes.value.push(archetype)
|
|
36
|
+
return archetype
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { archetypes, get, addAgent }
|
|
40
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { AddApiKeyInput, ApiKey, ApiKeyProvider } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The direct-provider API keys reachable from a workspace: the workspace's own keys
|
|
7
|
+
* plus the signed-in user's personal keys. Account-scoped keys (shared by every
|
|
8
|
+
* workspace in the account, admin-managed) are loaded separately via `loadAccountKeys`
|
|
9
|
+
* and surfaced in account/team settings. Onboarded via UI, stored encrypted + pooled by
|
|
10
|
+
* the backend; keys are write-only so only metadata + rolling-window usage is ever
|
|
11
|
+
* returned. `configuredProviders` drives the model picker (a direct model is selectable
|
|
12
|
+
* once a key for its provider is connected at any reachable scope).
|
|
13
|
+
*/
|
|
14
|
+
export const useApiKeysStore = defineStore('apiKeys', () => {
|
|
15
|
+
const api = useApi()
|
|
16
|
+
const workspaceKeys = ref<ApiKey[]>([])
|
|
17
|
+
const userKeys = ref<ApiKey[]>([])
|
|
18
|
+
const accountKeys = ref<ApiKey[]>([])
|
|
19
|
+
const accountId = ref<string | null>(null)
|
|
20
|
+
const workspaceId = ref<string | null>(null)
|
|
21
|
+
const loading = ref(false)
|
|
22
|
+
|
|
23
|
+
async function load(ws: string) {
|
|
24
|
+
workspaceId.value = ws
|
|
25
|
+
loading.value = true
|
|
26
|
+
try {
|
|
27
|
+
const [wsRes, meRes] = await Promise.all([
|
|
28
|
+
api.listWorkspaceApiKeys(ws),
|
|
29
|
+
api.listMyApiKeys().catch(() => ({ keys: [] as ApiKey[] })),
|
|
30
|
+
])
|
|
31
|
+
workspaceKeys.value = wsRes.keys
|
|
32
|
+
userKeys.value = meRes.keys
|
|
33
|
+
} finally {
|
|
34
|
+
loading.value = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function addWorkspaceKey(input: AddApiKeyInput) {
|
|
39
|
+
if (!workspaceId.value) return
|
|
40
|
+
const created = await api.addWorkspaceApiKey(workspaceId.value, input)
|
|
41
|
+
workspaceKeys.value = [...workspaceKeys.value, created]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function removeWorkspaceKey(id: string) {
|
|
45
|
+
if (!workspaceId.value) return
|
|
46
|
+
await api.removeWorkspaceApiKey(workspaceId.value, id)
|
|
47
|
+
workspaceKeys.value = workspaceKeys.value.filter((k) => k.id !== id)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function addUserKey(input: AddApiKeyInput) {
|
|
51
|
+
const created = await api.addMyApiKey(input)
|
|
52
|
+
userKeys.value = [...userKeys.value, created]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function removeUserKey(id: string) {
|
|
56
|
+
await api.removeMyApiKey(id)
|
|
57
|
+
userKeys.value = userKeys.value.filter((k) => k.id !== id)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- Account-scoped keys (admin-managed, shared by the account's workspaces) ----
|
|
61
|
+
|
|
62
|
+
async function loadAccountKeys(acc: string) {
|
|
63
|
+
accountId.value = acc
|
|
64
|
+
accountKeys.value = (await api.listAccountApiKeys(acc)).keys
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function addAccountKey(input: AddApiKeyInput) {
|
|
68
|
+
if (!accountId.value) return
|
|
69
|
+
const created = await api.addAccountApiKey(accountId.value, input)
|
|
70
|
+
accountKeys.value = [...accountKeys.value, created]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function removeAccountKey(id: string) {
|
|
74
|
+
if (!accountId.value) return
|
|
75
|
+
await api.removeAccountApiKey(accountId.value, id)
|
|
76
|
+
accountKeys.value = accountKeys.value.filter((k) => k.id !== id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Every key reachable from the workspace (workspace + user scopes), newest first. */
|
|
80
|
+
const allKeys = computed<ApiKey[]>(() => [...workspaceKeys.value, ...userKeys.value])
|
|
81
|
+
|
|
82
|
+
/** Providers with at least one reachable connected key. */
|
|
83
|
+
const configuredProviders = computed(
|
|
84
|
+
() => new Set<ApiKeyProvider>(allKeys.value.map((k) => k.provider)),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
function hasProvider(provider: ApiKeyProvider | undefined): boolean {
|
|
88
|
+
return provider ? configuredProviders.value.has(provider) : false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
workspaceKeys,
|
|
93
|
+
userKeys,
|
|
94
|
+
accountKeys,
|
|
95
|
+
allKeys,
|
|
96
|
+
loading,
|
|
97
|
+
load,
|
|
98
|
+
addWorkspaceKey,
|
|
99
|
+
removeWorkspaceKey,
|
|
100
|
+
addUserKey,
|
|
101
|
+
removeUserKey,
|
|
102
|
+
loadAccountKeys,
|
|
103
|
+
addAccountKey,
|
|
104
|
+
removeAccountKey,
|
|
105
|
+
configuredProviders,
|
|
106
|
+
hasProvider,
|
|
107
|
+
}
|
|
108
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { AuthUser } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* "Login with GitHub" session state. The backend mints a signed session token
|
|
7
|
+
* and hands it back via a URL fragment after the OAuth round-trip; we persist
|
|
8
|
+
* just that token and replay it as a bearer header on every API call (see
|
|
9
|
+
* `useApi`). Auth is opt-in on the backend, so `required` gates whether the UI
|
|
10
|
+
* shows a login screen at all — when the backend has auth disabled the app runs
|
|
11
|
+
* exactly as before.
|
|
12
|
+
*/
|
|
13
|
+
export const useAuthStore = defineStore(
|
|
14
|
+
'auth',
|
|
15
|
+
() => {
|
|
16
|
+
const api = useApi()
|
|
17
|
+
const apiBase = useRuntimeConfig().public.apiBase
|
|
18
|
+
|
|
19
|
+
/** Signed session token (persisted), or null when signed out. */
|
|
20
|
+
const token = ref<string | null>(null)
|
|
21
|
+
/** The signed-in user, resolved from the token on boot. */
|
|
22
|
+
const user = ref<AuthUser | null>(null)
|
|
23
|
+
/** Whether the backend requires authentication. */
|
|
24
|
+
const required = ref(false)
|
|
25
|
+
/** Which login providers the backend offers (drives the login UI). */
|
|
26
|
+
const providers = ref({ github: false, password: false, google: false })
|
|
27
|
+
/**
|
|
28
|
+
* Local-mode signals from the backend. Present only when running the local facade;
|
|
29
|
+
* `githubPatSetupUrl` is set when local mode has no GitHub PAT configured (drives the
|
|
30
|
+
* setup banner). Null on every other facade.
|
|
31
|
+
*/
|
|
32
|
+
const localMode = ref<{ enabled: boolean; githubPatSetupUrl?: string } | null>(null)
|
|
33
|
+
/** True once the initial auth handshake has settled. */
|
|
34
|
+
const ready = ref(false)
|
|
35
|
+
|
|
36
|
+
/** May the app render? True when auth is off, or on with a known user. */
|
|
37
|
+
const isAuthenticated = computed(() => !required.value || user.value !== null)
|
|
38
|
+
|
|
39
|
+
/** Pull a token handed back in the post-login URL fragment (#token=…). */
|
|
40
|
+
function consumeRedirectToken() {
|
|
41
|
+
if (typeof window === 'undefined') return
|
|
42
|
+
const match = /(?:^#|[#&])token=([^&]+)/.exec(window.location.hash)
|
|
43
|
+
if (!match) return
|
|
44
|
+
token.value = decodeURIComponent(match[1]!)
|
|
45
|
+
// Strip the token from the URL so it isn't left in history or shared.
|
|
46
|
+
history.replaceState(null, '', window.location.pathname + window.location.search)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve auth state: capture any redirect token, then check the backend. */
|
|
50
|
+
async function bootstrap() {
|
|
51
|
+
consumeRedirectToken()
|
|
52
|
+
try {
|
|
53
|
+
const config = await api.getAuthConfig()
|
|
54
|
+
required.value = config.enabled
|
|
55
|
+
if (config.providers) providers.value = config.providers
|
|
56
|
+
localMode.value = config.localMode ?? null
|
|
57
|
+
} catch {
|
|
58
|
+
// Backend unreachable — let the board's own error UI handle it.
|
|
59
|
+
required.value = false
|
|
60
|
+
ready.value = true
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (required.value && token.value) {
|
|
65
|
+
try {
|
|
66
|
+
user.value = (await api.getMe()).user
|
|
67
|
+
} catch {
|
|
68
|
+
user.value = null
|
|
69
|
+
}
|
|
70
|
+
if (!user.value) token.value = null
|
|
71
|
+
}
|
|
72
|
+
// An already-signed-in user who followed an invite link redeems it here (a
|
|
73
|
+
// brand-new user redeems it server-side during signup/OAuth instead).
|
|
74
|
+
if (user.value) await maybeAcceptInvite()
|
|
75
|
+
ready.value = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Redeem an `?invite=` token in the URL for the signed-in user, then clean the URL. */
|
|
79
|
+
async function maybeAcceptInvite() {
|
|
80
|
+
if (typeof window === 'undefined') return
|
|
81
|
+
const params = new URLSearchParams(window.location.search)
|
|
82
|
+
const inviteToken = params.get('invite')
|
|
83
|
+
if (!inviteToken) return
|
|
84
|
+
try {
|
|
85
|
+
await api.acceptInvite(inviteToken)
|
|
86
|
+
} catch {
|
|
87
|
+
// Stale/already-accepted invite — ignore and let the app load normally.
|
|
88
|
+
}
|
|
89
|
+
params.delete('invite')
|
|
90
|
+
const qs = params.toString()
|
|
91
|
+
history.replaceState(null, '', window.location.pathname + (qs ? `?${qs}` : ''))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Build a post-login redirect back to the current page, with an optional invite. */
|
|
95
|
+
function redirectTarget(invite?: string): string {
|
|
96
|
+
const here = window.location.origin + window.location.pathname
|
|
97
|
+
const params = new URLSearchParams({ redirect: here })
|
|
98
|
+
if (invite) params.set('invite', invite)
|
|
99
|
+
return params.toString()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Send the browser to the backend's GitHub login, returning here after. */
|
|
103
|
+
function login(invite?: string) {
|
|
104
|
+
if (typeof window === 'undefined') return
|
|
105
|
+
window.location.href = `${apiBase}/auth/login?${redirectTarget(invite)}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Send the browser to the backend's Google login, returning here after. */
|
|
109
|
+
function loginWithGoogle(invite?: string) {
|
|
110
|
+
if (typeof window === 'undefined') return
|
|
111
|
+
window.location.href = `${apiBase}/auth/google/login?${redirectTarget(invite)}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Apply a freshly-minted token + user (from password signup/login). */
|
|
115
|
+
function applySession(result: { token: string; user: AuthUser }) {
|
|
116
|
+
token.value = result.token
|
|
117
|
+
user.value = result.user
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Register a new email/password user (optionally redeeming an invite). */
|
|
121
|
+
async function signup(body: {
|
|
122
|
+
email: string
|
|
123
|
+
password: string
|
|
124
|
+
name?: string
|
|
125
|
+
invite?: string
|
|
126
|
+
}) {
|
|
127
|
+
applySession(await api.signup(body))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Sign in with email/password. */
|
|
131
|
+
async function passwordLogin(body: { email: string; password: string }) {
|
|
132
|
+
applySession(await api.passwordLogin(body))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Drop the local session (sessions are stateless server-side). */
|
|
136
|
+
function logout() {
|
|
137
|
+
api.logout().catch(() => {})
|
|
138
|
+
token.value = null
|
|
139
|
+
user.value = null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Called by the API client when a request comes back 401. */
|
|
143
|
+
function handleUnauthorized() {
|
|
144
|
+
token.value = null
|
|
145
|
+
user.value = null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
token,
|
|
150
|
+
user,
|
|
151
|
+
required,
|
|
152
|
+
providers,
|
|
153
|
+
localMode,
|
|
154
|
+
ready,
|
|
155
|
+
isAuthenticated,
|
|
156
|
+
bootstrap,
|
|
157
|
+
login,
|
|
158
|
+
loginWithGoogle,
|
|
159
|
+
signup,
|
|
160
|
+
passwordLogin,
|
|
161
|
+
logout,
|
|
162
|
+
handleUnauthorized,
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ persist: { pick: ['token'] } },
|
|
166
|
+
)
|