@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,156 @@
1
+ <script setup lang="ts">
2
+ import { useBoardFlow } from '~/composables/useBoardFlow'
3
+ import NotificationsInbox from '~/components/layout/NotificationsInbox.vue'
4
+ import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
5
+ import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
6
+
7
+ const ui = useUiStore()
8
+ const board = useBoardStore()
9
+ const execution = useExecutionStore()
10
+ const workspace = useWorkspaceStore()
11
+ const services = useServicesStore()
12
+ const toast = useToast()
13
+ const { fitView, zoomIn, zoomOut } = useBoardFlow()
14
+
15
+ async function mountService(serviceId: string, title: string) {
16
+ try {
17
+ await services.mount(serviceId)
18
+ toast.add({ title: `Added ${title}`, icon: 'i-lucide-box', color: 'success' })
19
+ } catch (e) {
20
+ toast.add({
21
+ title: 'Could not add service',
22
+ description: e instanceof Error ? e.message : String(e),
23
+ color: 'error',
24
+ })
25
+ }
26
+ }
27
+
28
+ // The org's services not yet on this board — mounting one adds its shared frame here.
29
+ const mountableItems = computed(() =>
30
+ services.mountable.map((s) => {
31
+ const title = board.getBlock(s.frameBlockId)?.title ?? s.frameBlockId
32
+ return {
33
+ label: title,
34
+ icon: 'i-lucide-box',
35
+ onSelect: () => {
36
+ void mountService(s.id, title)
37
+ },
38
+ }
39
+ }),
40
+ )
41
+
42
+ const zoomPct = computed(() => Math.round(ui.zoom * 100))
43
+ const lodLabel = computed(
44
+ () =>
45
+ ({
46
+ far: 'Overview',
47
+ mid: 'Summary',
48
+ close: 'Detail',
49
+ steps: 'Build steps',
50
+ subtasks: 'Subtasks',
51
+ })[ui.lod],
52
+ )
53
+
54
+ // Live spend indicator: shown once any tokens have been metered this period.
55
+ const spend = computed(() => workspace.spend)
56
+ const showSpend = computed(() => !!spend.value && spend.value.costSpent > 0)
57
+ const spendLabel = computed(() => {
58
+ const s = spend.value
59
+ if (!s) return ''
60
+ const fmt = (n: number) => {
61
+ try {
62
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency: s.currency }).format(n)
63
+ } catch {
64
+ return `${n.toFixed(2)} ${s.currency}`
65
+ }
66
+ }
67
+ return `${fmt(s.costSpent)} / ${fmt(s.costLimit)}`
68
+ })
69
+ const spendColor = computed(() => (spend.value?.exceeded ? 'error' : 'neutral'))
70
+
71
+ const decisionItems = computed(() =>
72
+ execution.openDecisions.map((d) => {
73
+ const b = board.getBlock(d.blockId)
74
+ return {
75
+ label: b?.title ?? 'Block',
76
+ description: d.decision.question,
77
+ icon: 'i-lucide-circle-help',
78
+ onSelect: () => ui.openDecision(d.instanceId, d.decision.id),
79
+ }
80
+ }),
81
+ )
82
+ </script>
83
+
84
+ <template>
85
+ <div
86
+ class="absolute left-1/2 top-4 z-20 flex -translate-x-1/2 items-center gap-1 rounded-full border border-slate-700 bg-slate-900/90 px-2 py-1.5 shadow-xl backdrop-blur"
87
+ >
88
+ <!-- zoom controls -->
89
+ <UButton
90
+ icon="i-lucide-zoom-out"
91
+ color="neutral"
92
+ variant="ghost"
93
+ size="sm"
94
+ @click="zoomOut()"
95
+ />
96
+ <div class="w-20 text-center text-xs tabular-nums text-slate-300">
97
+ {{ zoomPct }}%
98
+ <div class="text-[9px] uppercase tracking-wide text-slate-500">{{ lodLabel }}</div>
99
+ </div>
100
+ <UButton icon="i-lucide-zoom-in" color="neutral" variant="ghost" size="sm" @click="zoomIn()" />
101
+ <UButton
102
+ icon="i-lucide-maximize"
103
+ color="neutral"
104
+ variant="ghost"
105
+ size="sm"
106
+ @click="fitView({ padding: 0.2 })"
107
+ />
108
+
109
+ <USeparator orientation="vertical" class="mx-1 h-6" />
110
+
111
+ <!-- decisions queue -->
112
+ <UDropdownMenu v-if="execution.pendingDecisionCount" :items="decisionItems">
113
+ <UButton color="warning" variant="soft" size="sm" icon="i-lucide-circle-help">
114
+ {{ execution.pendingDecisionCount }} decision{{
115
+ execution.pendingDecisionCount === 1 ? '' : 's'
116
+ }}
117
+ </UButton>
118
+ </UDropdownMenu>
119
+
120
+ <!-- in-org sharing: add an existing org service to this board -->
121
+ <UDropdownMenu v-if="mountableItems.length" :items="mountableItems">
122
+ <UButton color="neutral" variant="ghost" size="sm" icon="i-lucide-plus-circle">
123
+ Add service
124
+ </UButton>
125
+ </UDropdownMenu>
126
+
127
+ <!-- human-actionable notifications (merge review, pipeline complete, CI failed) -->
128
+ <NotificationsInbox />
129
+
130
+ <!-- LLM vendor subscriptions (Claude Code / Codex token pool) -->
131
+ <UButton
132
+ color="neutral"
133
+ variant="ghost"
134
+ size="sm"
135
+ icon="i-lucide-key-round"
136
+ title="Connect LLM vendor subscriptions (Claude Code / Codex)"
137
+ @click="ui.openVendorCredentials()"
138
+ >
139
+ Vendors
140
+ </UButton>
141
+ <VendorCredentialsModal />
142
+ <PersonalCredentialModal />
143
+
144
+ <!-- spend safeguard usage -->
145
+ <UButton
146
+ v-if="showSpend"
147
+ :color="spendColor"
148
+ variant="soft"
149
+ size="sm"
150
+ icon="i-lucide-wallet"
151
+ :title="spend?.exceeded ? 'Spend limit reached — runs paused' : 'Token spend this month'"
152
+ >
153
+ {{ spendLabel }}
154
+ </UButton>
155
+ </div>
156
+ </template>
@@ -0,0 +1,336 @@
1
+ <script setup lang="ts">
2
+ // The command bar (⌘K / Ctrl+K) — a searchable launcher for every action that
3
+ // used to live as a button or draggable in the left panel. It is the primary way
4
+ // to create blocks and pipelines now that the draggable palettes are gone, and a
5
+ // fast path to every integration / settings surface. Commands are assembled from
6
+ // the live stores so only available actions (connected integrations, etc.) show.
7
+ import type { BlockType } from '~/types/domain'
8
+ import { BLOCK_TYPE_META } from '~/utils/catalog'
9
+
10
+ interface Command {
11
+ id: string
12
+ label: string
13
+ group: string
14
+ icon: string
15
+ /** Extra words matched by the fuzzy filter beyond the label. */
16
+ keywords?: string
17
+ run: () => void | Promise<void>
18
+ }
19
+
20
+ const ui = useUiStore()
21
+ const board = useBoardStore()
22
+ const github = useGitHubStore()
23
+ const slack = useSlackStore()
24
+ const documents = useDocumentsStore()
25
+ const tasks = useTasksStore()
26
+ const library = useFragmentLibraryStore()
27
+
28
+ const open = computed({
29
+ get: () => ui.commandBarOpen,
30
+ set: (v: boolean) => (v ? ui.openCommandBar() : ui.closeCommandBar()),
31
+ })
32
+
33
+ const query = ref('')
34
+ const activeIndex = ref(0)
35
+
36
+ // New top-level blocks are created without a drop position now, so stagger each
37
+ // one slightly off the canvas origin to keep them from stacking exactly.
38
+ let spawnCount = 0
39
+ function spawnPosition() {
40
+ const offset = (spawnCount++ % 6) * 28
41
+ return { x: 160 + offset, y: 160 + offset }
42
+ }
43
+
44
+ async function addBlock(type: BlockType) {
45
+ const block = await board.addBlock(type, spawnPosition())
46
+ ui.select(block.id)
47
+ }
48
+
49
+ const commands = computed<Command[]>(() => {
50
+ const list: Command[] = []
51
+
52
+ // ---- Create -------------------------------------------------------------
53
+ list.push({
54
+ id: 'new-pipeline',
55
+ label: 'Build a pipeline',
56
+ group: 'Create',
57
+ icon: 'i-lucide-workflow',
58
+ keywords: 'pipeline agents chain',
59
+ run: () => ui.openBuilder(),
60
+ })
61
+ for (const type of Object.keys(BLOCK_TYPE_META) as BlockType[]) {
62
+ const meta = BLOCK_TYPE_META[type]
63
+ list.push({
64
+ id: `add-block-${type}`,
65
+ label: `Add ${meta.label} block`,
66
+ group: 'Create',
67
+ icon: meta.icon,
68
+ keywords: 'block frame service create new',
69
+ run: () => addBlock(type),
70
+ })
71
+ }
72
+
73
+ // ---- Repositories -------------------------------------------------------
74
+ if (github.available) {
75
+ list.push({
76
+ id: 'add-from-repo',
77
+ label: 'Add service from existing repo',
78
+ group: 'Repositories',
79
+ icon: 'i-lucide-folder-git-2',
80
+ keywords: 'github import existing',
81
+ run: () => ui.openAddService(),
82
+ })
83
+ }
84
+ list.push({
85
+ id: 'bootstrap-repo',
86
+ label: 'Bootstrap a new repo',
87
+ group: 'Repositories',
88
+ icon: 'i-lucide-git-branch-plus',
89
+ keywords: 'scaffold create reference architecture',
90
+ run: () => ui.openBootstrap(),
91
+ })
92
+
93
+ // ---- Integrations -------------------------------------------------------
94
+ if (github.available) {
95
+ list.push({
96
+ id: 'github',
97
+ label: github.connected ? 'Manage GitHub connection' : 'Connect GitHub',
98
+ group: 'Integrations',
99
+ icon: 'i-lucide-github',
100
+ keywords: 'git repos pull requests issues',
101
+ run: () => ui.openGitHub(),
102
+ })
103
+ }
104
+ if (slack.available) {
105
+ list.push({
106
+ id: 'slack',
107
+ label: slack.connected ? 'Manage Slack notifications' : 'Connect Slack',
108
+ group: 'Integrations',
109
+ icon: 'i-lucide-slack',
110
+ keywords: 'slack notifications channel mentions',
111
+ run: () => ui.openSlack(),
112
+ })
113
+ }
114
+ if (documents.available) {
115
+ for (const src of documents.sources) {
116
+ list.push({
117
+ id: `doc-connect-${src.source}`,
118
+ label: documents.isConnected(src.source) ? `Manage ${src.label}` : `Connect ${src.label}`,
119
+ group: 'Integrations',
120
+ icon: src.icon,
121
+ keywords: 'document source prd rfc',
122
+ run: () => ui.openDocumentConnect(src.source),
123
+ })
124
+ }
125
+ if (documents.anyConnected) {
126
+ list.push({
127
+ id: 'doc-import',
128
+ label: 'Import & spawn from documents',
129
+ group: 'Integrations',
130
+ icon: 'i-lucide-file-down',
131
+ keywords: 'document import spawn',
132
+ run: () => ui.openDocumentImport(null),
133
+ })
134
+ }
135
+ }
136
+ if (tasks.available) {
137
+ for (const src of tasks.sources) {
138
+ list.push({
139
+ id: `task-connect-${src.source}`,
140
+ label: tasks.isConnected(src.source) ? `Manage ${src.label}` : `Connect ${src.label}`,
141
+ group: 'Integrations',
142
+ icon: src.icon,
143
+ keywords: 'task source tracker issues',
144
+ run: () => ui.openTaskConnect(src.source),
145
+ })
146
+ }
147
+ if (tasks.anyConnected) {
148
+ list.push({
149
+ id: 'task-import',
150
+ label: 'Import issues',
151
+ group: 'Integrations',
152
+ icon: 'i-lucide-file-down',
153
+ keywords: 'task import issues',
154
+ run: () => ui.openTaskImport(null),
155
+ })
156
+ }
157
+ }
158
+
159
+ // ---- Workspace ----------------------------------------------------------
160
+ if (library.available) {
161
+ list.push({
162
+ id: 'fragments',
163
+ label: 'Context fragment library',
164
+ group: 'Workspace',
165
+ icon: 'i-lucide-book-marked',
166
+ keywords: 'prompt fragments best practice guidelines context',
167
+ run: () => ui.openFragmentLibrary(),
168
+ })
169
+ }
170
+ list.push({
171
+ id: 'merge-thresholds',
172
+ label: 'Merge thresholds',
173
+ group: 'Workspace',
174
+ icon: 'i-lucide-git-merge',
175
+ keywords: 'merge policy preset auto-merge ci',
176
+ run: () => ui.openMergeThresholds(),
177
+ })
178
+ list.push({
179
+ id: 'workspace-settings',
180
+ label: 'Workspace settings',
181
+ group: 'Workspace',
182
+ icon: 'i-lucide-sliders-horizontal',
183
+ keywords: 'limit running tasks per service waiting escalation overdue notification timeout',
184
+ run: () => ui.openWorkspaceSettings(),
185
+ })
186
+ list.push({
187
+ id: 'model-defaults',
188
+ label: 'Default models for agents',
189
+ group: 'Workspace',
190
+ icon: 'i-lucide-cpu',
191
+ keywords: 'model llm routing agent kind default',
192
+ run: () => ui.openModelDefaults(),
193
+ })
194
+ list.push({
195
+ id: 'service-fragment-defaults',
196
+ label: 'Default service best practices',
197
+ group: 'Workspace',
198
+ icon: 'i-lucide-book-open-check',
199
+ keywords: 'fragment best practice guideline service default code-aware',
200
+ run: () => ui.openServiceFragmentDefaults(),
201
+ })
202
+ list.push({
203
+ id: 'local-models',
204
+ label: 'My local runners',
205
+ group: 'Workspace',
206
+ icon: 'i-lucide-server',
207
+ keywords: 'local model runner ollama lm studio llamacpp vllm endpoint',
208
+ run: () => ui.openLocalModels(),
209
+ })
210
+
211
+ return list
212
+ })
213
+
214
+ const filtered = computed<Command[]>(() => {
215
+ const q = query.value.trim().toLowerCase()
216
+ if (!q) return commands.value
217
+ return commands.value.filter((c) =>
218
+ `${c.label} ${c.group} ${c.keywords ?? ''}`.toLowerCase().includes(q),
219
+ )
220
+ })
221
+
222
+ // Group the filtered commands for rendering, preserving first-seen group order.
223
+ const groups = computed(() => {
224
+ const map = new Map<string, Command[]>()
225
+ for (const c of filtered.value) {
226
+ const bucket = map.get(c.group)
227
+ if (bucket) bucket.push(c)
228
+ else map.set(c.group, [c])
229
+ }
230
+ return [...map.entries()].map(([name, items]) => ({ name, items }))
231
+ })
232
+
233
+ // Keep the active highlight in range as the filter narrows the list.
234
+ watch(filtered, () => {
235
+ activeIndex.value = 0
236
+ })
237
+
238
+ function run(cmd: Command) {
239
+ ui.closeCommandBar()
240
+ void cmd.run()
241
+ }
242
+
243
+ function onKeydown(event: KeyboardEvent) {
244
+ const items = filtered.value
245
+ if (event.key === 'ArrowDown') {
246
+ event.preventDefault()
247
+ activeIndex.value = (activeIndex.value + 1) % Math.max(items.length, 1)
248
+ } else if (event.key === 'ArrowUp') {
249
+ event.preventDefault()
250
+ activeIndex.value = (activeIndex.value - 1 + items.length) % Math.max(items.length, 1)
251
+ } else if (event.key === 'Enter') {
252
+ event.preventDefault()
253
+ const cmd = items[activeIndex.value]
254
+ if (cmd) run(cmd)
255
+ }
256
+ }
257
+
258
+ // Reset the query each time the bar opens, and focus the input.
259
+ const inputRef = ref<{ inputRef?: HTMLInputElement } | null>(null)
260
+ watch(open, (isOpen) => {
261
+ if (!isOpen) return
262
+ query.value = ''
263
+ activeIndex.value = 0
264
+ void documents.probe()
265
+ void tasks.probe()
266
+ void github.probe()
267
+ void library.probe()
268
+ nextTick(() => inputRef.value?.inputRef?.focus())
269
+ })
270
+
271
+ // Global ⌘K / Ctrl+K toggles the bar from anywhere in the app.
272
+ function onGlobalKey(event: KeyboardEvent) {
273
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
274
+ event.preventDefault()
275
+ ui.toggleCommandBar()
276
+ }
277
+ }
278
+ onMounted(() => window.addEventListener('keydown', onGlobalKey))
279
+ onBeforeUnmount(() => window.removeEventListener('keydown', onGlobalKey))
280
+
281
+ // Flat index of each command so per-group rendering can map to the global cursor.
282
+ function indexOf(cmd: Command) {
283
+ return filtered.value.indexOf(cmd)
284
+ }
285
+ </script>
286
+
287
+ <template>
288
+ <UModal v-model:open="open" :ui="{ content: 'max-w-xl' }">
289
+ <template #content>
290
+ <div class="flex flex-col" @keydown="onKeydown">
291
+ <div class="flex items-center gap-2 border-b border-slate-800 px-3">
292
+ <UIcon name="i-lucide-search" class="h-4 w-4 shrink-0 text-slate-500" />
293
+ <UInput
294
+ ref="inputRef"
295
+ v-model="query"
296
+ variant="none"
297
+ placeholder="Search or run a command…"
298
+ class="w-full"
299
+ :ui="{ base: 'py-3 text-sm' }"
300
+ />
301
+ <UKbd value="esc" />
302
+ </div>
303
+
304
+ <div class="max-h-80 overflow-y-auto p-1.5">
305
+ <p v-if="filtered.length === 0" class="px-3 py-6 text-center text-sm text-slate-500">
306
+ No matching commands.
307
+ </p>
308
+
309
+ <div v-for="group in groups" :key="group.name" class="mb-1">
310
+ <p
311
+ class="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
312
+ >
313
+ {{ group.name }}
314
+ </p>
315
+ <button
316
+ v-for="cmd in group.items"
317
+ :key="cmd.id"
318
+ type="button"
319
+ class="flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm transition"
320
+ :class="
321
+ indexOf(cmd) === activeIndex
322
+ ? 'bg-slate-800 text-slate-100'
323
+ : 'text-slate-300 hover:bg-slate-800/60'
324
+ "
325
+ @mousemove="activeIndex = indexOf(cmd)"
326
+ @click="run(cmd)"
327
+ >
328
+ <UIcon :name="cmd.icon" class="h-4 w-4 shrink-0 text-slate-400" />
329
+ <span class="truncate">{{ cmd.label }}</span>
330
+ </button>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </template>
335
+ </UModal>
336
+ </template>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ // Local-mode setup prompt: when the local facade boots without a GitHub PAT, every
5
+ // repo-operating agent step (clone, push, open PR, CI gate, merge) will fail. The server
6
+ // also logs this, but a dev terminal is easy to miss — so surface it in the UI with the
7
+ // (scopes-preselected) creation URL as a one-click link straight to GitHub.
8
+ const auth = useAuthStore()
9
+
10
+ const setupUrl = computed(() => auth.localMode?.githubPatSetupUrl ?? '')
11
+ const dismissed = ref(false)
12
+ const show = computed(() => !!setupUrl.value && !dismissed.value)
13
+ </script>
14
+
15
+ <template>
16
+ <Transition name="fade">
17
+ <div v-if="show" class="absolute inset-x-0 top-0 z-50 flex justify-center px-4 pt-4">
18
+ <div
19
+ class="w-full max-w-3xl rounded-2xl border-2 border-amber-500/70 bg-amber-950/95 p-5 shadow-2xl backdrop-blur"
20
+ role="alert"
21
+ >
22
+ <div class="flex items-start gap-4">
23
+ <UIcon name="i-lucide-key-round" class="mt-0.5 h-9 w-9 shrink-0 text-amber-400" />
24
+ <div class="min-w-0 flex-1">
25
+ <div class="flex items-start justify-between gap-3">
26
+ <h2 class="text-lg font-semibold text-amber-100">GitHub PAT not configured</h2>
27
+ <UButton
28
+ color="neutral"
29
+ variant="ghost"
30
+ size="xs"
31
+ icon="i-lucide-x"
32
+ aria-label="Dismiss"
33
+ @click="dismissed = true"
34
+ />
35
+ </div>
36
+ <p class="mt-1 text-sm text-amber-200/90">
37
+ Local mode reaches GitHub with a personal access token. Without one, agent steps that
38
+ clone, push, open PRs, gate on CI or merge will fail.
39
+ </p>
40
+
41
+ <div class="mt-4">
42
+ <UButton
43
+ :to="setupUrl"
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ color="warning"
47
+ variant="solid"
48
+ icon="i-lucide-external-link"
49
+ trailing
50
+ >
51
+ Create a GitHub token (scopes pre-selected)
52
+ </UButton>
53
+ <p class="mt-2 text-xs text-amber-300/70">
54
+ Then set <code class="font-mono">GITHUB_PAT</code> and restart.
55
+ </p>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </Transition>
62
+ </template>
63
+
64
+ <style scoped>
65
+ .fade-enter-active,
66
+ .fade-leave-active {
67
+ transition: opacity 0.2s ease;
68
+ }
69
+ .fade-enter-from,
70
+ .fade-leave-to {
71
+ opacity: 0;
72
+ }
73
+ </style>