@cat-factory/app 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. package/package.json +45 -0
@@ -0,0 +1,175 @@
1
+ <script setup lang="ts">
2
+ import type { Notification } from '~/types/domain'
3
+
4
+ // The board's notification inbox: a bell with an open-count badge that opens a
5
+ // panel of human-actionable items (a PR awaiting a merge decision, a completed
6
+ // pipeline awaiting confirmation, CI that gave up). Each item can be acted on
7
+ // (merge / confirm / retry) or dismissed. Hydrated from the snapshot and patched
8
+ // live via the `notification` WorkspaceEvent.
9
+
10
+ const notifications = useNotificationsStore()
11
+ const ui = useUiStore()
12
+ const execution = useExecutionStore()
13
+
14
+ const busy = ref<string | null>(null)
15
+
16
+ /** Per-type display metadata (icon, colour, primary-action label). */
17
+ type Accent = 'warning' | 'primary' | 'error'
18
+ const META: Record<Notification['type'], { icon: string; color: Accent; action: string }> = {
19
+ merge_review: { icon: 'i-lucide-git-pull-request-arrow', color: 'warning', action: 'Merge' },
20
+ pipeline_complete: { icon: 'i-lucide-circle-check', color: 'primary', action: 'Confirm & merge' },
21
+ ci_failed: { icon: 'i-lucide-triangle-alert', color: 'error', action: 'Retry run' },
22
+ test_failed: { icon: 'i-lucide-flask-conical', color: 'error', action: 'Retry run' },
23
+ // Clicking the title opens the review window for the task (see `reveal`); "act" just marks
24
+ // it read (the server performs no side-effect for this type).
25
+ requirement_review: { icon: 'i-lucide-clipboard-list', color: 'primary', action: 'Mark read' },
26
+ // Clicking the title opens the clarity review window for the task (see `reveal`); "act"
27
+ // just marks it read (the server performs no side-effect for this type).
28
+ clarity_review: { icon: 'i-lucide-bug', color: 'primary', action: 'Mark read' },
29
+ // A post-release Datadog regression the on-call agent investigated. The human decides
30
+ // whether to revert (in GitHub via the PR link) or acknowledge; "act" marks it handled.
31
+ release_regression: { icon: 'i-lucide-activity', color: 'error', action: 'Acknowledge' },
32
+ // Clicking the title opens the parked step's decision surface (companion → step detail
33
+ // with the iteration-cap prompt; requirements → the review window); "act" just marks it
34
+ // read (the decision itself is resolved in that surface, not here).
35
+ decision_required: { icon: 'i-lucide-circle-help', color: 'warning', action: 'Mark read' },
36
+ }
37
+
38
+ /** A notification the escalation sweep has flagged as overdue (waited past the threshold). */
39
+ function isUrgent(n: Notification): boolean {
40
+ return n.severity === 'urgent'
41
+ }
42
+
43
+ /** True when any open notification is overdue — turns the toolbar bell red. */
44
+ const hasUrgent = computed(() => notifications.open.some(isUrgent))
45
+
46
+ /** Effective accent: urgent (red) overrides the type's base colour to convey "overdue". */
47
+ function accent(n: Notification): Accent {
48
+ return isUrgent(n) ? 'error' : META[n.type].color
49
+ }
50
+
51
+ async function act(n: Notification) {
52
+ busy.value = n.id
53
+ try {
54
+ await notifications.act(n.id)
55
+ } finally {
56
+ busy.value = null
57
+ }
58
+ }
59
+
60
+ async function dismiss(n: Notification) {
61
+ busy.value = n.id
62
+ try {
63
+ await notifications.dismiss(n.id)
64
+ } finally {
65
+ busy.value = null
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Clicking a notification's title takes the user where they can act on it. A
71
+ * `requirement_review` / `clarity_review` summons them straight back into the matching review
72
+ * window (the async incorporate/re-review raised new findings or hit the cap); every other
73
+ * type just focuses the related block on the board.
74
+ */
75
+ function reveal(n: Notification) {
76
+ if (!n.blockId) return
77
+ if (n.type === 'requirement_review') ui.openRequirementReview(n.blockId)
78
+ else if (n.type === 'clarity_review') ui.openClarityReview(n.blockId)
79
+ else if (n.type === 'decision_required') revealDecision(n)
80
+ else ui.select(n.blockId)
81
+ }
82
+
83
+ /**
84
+ * Open the decision surface for a parked iteration-cap run: find the run's step that is
85
+ * waiting on a human and open it through the universal step dispatch — which routes a
86
+ * `requirements-review` step to the review window and a companion step to its detail
87
+ * panel (where the iteration-cap prompt lives). Falls back to focusing the block.
88
+ */
89
+ function revealDecision(n: Notification) {
90
+ const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
91
+ const idx = instance?.steps.findIndex((s) => s.state === 'waiting_decision') ?? -1
92
+ if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
93
+ else if (n.blockId) ui.select(n.blockId)
94
+ }
95
+ </script>
96
+
97
+ <template>
98
+ <UPopover v-if="notifications.count" :content="{ align: 'end' }">
99
+ <UButton :color="hasUrgent ? 'error' : 'warning'" variant="soft" size="sm" icon="i-lucide-bell">
100
+ {{ notifications.count }}
101
+ </UButton>
102
+
103
+ <template #content>
104
+ <div class="max-h-[28rem] w-96 overflow-y-auto p-2">
105
+ <div class="px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
106
+ Needs your attention
107
+ </div>
108
+ <div
109
+ v-for="n in notifications.open"
110
+ :key="n.id"
111
+ class="rounded-lg border p-2.5 mt-1.5"
112
+ :class="
113
+ isUrgent(n)
114
+ ? 'border-error-500/60 bg-error-500/10'
115
+ : 'border-slate-700/60 bg-slate-800/40'
116
+ "
117
+ >
118
+ <div class="flex items-start gap-2">
119
+ <UIcon
120
+ :name="META[n.type].icon"
121
+ :class="`mt-0.5 h-4 w-4 text-${accent(n)}-400 shrink-0`"
122
+ />
123
+ <div class="min-w-0 flex-1">
124
+ <div class="flex items-center gap-1.5">
125
+ <button
126
+ class="block min-w-0 flex-1 truncate text-left text-sm font-medium text-slate-200 hover:underline"
127
+ :title="n.title"
128
+ @click="reveal(n)"
129
+ >
130
+ {{ n.title }}
131
+ </button>
132
+ <span
133
+ v-if="isUrgent(n)"
134
+ class="shrink-0 rounded bg-error-500/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-error-400"
135
+ >
136
+ Overdue
137
+ </span>
138
+ </div>
139
+ <p class="mt-0.5 text-[11px] leading-snug text-slate-400">{{ n.body }}</p>
140
+ <a
141
+ v-if="n.payload?.prUrl"
142
+ :href="n.payload.prUrl"
143
+ target="_blank"
144
+ rel="noopener"
145
+ class="mt-1 inline-flex items-center gap-1 text-[11px] text-sky-400 hover:underline"
146
+ >
147
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" /> Open PR
148
+ </a>
149
+ <div class="mt-2 flex items-center gap-1.5">
150
+ <UButton
151
+ :color="accent(n)"
152
+ variant="soft"
153
+ size="xs"
154
+ :loading="busy === n.id"
155
+ @click="act(n)"
156
+ >
157
+ {{ META[n.type].action }}
158
+ </UButton>
159
+ <UButton
160
+ color="neutral"
161
+ variant="ghost"
162
+ size="xs"
163
+ :disabled="busy === n.id"
164
+ @click="dismiss(n)"
165
+ >
166
+ Dismiss
167
+ </UButton>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </template>
174
+ </UPopover>
175
+ </template>
@@ -0,0 +1,314 @@
1
+ <script setup lang="ts">
2
+ // The left navbar. The old draggable block/pipeline palettes are gone — blocks
3
+ // and pipelines are created through the command bar (⌘K) and the board's own
4
+ // affordances. This panel is now navigation + a command-bar launcher: quick
5
+ // actions, repository management, integration management, the workspace-wide
6
+ // context-fragment library, and workspace configuration (merge thresholds +
7
+ // default models).
8
+ import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
9
+ import UserMenu from '~/components/auth/UserMenu.vue'
10
+
11
+ const documents = useDocumentsStore()
12
+ const tasks = useTasksStore()
13
+ const github = useGitHubStore()
14
+ const slack = useSlackStore()
15
+ const library = useFragmentLibraryStore()
16
+ const workspace = useWorkspaceStore()
17
+ const ui = useUiStore()
18
+
19
+ // Resolve whether the document-source / task-source / GitHub integrations are
20
+ // enabled on the backend, so each section is hidden entirely when it is off
21
+ // (mirrors how auth gates its UI). A 503 from a probe flips its `available` to
22
+ // false. Re-probe whenever the active board changes — connections are per board.
23
+ watch(
24
+ () => workspace.workspaceId,
25
+ (id) => {
26
+ if (!id) return
27
+ void documents.probe()
28
+ void tasks.probe()
29
+ void github.probe()
30
+ void slack.probe()
31
+ void library.probe()
32
+ },
33
+ { immediate: true },
34
+ )
35
+ </script>
36
+
37
+ <template>
38
+ <aside
39
+ class="flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-r border-slate-800 bg-slate-900/80 p-3 backdrop-blur"
40
+ >
41
+ <BoardSwitcher />
42
+
43
+ <!-- Command bar launcher (⌘K) — the primary way to create blocks / pipelines
44
+ and reach every action below. -->
45
+ <button
46
+ type="button"
47
+ class="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800/60 px-2.5 py-2 text-left text-sm text-slate-400 transition hover:border-slate-500 hover:bg-slate-800"
48
+ @click="ui.openCommandBar()"
49
+ >
50
+ <UIcon name="i-lucide-search" class="h-4 w-4 shrink-0" />
51
+ <span class="flex-1 truncate">Search or run a command…</span>
52
+ <UKbd value="⌘K" />
53
+ </button>
54
+
55
+ <section>
56
+ <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
57
+ Create
58
+ </h2>
59
+ <div class="space-y-1.5">
60
+ <UButton
61
+ block
62
+ color="primary"
63
+ variant="soft"
64
+ size="sm"
65
+ icon="i-lucide-workflow"
66
+ class="justify-start"
67
+ @click="ui.openBuilder()"
68
+ >
69
+ Build a pipeline
70
+ </UButton>
71
+ <UButton
72
+ block
73
+ color="primary"
74
+ variant="soft"
75
+ size="sm"
76
+ icon="i-lucide-plus"
77
+ class="justify-start"
78
+ @click="ui.openCommandBar()"
79
+ >
80
+ Add a block
81
+ </UButton>
82
+ </div>
83
+ </section>
84
+
85
+ <USeparator />
86
+
87
+ <section>
88
+ <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
89
+ Repositories
90
+ </h2>
91
+ <div class="space-y-1.5">
92
+ <UButton
93
+ v-if="github.available"
94
+ block
95
+ color="primary"
96
+ variant="soft"
97
+ size="sm"
98
+ icon="i-lucide-folder-git-2"
99
+ class="justify-start"
100
+ @click="ui.openAddService()"
101
+ >
102
+ Add from existing repo
103
+ </UButton>
104
+ <UButton
105
+ block
106
+ color="primary"
107
+ variant="soft"
108
+ size="sm"
109
+ icon="i-lucide-git-branch-plus"
110
+ class="justify-start"
111
+ @click="ui.openBootstrap()"
112
+ >
113
+ Bootstrap repo
114
+ </UButton>
115
+ </div>
116
+ </section>
117
+
118
+ <USeparator />
119
+
120
+ <section>
121
+ <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
122
+ Integrations
123
+ </h2>
124
+ <div class="space-y-1.5">
125
+ <UButton
126
+ v-if="github.available"
127
+ block
128
+ color="primary"
129
+ variant="soft"
130
+ size="sm"
131
+ icon="i-lucide-github"
132
+ class="justify-start"
133
+ @click="ui.openGitHub()"
134
+ >
135
+ <span class="truncate">
136
+ {{ github.connected ? github.connection?.accountLogin : 'Connect GitHub' }}
137
+ </span>
138
+ </UButton>
139
+
140
+ <UButton
141
+ v-if="slack.available"
142
+ block
143
+ color="primary"
144
+ variant="soft"
145
+ size="sm"
146
+ icon="i-lucide-slack"
147
+ class="justify-start"
148
+ @click="ui.openSlack()"
149
+ >
150
+ <span class="truncate">
151
+ {{ slack.connected ? slack.connection?.teamName : 'Connect Slack' }}
152
+ </span>
153
+ </UButton>
154
+
155
+ <template v-if="documents.available">
156
+ <UButton
157
+ v-for="src in documents.sources"
158
+ :key="src.source"
159
+ block
160
+ color="neutral"
161
+ variant="soft"
162
+ size="sm"
163
+ :icon="src.icon"
164
+ class="justify-start"
165
+ @click="ui.openDocumentConnect(src.source)"
166
+ >
167
+ <span class="truncate">
168
+ {{ documents.isConnected(src.source) ? src.label : `Connect ${src.label}` }}
169
+ </span>
170
+ </UButton>
171
+ <UButton
172
+ v-if="documents.anyConnected"
173
+ block
174
+ color="neutral"
175
+ variant="soft"
176
+ size="sm"
177
+ icon="i-lucide-file-down"
178
+ class="justify-start"
179
+ @click="ui.openDocumentImport(null)"
180
+ >
181
+ Import &amp; spawn
182
+ </UButton>
183
+ </template>
184
+
185
+ <template v-if="tasks.available">
186
+ <UButton
187
+ v-for="src in tasks.sources"
188
+ :key="src.source"
189
+ block
190
+ color="neutral"
191
+ variant="soft"
192
+ size="sm"
193
+ :icon="src.icon"
194
+ class="justify-start"
195
+ @click="ui.openTaskConnect(src.source)"
196
+ >
197
+ <span class="truncate">
198
+ {{ tasks.isConnected(src.source) ? src.label : `Connect ${src.label}` }}
199
+ </span>
200
+ </UButton>
201
+ <UButton
202
+ v-if="tasks.anyConnected"
203
+ block
204
+ color="neutral"
205
+ variant="soft"
206
+ size="sm"
207
+ icon="i-lucide-file-down"
208
+ class="justify-start"
209
+ @click="ui.openTaskImport(null)"
210
+ >
211
+ Import issues
212
+ </UButton>
213
+ </template>
214
+ </div>
215
+ </section>
216
+
217
+ <template v-if="library.available">
218
+ <USeparator />
219
+ <section>
220
+ <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
221
+ Workspace context
222
+ </h2>
223
+ <UButton
224
+ block
225
+ color="primary"
226
+ variant="soft"
227
+ size="sm"
228
+ icon="i-lucide-book-marked"
229
+ class="justify-start"
230
+ @click="ui.openFragmentLibrary()"
231
+ >
232
+ Context fragments
233
+ </UButton>
234
+ </section>
235
+ </template>
236
+
237
+ <USeparator />
238
+ <section>
239
+ <h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
240
+ Configuration
241
+ </h2>
242
+ <div class="space-y-1.5">
243
+ <UButton
244
+ block
245
+ color="primary"
246
+ variant="soft"
247
+ size="sm"
248
+ icon="i-lucide-git-merge"
249
+ class="justify-start"
250
+ @click="ui.openMergeThresholds()"
251
+ >
252
+ Merge thresholds
253
+ </UButton>
254
+ <UButton
255
+ block
256
+ color="primary"
257
+ variant="soft"
258
+ size="sm"
259
+ icon="i-lucide-sliders-horizontal"
260
+ class="justify-start"
261
+ @click="ui.openWorkspaceSettings()"
262
+ >
263
+ Workspace settings
264
+ </UButton>
265
+ <UButton
266
+ block
267
+ color="primary"
268
+ variant="soft"
269
+ size="sm"
270
+ icon="i-lucide-activity"
271
+ class="justify-start"
272
+ @click="ui.openDatadog()"
273
+ >
274
+ Post-release health
275
+ </UButton>
276
+ <UButton
277
+ block
278
+ color="primary"
279
+ variant="soft"
280
+ size="sm"
281
+ icon="i-lucide-cpu"
282
+ class="justify-start"
283
+ @click="ui.openModelDefaults()"
284
+ >
285
+ Default models
286
+ </UButton>
287
+ <UButton
288
+ block
289
+ color="primary"
290
+ variant="soft"
291
+ size="sm"
292
+ icon="i-lucide-book-open-check"
293
+ class="justify-start"
294
+ @click="ui.openServiceFragmentDefaults()"
295
+ >
296
+ Default service best practices
297
+ </UButton>
298
+ <UButton
299
+ block
300
+ color="primary"
301
+ variant="soft"
302
+ size="sm"
303
+ icon="i-lucide-server"
304
+ class="justify-start"
305
+ @click="ui.openLocalModels()"
306
+ >
307
+ My local runners
308
+ </UButton>
309
+ </div>
310
+ </section>
311
+
312
+ <UserMenu class="mt-auto" />
313
+ </aside>
314
+ </template>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ const workspace = useWorkspaceStore()
5
+
6
+ const spend = computed(() => workspace.spend)
7
+ /** Show the large warning only once the budget has been reached. */
8
+ const exceeded = computed(() => spend.value?.exceeded ?? false)
9
+
10
+ function money(amount: number, currency: string) {
11
+ try {
12
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
13
+ } catch {
14
+ // Fall back if the currency code isn't recognised by the runtime.
15
+ return `${amount.toFixed(2)} ${currency}`
16
+ }
17
+ }
18
+
19
+ const tokens = computed(() => {
20
+ const s = spend.value
21
+ if (!s) return ''
22
+ return new Intl.NumberFormat().format(s.inputTokens + s.outputTokens)
23
+ })
24
+
25
+ const resuming = ref(false)
26
+ async function resume() {
27
+ resuming.value = true
28
+ try {
29
+ await workspace.resumeSpend()
30
+ } finally {
31
+ resuming.value = false
32
+ }
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <Transition name="fade">
38
+ <div
39
+ v-if="exceeded && spend"
40
+ class="absolute inset-x-0 top-0 z-50 flex justify-center px-4 pt-4"
41
+ >
42
+ <div
43
+ class="w-full max-w-3xl rounded-2xl border-2 border-red-500/70 bg-red-950/95 p-5 shadow-2xl backdrop-blur"
44
+ role="alert"
45
+ >
46
+ <div class="flex items-start gap-4">
47
+ <UIcon name="i-lucide-octagon-alert" class="mt-0.5 h-10 w-10 shrink-0 text-red-400" />
48
+ <div class="min-w-0 flex-1">
49
+ <h2 class="text-lg font-semibold text-red-100">
50
+ Spend limit reached — agent execution paused
51
+ </h2>
52
+ <p class="mt-1 text-sm text-red-200/90">
53
+ This month's token spend has hit the configured budget, so running pipelines were
54
+ paused to avoid further cost. Execution resumes automatically when the budget rolls
55
+ over next month, or once the limit is raised.
56
+ </p>
57
+
58
+ <dl class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
59
+ <div class="rounded-lg bg-red-900/50 px-3 py-2">
60
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Spent</dt>
61
+ <dd class="text-base font-semibold tabular-nums text-red-50">
62
+ {{ money(spend.costSpent, spend.currency) }}
63
+ </dd>
64
+ </div>
65
+ <div class="rounded-lg bg-red-900/50 px-3 py-2">
66
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Budget</dt>
67
+ <dd class="text-base font-semibold tabular-nums text-red-50">
68
+ {{ money(spend.costLimit, spend.currency) }}
69
+ </dd>
70
+ </div>
71
+ <div class="rounded-lg bg-red-900/50 px-3 py-2">
72
+ <dt class="text-[11px] uppercase tracking-wide text-red-300/80">Tokens</dt>
73
+ <dd class="text-base font-semibold tabular-nums text-red-50">{{ tokens }}</dd>
74
+ </div>
75
+ </dl>
76
+
77
+ <div class="mt-4 flex items-center gap-3">
78
+ <UButton
79
+ color="error"
80
+ variant="solid"
81
+ icon="i-lucide-play"
82
+ :loading="resuming"
83
+ @click="resume"
84
+ >
85
+ Resume anyway
86
+ </UButton>
87
+ <span class="text-xs text-red-300/70">
88
+ Resuming continues spending; it will pause again if still over budget.
89
+ </span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </Transition>
96
+ </template>
97
+
98
+ <style scoped>
99
+ .fade-enter-active,
100
+ .fade-leave-active {
101
+ transition: opacity 0.2s ease;
102
+ }
103
+ .fade-enter-from,
104
+ .fade-leave-to {
105
+ opacity: 0;
106
+ }
107
+ </style>