@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,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>
|