@cat-factory/app 0.312.0 → 0.313.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/README.md +5 -0
- package/app/components/layout/AiProvidersBanner.vue +3 -2
- package/app/components/layout/GitHubPatBanner.vue +2 -2
- package/app/components/layout/GitHubPatPermissionsBanner.vue +4 -12
- package/app/components/layout/InfraSetupBanner.vue +6 -7
- package/app/components/setup/SetupChecklistPanel.vue +340 -0
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/useSetupChecklist.ts +122 -0
- package/app/composables/useSetupRemedies.ts +72 -0
- package/app/modular/nav-contributions.spec.ts +2 -0
- package/app/modular/nav-contributions.ts +25 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/github/context.ts +1 -0
- package/app/stores/github/probe.ts +7 -3
- package/app/stores/github.ts +9 -0
- package/app/stores/ui/modals.ts +14 -0
- package/app/utils/setupChecklist.spec.ts +184 -0
- package/app/utils/setupChecklist.ts +229 -0
- package/i18n/locales/de.json +71 -1
- package/i18n/locales/en.json +71 -1
- package/i18n/locales/es.json +71 -1
- package/i18n/locales/fr.json +71 -1
- package/i18n/locales/he.json +71 -1
- package/i18n/locales/it.json +71 -1
- package/i18n/locales/ja.json +71 -1
- package/i18n/locales/pl.json +71 -1
- package/i18n/locales/tr.json +71 -1
- package/i18n/locales/uk.json +71 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1492,6 +1492,11 @@ example ships in [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tr
|
|
|
1492
1492
|
switchers, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision queue).
|
|
1493
1493
|
- **Palettes** (`components/palettes`): drag blocks, pipelines and agents onto
|
|
1494
1494
|
the board.
|
|
1495
|
+
- **Setup checklist** (`components/setup/SetupChecklistPanel.vue`, `utils/setupChecklist.ts`):
|
|
1496
|
+
every prerequisite for "a task can run and merge" in one list, for people who can change the
|
|
1497
|
+
setup. It adds no probe of its own: each row reads what its banner or startup dialog already
|
|
1498
|
+
reads (`useSetupChecklist`) and opens the same surface. "Checking" and "missing" are separate
|
|
1499
|
+
states, because an unanswered probe and an absent capability are opposite facts.
|
|
1495
1500
|
- **Inspector** (`components/panels` + `panels/inspector`): per-block tabs:
|
|
1496
1501
|
structure, dependencies, model + fragment picker, live execution, and linked
|
|
1497
1502
|
docs/issues/scenarios. Decisions resolve via `DecisionModal`. A `review` task
|
|
@@ -8,6 +8,7 @@ import { computed } from 'vue'
|
|
|
8
8
|
|
|
9
9
|
const { t } = useI18n()
|
|
10
10
|
const ui = useUiStore()
|
|
11
|
+
const remedies = useSetupRemedies()
|
|
11
12
|
const { ready, hasUsableModel, defaultPresetBroken } = useAiReadiness()
|
|
12
13
|
|
|
13
14
|
const showSetup = computed(() => ready.value && !hasUsableModel.value && !ui.aiSetupDismissed)
|
|
@@ -47,7 +48,7 @@ const show = computed(() => showSetup.value || showPreset.value)
|
|
|
47
48
|
{{ t('layout.aiProvidersBanner.setup.body') }}
|
|
48
49
|
</p>
|
|
49
50
|
<div class="mt-4">
|
|
50
|
-
<UButton color="warning" icon="i-lucide-settings" @click="
|
|
51
|
+
<UButton color="warning" icon="i-lucide-settings" @click="remedies.openModelSetup()">
|
|
51
52
|
{{ t('layout.aiProvidersBanner.setup.action') }}
|
|
52
53
|
</UButton>
|
|
53
54
|
</div>
|
|
@@ -88,7 +89,7 @@ const show = computed(() => showSetup.value || showPreset.value)
|
|
|
88
89
|
size="sm"
|
|
89
90
|
color="warning"
|
|
90
91
|
icon="i-lucide-cpu"
|
|
91
|
-
@click="
|
|
92
|
+
@click="remedies.openPresetMismatch()"
|
|
92
93
|
>
|
|
93
94
|
{{ t('layout.aiProvidersBanner.preset.action') }}
|
|
94
95
|
</UButton>
|
|
@@ -7,9 +7,9 @@ const { t } = useI18n()
|
|
|
7
7
|
// repo-operating agent step (clone, push, open PR, CI gate, merge) will fail. The server
|
|
8
8
|
// also logs this, but a dev terminal is easy to miss — so surface it in the UI with the
|
|
9
9
|
// (scopes-preselected) creation URL as a one-click link straight to GitHub.
|
|
10
|
-
const
|
|
10
|
+
const { patSetupUrl } = useSetupRemedies()
|
|
11
11
|
|
|
12
|
-
const setupUrl = computed(() =>
|
|
12
|
+
const setupUrl = computed(() => patSetupUrl.value ?? '')
|
|
13
13
|
const dismissed = ref(false)
|
|
14
14
|
const show = computed(() => !!setupUrl.value && !dismissed.value)
|
|
15
15
|
</script>
|
|
@@ -3,11 +3,9 @@ import { computed, ref } from 'vue'
|
|
|
3
3
|
import {
|
|
4
4
|
GITHUB_PAT_FINE_GRAINED_PERMISSIONS,
|
|
5
5
|
githubPatCheckNeedsAttention,
|
|
6
|
-
githubPatCheckSource,
|
|
7
6
|
missingGitHubPatCapabilities,
|
|
8
7
|
} from '@cat-factory/contracts'
|
|
9
|
-
import type { GitHubPatCapability
|
|
10
|
-
import { githubPatRemintUrl } from '~/utils/vcs'
|
|
8
|
+
import type { GitHubPatCapability } from '~/types/domain'
|
|
11
9
|
|
|
12
10
|
// The token a run would authenticate with cannot do what the pipeline needs.
|
|
13
11
|
//
|
|
@@ -67,14 +65,8 @@ function capabilityLabel(capability: GitHubPatCapability): string {
|
|
|
67
65
|
return t(CAPABILITY_KEYS[capability])
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* back to `unknown` — which lands on the classic form, the only one GitHub lets us pre-fill.
|
|
73
|
-
*/
|
|
74
|
-
const kind = computed<GitHubPatKind>(() => report.value?.kind ?? 'unknown')
|
|
75
|
-
const remintUrl = computed(() =>
|
|
76
|
-
githubPatRemintUrl(kind.value, report.value?.webUrl ?? github.connection?.webUrl),
|
|
77
|
-
)
|
|
68
|
+
// The re-mint page and whose token it is: the map the setup checklist opens through too.
|
|
69
|
+
const { patRemintUrl: remintUrl, patSource, patKind: kind } = useSetupRemedies()
|
|
78
70
|
|
|
79
71
|
/**
|
|
80
72
|
* Who has to act. A deployment token is replaced by whoever runs the deployment (local mode:
|
|
@@ -89,7 +81,7 @@ const remintUrl = computed(() =>
|
|
|
89
81
|
* personal settings: the exact misrouting the wire field exists to prevent.
|
|
90
82
|
*/
|
|
91
83
|
const sourceKey = computed(() =>
|
|
92
|
-
|
|
84
|
+
patSource.value === 'deployment'
|
|
93
85
|
? 'layout.githubPatPermissionsBanner.sourceDeployment'
|
|
94
86
|
: 'layout.githubPatPermissionsBanner.sourceInitiator',
|
|
95
87
|
)
|
|
@@ -65,6 +65,8 @@ const ui = useUiStore()
|
|
|
65
65
|
const auth = useAuthStore()
|
|
66
66
|
const workspace = useWorkspaceStore()
|
|
67
67
|
const { actor } = useSetupActor()
|
|
68
|
+
// Where each area is configured: the map the setup checklist opens through too.
|
|
69
|
+
const remedies = useSetupRemedies()
|
|
68
70
|
|
|
69
71
|
// Severity order: no executor blocks EVERY agent, so it leads; a missing test environment blocks
|
|
70
72
|
// only testing agents; missing storage only degrades the UI-tester's screenshots.
|
|
@@ -73,32 +75,29 @@ const AREAS: InfraSetupArea[] = ['agentExecutor', 'ephemeralEnvironments', 'bina
|
|
|
73
75
|
// Exhaustive per-area presentation (an exhaustive `Record<InfraSetupArea, …>`, so adding an area
|
|
74
76
|
// without a meta entry fails typecheck — the tier-2 guard). The i18n keys are resolved
|
|
75
77
|
// dynamically (`t(AREA_META[area].titleKey)`), so tier-1 typed-key checking doesn't cover them;
|
|
76
|
-
// the `i18n:check` drift guard (tier 3) catches any that are absent from the catalog.
|
|
77
|
-
//
|
|
78
|
+
// the `i18n:check` drift guard (tier 3) catches any that are absent from the catalog. The action
|
|
79
|
+
// opens the area's setup surface through `useSetupRemedies`.
|
|
78
80
|
const AREA_META: Record<
|
|
79
81
|
InfraSetupArea,
|
|
80
|
-
{ icon: string; titleKey: string; bodyKey: string; actionKey: string
|
|
82
|
+
{ icon: string; titleKey: string; bodyKey: string; actionKey: string }
|
|
81
83
|
> = {
|
|
82
84
|
agentExecutor: {
|
|
83
85
|
icon: 'i-lucide-server-cog',
|
|
84
86
|
titleKey: 'layout.infraSetupBanner.agentExecutor.title',
|
|
85
87
|
bodyKey: 'layout.infraSetupBanner.agentExecutor.body',
|
|
86
88
|
actionKey: 'layout.infraSetupBanner.agentExecutor.action',
|
|
87
|
-
onConfigure: () => ui.openProviderConnection('runner-pool'),
|
|
88
89
|
},
|
|
89
90
|
ephemeralEnvironments: {
|
|
90
91
|
icon: 'i-lucide-flask-conical',
|
|
91
92
|
titleKey: 'layout.infraSetupBanner.ephemeralEnvironments.title',
|
|
92
93
|
bodyKey: 'layout.infraSetupBanner.ephemeralEnvironments.body',
|
|
93
94
|
actionKey: 'layout.infraSetupBanner.ephemeralEnvironments.action',
|
|
94
|
-
onConfigure: () => ui.openProviderConnection('environment'),
|
|
95
95
|
},
|
|
96
96
|
binaryStorage: {
|
|
97
97
|
icon: 'i-lucide-hard-drive',
|
|
98
98
|
titleKey: 'layout.infraSetupBanner.binaryStorage.title',
|
|
99
99
|
bodyKey: 'layout.infraSetupBanner.binaryStorage.body',
|
|
100
100
|
actionKey: 'layout.infraSetupBanner.binaryStorage.action',
|
|
101
|
-
onConfigure: () => ui.openContentStorageSettings(),
|
|
102
101
|
},
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -291,7 +290,7 @@ function dismissMenu(card: Pick<AreaCard, 'area' | 'kind'>): DropdownMenuItem[][
|
|
|
291
290
|
:color="card.kind === 'outage' ? 'error' : 'warning'"
|
|
292
291
|
icon="i-lucide-settings"
|
|
293
292
|
:data-testid="`infra-setup-configure-${card.area}`"
|
|
294
|
-
@click="
|
|
293
|
+
@click="remedies.openInfraArea(card.area)"
|
|
295
294
|
>
|
|
296
295
|
{{
|
|
297
296
|
card.kind === 'outage'
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SetupRow, SetupRowCause, SetupRowId, SetupRowStatus } from '~/utils/setupChecklist'
|
|
3
|
+
import type { SetupRemedy } from '~/utils/setupAudience'
|
|
4
|
+
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
5
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The setup checklist: every prerequisite for "a task can run and merge", its state, and one
|
|
9
|
+
* action per row (issue #2258).
|
|
10
|
+
*
|
|
11
|
+
* The probes and the surfaces each row opens are the ones the setup banners, the AI-provider
|
|
12
|
+
* dialogs and the startup health advisories already use (`useSetupChecklist`), and so is the rule
|
|
13
|
+
* for who may act: a row the caller cannot fix shows who can instead of a button. What it adds is
|
|
14
|
+
* the whole list in one place, with a finish line, instead of one interruption per missing piece.
|
|
15
|
+
* Opening a row's surface closes the checklist, so the two never stack.
|
|
16
|
+
*/
|
|
17
|
+
const { t } = useI18n()
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const { checklist, actionFor, refresh, patSource, patMissing } = useSetupChecklist()
|
|
20
|
+
|
|
21
|
+
const open = computed({
|
|
22
|
+
get: () => ui.setupChecklistOpen,
|
|
23
|
+
set: (v: boolean) => (v ? ui.openSetupChecklist() : ui.closeSetupChecklist()),
|
|
24
|
+
})
|
|
25
|
+
watch(
|
|
26
|
+
() => ui.setupChecklistOpen,
|
|
27
|
+
(isOpen) => {
|
|
28
|
+
if (isOpen) refresh()
|
|
29
|
+
},
|
|
30
|
+
{ immediate: true },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/** Open a surface from the checklist: close the checklist first, so the two never stack. */
|
|
34
|
+
function go(fn: () => void) {
|
|
35
|
+
ui.closeSetupChecklist()
|
|
36
|
+
fn()
|
|
37
|
+
}
|
|
38
|
+
function act(row: SetupRow) {
|
|
39
|
+
const action = actionFor(row)
|
|
40
|
+
if (!action) return
|
|
41
|
+
// Asking a probe again keeps the checklist open: its answer lands here.
|
|
42
|
+
if (row.cause === 'probeFailed') action()
|
|
43
|
+
else go(action)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The rows this caller can act on, with the action to show. */
|
|
47
|
+
function canAct(row: SetupRow): boolean {
|
|
48
|
+
return row.remedy === null && actionFor(row) !== null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The ONE solid button in the view: the first required row that is not done and that this caller
|
|
53
|
+
* can act on. Every other open row takes a lighter variant, so the list reads in order.
|
|
54
|
+
*/
|
|
55
|
+
const primaryRowId = computed<SetupRowId | null>(
|
|
56
|
+
() =>
|
|
57
|
+
checklist.value.rows.find(
|
|
58
|
+
(r) => r.required && (r.status === 'missing' || r.status === 'attention') && canAct(r),
|
|
59
|
+
)?.id ?? null,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const pipelineHealth = usePipelineHealth()
|
|
63
|
+
const riskPolicyHealth = useRiskPolicyHealth()
|
|
64
|
+
const modelPresetHealth = useModelPresetHealth()
|
|
65
|
+
|
|
66
|
+
const STATUS_META: Record<SetupRowStatus, { icon: string; class: string }> = {
|
|
67
|
+
done: { icon: 'i-lucide-circle-check', class: 'text-success' },
|
|
68
|
+
missing: { icon: 'i-lucide-circle', class: 'text-dimmed' },
|
|
69
|
+
attention: { icon: 'i-lucide-circle-alert', class: 'text-warning' },
|
|
70
|
+
checking: { icon: 'i-lucide-loader-circle', class: 'text-dimmed' },
|
|
71
|
+
not_needed: { icon: 'i-lucide-circle-minus', class: 'text-dimmed' },
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The action's word per state. Null for the two states that never carry an action
|
|
76
|
+
* (`useSetupChecklist.actionFor`), so no copy ships for a button that cannot render.
|
|
77
|
+
*/
|
|
78
|
+
const ACTION_KEYS: Record<SetupRowStatus, string | null> = {
|
|
79
|
+
done: 'setup.action.change',
|
|
80
|
+
missing: 'setup.action.connect',
|
|
81
|
+
attention: 'setup.action.fix',
|
|
82
|
+
checking: null,
|
|
83
|
+
not_needed: null,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function actionLabel(row: SetupRow): string {
|
|
87
|
+
if (row.cause === 'probeFailed') return t('setup.action.retry')
|
|
88
|
+
const key = ACTION_KEYS[row.status]
|
|
89
|
+
return key ? t(key) : ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Whose token a token row is about: the remedy differs, so the row says it, as the banner does. */
|
|
93
|
+
const patSourceKey = computed(() =>
|
|
94
|
+
patSource.value === 'deployment'
|
|
95
|
+
? 'layout.githubPatPermissionsBanner.sourceDeployment'
|
|
96
|
+
: 'layout.githubPatPermissionsBanner.sourceInitiator',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Each row's title and its line for the states that are about the row. An `attention` row's line
|
|
101
|
+
* is its CAUSE (`CAUSE_KEYS`): the title already names the area, and each cause needs its own fix.
|
|
102
|
+
*/
|
|
103
|
+
const ROW_COPY: Record<
|
|
104
|
+
SetupRowId,
|
|
105
|
+
{ title: string } & Record<Exclude<SetupRowStatus, 'attention'>, string>
|
|
106
|
+
> = {
|
|
107
|
+
model: {
|
|
108
|
+
title: 'setup.rows.model.title',
|
|
109
|
+
done: 'setup.rows.model.done',
|
|
110
|
+
missing: 'setup.rows.model.missing',
|
|
111
|
+
checking: 'setup.rows.checking',
|
|
112
|
+
not_needed: 'setup.rows.notNeeded',
|
|
113
|
+
},
|
|
114
|
+
sourceControl: {
|
|
115
|
+
title: 'setup.rows.sourceControl.title',
|
|
116
|
+
done: 'setup.rows.sourceControl.done',
|
|
117
|
+
missing: 'setup.rows.sourceControl.missing',
|
|
118
|
+
checking: 'setup.rows.checking',
|
|
119
|
+
not_needed: 'setup.rows.notNeeded',
|
|
120
|
+
},
|
|
121
|
+
agentExecutor: {
|
|
122
|
+
title: 'setup.rows.agentExecutor.title',
|
|
123
|
+
done: 'setup.rows.agentExecutor.done',
|
|
124
|
+
missing: 'setup.rows.agentExecutor.missing',
|
|
125
|
+
checking: 'setup.rows.checking',
|
|
126
|
+
not_needed: 'setup.rows.agentExecutor.notNeeded',
|
|
127
|
+
},
|
|
128
|
+
ephemeralEnvironments: {
|
|
129
|
+
title: 'setup.rows.ephemeralEnvironments.title',
|
|
130
|
+
done: 'setup.rows.ephemeralEnvironments.done',
|
|
131
|
+
missing: 'setup.rows.ephemeralEnvironments.missing',
|
|
132
|
+
checking: 'setup.rows.checking',
|
|
133
|
+
not_needed: 'setup.rows.notNeeded',
|
|
134
|
+
},
|
|
135
|
+
binaryStorage: {
|
|
136
|
+
title: 'setup.rows.binaryStorage.title',
|
|
137
|
+
done: 'setup.rows.binaryStorage.done',
|
|
138
|
+
missing: 'setup.rows.binaryStorage.missing',
|
|
139
|
+
checking: 'setup.rows.checking',
|
|
140
|
+
not_needed: 'setup.rows.notNeeded',
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
const CAUSE_KEYS: Record<SetupRowCause, string> = {
|
|
144
|
+
unreachable: 'setup.causes.unreachable',
|
|
145
|
+
incomplete: 'setup.causes.incomplete',
|
|
146
|
+
rejected: 'setup.causes.rejected',
|
|
147
|
+
underscoped: 'setup.causes.underscoped',
|
|
148
|
+
presetBroken: 'setup.causes.presetBroken',
|
|
149
|
+
probeFailed: 'setup.causes.probeFailed',
|
|
150
|
+
notReported: 'setup.causes.notReported',
|
|
151
|
+
}
|
|
152
|
+
function rowLine(row: SetupRow): string {
|
|
153
|
+
return row.status === 'attention' ? t(CAUSE_KEYS[row.cause]) : t(ROW_COPY[row.id][row.status])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The same "who can fix this" lines the infrastructure notice shows a caller who cannot act.
|
|
157
|
+
const REMEDY_KEYS: Record<SetupRemedy, string> = {
|
|
158
|
+
workspace_admin: 'layout.infraSetupNotice.owner.workspace_admin',
|
|
159
|
+
account_admin: 'layout.infraSetupNotice.owner.account_admin',
|
|
160
|
+
operator: 'layout.infraSetupNotice.owner.operator',
|
|
161
|
+
full_surface: 'layout.infraSetupNotice.fullSurface',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The secondary list: advisories that used to open as startup dialogs, each count on its own so
|
|
166
|
+
* no sentence has to agree with a number. Only rows with work.
|
|
167
|
+
*/
|
|
168
|
+
const also = computed(() =>
|
|
169
|
+
[
|
|
170
|
+
{
|
|
171
|
+
id: 'pipelines',
|
|
172
|
+
labelKey: 'setup.also.pipelines',
|
|
173
|
+
counts: [
|
|
174
|
+
{
|
|
175
|
+
key: 'setup.also.counts.repair',
|
|
176
|
+
n:
|
|
177
|
+
pipelineHealth.health.value.filter((h) => h.invalid).length +
|
|
178
|
+
pipelineHealth.retired.value.length,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: 'setup.also.counts.outdated',
|
|
182
|
+
n: pipelineHealth.health.value.filter((h) => !h.invalid && h.outdated).length,
|
|
183
|
+
},
|
|
184
|
+
{ key: 'setup.also.counts.new', n: pipelineHealth.newPipelines.value.length },
|
|
185
|
+
],
|
|
186
|
+
open: () => go(() => ui.openPipelineHealth()),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'riskPolicies',
|
|
190
|
+
labelKey: 'setup.also.riskPolicies',
|
|
191
|
+
counts: [
|
|
192
|
+
{ key: 'setup.also.counts.outdated', n: riskPolicyHealth.outdated.value.length },
|
|
193
|
+
{ key: 'setup.also.counts.new', n: riskPolicyHealth.newPresets.value.length },
|
|
194
|
+
],
|
|
195
|
+
open: () => go(() => ui.openRiskPolicyHealth()),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'modelPresets',
|
|
199
|
+
labelKey: 'setup.also.modelPresets',
|
|
200
|
+
counts: [
|
|
201
|
+
{ key: 'setup.also.counts.outdated', n: modelPresetHealth.outdated.value.length },
|
|
202
|
+
{ key: 'setup.also.counts.new', n: modelPresetHealth.newPresets.value.length },
|
|
203
|
+
],
|
|
204
|
+
open: () => go(() => ui.openModelPresetHealth()),
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
.map((row) => ({ ...row, counts: row.counts.filter((c) => c.n > 0) }))
|
|
208
|
+
.filter((row) => row.counts.length > 0),
|
|
209
|
+
)
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<template>
|
|
213
|
+
<UModal v-model:open="open" :title="t('setup.title')" :ui="{ content: 'sm:max-w-3xl' }">
|
|
214
|
+
<template #title>
|
|
215
|
+
<span class="flex items-baseline gap-3">
|
|
216
|
+
{{ t('setup.title') }}
|
|
217
|
+
<!-- While a probe has not answered, the count is not one yet, so the badge says so. -->
|
|
218
|
+
<UBadge color="neutral" variant="subtle" data-testid="setup-progress">
|
|
219
|
+
{{
|
|
220
|
+
checklist.checking
|
|
221
|
+
? t('setup.rows.checking')
|
|
222
|
+
: t('setup.progress', { done: checklist.done, total: checklist.total })
|
|
223
|
+
}}
|
|
224
|
+
</UBadge>
|
|
225
|
+
</span>
|
|
226
|
+
</template>
|
|
227
|
+
<template #body>
|
|
228
|
+
<div data-testid="setup-checklist">
|
|
229
|
+
<p class="text-sm text-muted">{{ t('setup.intro') }}</p>
|
|
230
|
+
|
|
231
|
+
<ul class="mt-6 space-y-3">
|
|
232
|
+
<li
|
|
233
|
+
v-for="row in checklist.rows"
|
|
234
|
+
:key="row.id"
|
|
235
|
+
class="flex items-center gap-3 rounded-lg border border-muted bg-default p-4"
|
|
236
|
+
data-testid="setup-row"
|
|
237
|
+
:data-row="row.id"
|
|
238
|
+
:data-status="row.status"
|
|
239
|
+
:data-cause="row.cause ?? undefined"
|
|
240
|
+
:data-remedy="row.remedy ?? undefined"
|
|
241
|
+
>
|
|
242
|
+
<Spinner
|
|
243
|
+
:name="STATUS_META[row.status].icon"
|
|
244
|
+
:spinning="row.status === 'checking'"
|
|
245
|
+
class="h-5 w-5 shrink-0"
|
|
246
|
+
:class="STATUS_META[row.status].class"
|
|
247
|
+
/>
|
|
248
|
+
<div class="min-w-0 flex-1">
|
|
249
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
250
|
+
<span class="font-medium text-highlighted">{{ t(ROW_COPY[row.id].title) }}</span>
|
|
251
|
+
<UBadge v-if="row.status !== 'done'" size="sm" color="neutral" variant="outline">
|
|
252
|
+
{{ row.required ? t('setup.required') : t('setup.optional') }}
|
|
253
|
+
</UBadge>
|
|
254
|
+
</div>
|
|
255
|
+
<p class="mt-0.5 text-xs text-muted">{{ rowLine(row) }}</p>
|
|
256
|
+
<!-- The token rows carry the banners' second half: whose token it is, and, for a
|
|
257
|
+
missing local token, where the new one goes. -->
|
|
258
|
+
<p
|
|
259
|
+
v-if="row.cause === 'rejected' || row.cause === 'underscoped'"
|
|
260
|
+
class="mt-0.5 text-xs text-dimmed"
|
|
261
|
+
>
|
|
262
|
+
{{ t(patSourceKey) }}
|
|
263
|
+
</p>
|
|
264
|
+
<p
|
|
265
|
+
v-else-if="row.id === 'sourceControl' && row.status === 'missing' && patMissing"
|
|
266
|
+
class="mt-0.5 text-xs text-dimmed"
|
|
267
|
+
>
|
|
268
|
+
<i18n-t keypath="layout.githubPatBanner.thenSet" tag="span" scope="global">
|
|
269
|
+
<template #envVar>
|
|
270
|
+
<code class="font-mono">GITHUB_PAT</code>
|
|
271
|
+
</template>
|
|
272
|
+
</i18n-t>
|
|
273
|
+
</p>
|
|
274
|
+
<!-- A gap this caller cannot close: who can, in place of a button their save would
|
|
275
|
+
be refused on. A done row needs neither. -->
|
|
276
|
+
<p
|
|
277
|
+
v-if="row.remedy && row.status !== 'done' && row.status !== 'not_needed'"
|
|
278
|
+
class="mt-0.5 text-xs text-dimmed"
|
|
279
|
+
data-testid="setup-row-remedy"
|
|
280
|
+
>
|
|
281
|
+
{{ t(REMEDY_KEYS[row.remedy]) }}
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
<UButton
|
|
285
|
+
v-if="canAct(row)"
|
|
286
|
+
size="sm"
|
|
287
|
+
:color="row.status === 'done' ? 'neutral' : 'primary'"
|
|
288
|
+
:variant="
|
|
289
|
+
row.id === primaryRowId ? 'solid' : row.status === 'done' ? 'outline' : 'soft'
|
|
290
|
+
"
|
|
291
|
+
data-testid="setup-row-action"
|
|
292
|
+
@click="act(row)"
|
|
293
|
+
>
|
|
294
|
+
{{ actionLabel(row) }}
|
|
295
|
+
</UButton>
|
|
296
|
+
</li>
|
|
297
|
+
</ul>
|
|
298
|
+
|
|
299
|
+
<USeparator class="my-6" />
|
|
300
|
+
<SectionLabel as="h2" class="mb-3">{{ t('setup.also.title') }}</SectionLabel>
|
|
301
|
+
<ul class="space-y-2 text-sm">
|
|
302
|
+
<li v-for="row in also" :key="row.id" class="flex flex-wrap items-center gap-2">
|
|
303
|
+
<span class="text-muted">{{ t(row.labelKey) }}</span>
|
|
304
|
+
<UBadge v-for="c in row.counts" :key="c.key" size="sm" color="neutral" variant="subtle">
|
|
305
|
+
{{ t(c.key, { count: c.n }) }}
|
|
306
|
+
</UBadge>
|
|
307
|
+
<UButton
|
|
308
|
+
class="ms-auto"
|
|
309
|
+
size="xs"
|
|
310
|
+
color="neutral"
|
|
311
|
+
variant="outline"
|
|
312
|
+
@click="row.open()"
|
|
313
|
+
>
|
|
314
|
+
{{ t('setup.action.review') }}
|
|
315
|
+
</UButton>
|
|
316
|
+
</li>
|
|
317
|
+
<li class="flex flex-wrap items-center gap-2 text-muted">
|
|
318
|
+
<span class="flex-1">{{ t('setup.also.settings') }}</span>
|
|
319
|
+
<UButton
|
|
320
|
+
size="xs"
|
|
321
|
+
color="neutral"
|
|
322
|
+
variant="ghost"
|
|
323
|
+
@click="go(() => ui.openWorkspaceSettings())"
|
|
324
|
+
>
|
|
325
|
+
{{ t('setup.also.workspaceSettings') }}
|
|
326
|
+
</UButton>
|
|
327
|
+
<UButton
|
|
328
|
+
size="xs"
|
|
329
|
+
color="neutral"
|
|
330
|
+
variant="ghost"
|
|
331
|
+
@click="go(() => ui.openIntegrations())"
|
|
332
|
+
>
|
|
333
|
+
{{ t('setup.also.integrations') }}
|
|
334
|
+
</UButton>
|
|
335
|
+
</li>
|
|
336
|
+
</ul>
|
|
337
|
+
</div>
|
|
338
|
+
</template>
|
|
339
|
+
</UModal>
|
|
340
|
+
</template>
|
|
@@ -55,6 +55,7 @@ export function useNavContributions() {
|
|
|
55
55
|
foundationalServices: () => ui.openFoundationalServices(),
|
|
56
56
|
mergeThresholds: () => ui.openWorkspaceSettings('merge'),
|
|
57
57
|
workspaceSettings: () => ui.openWorkspaceSettings(),
|
|
58
|
+
setupChecklist: () => ui.openSetupChecklist(),
|
|
58
59
|
modelConfiguration: () => ui.openModelConfig(),
|
|
59
60
|
serviceFragmentDefaults: () => ui.openWorkspaceSettings('fragments'),
|
|
60
61
|
localModels: () => ui.openLocalModels(),
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
import { githubPatCheckNeedsAttention } from '@cat-factory/contracts'
|
|
3
|
+
import { deriveSetupChecklist, type SetupRow } from '~/utils/setupChecklist'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The store-facing half of `utils/setupChecklist.ts`: resolve every probe the checklist reads,
|
|
7
|
+
* and the existing surface each row's action opens.
|
|
8
|
+
*
|
|
9
|
+
* Every action goes to the surface the matching banner or dialog opens for the same state, so
|
|
10
|
+
* the checklist is a new index over the setup the product has, never a second copy of it.
|
|
11
|
+
*/
|
|
12
|
+
export function useSetupChecklist() {
|
|
13
|
+
const ui = useUiStore()
|
|
14
|
+
const github = useGitHubStore()
|
|
15
|
+
const models = useModelsStore()
|
|
16
|
+
const workspace = useWorkspaceStore()
|
|
17
|
+
const providerConnections = useProviderConnectionsStore()
|
|
18
|
+
const ai = useAiReadiness()
|
|
19
|
+
const { actor } = useSetupActor()
|
|
20
|
+
// Where each gap is fixed: the map the setup banners open through too.
|
|
21
|
+
const remedies = useSetupRemedies()
|
|
22
|
+
/** Local mode with no token: the row says where the new one goes, as the banner does. */
|
|
23
|
+
const patMissing = computed(() => !!remedies.patSetupUrl.value)
|
|
24
|
+
|
|
25
|
+
// The catalog is loaded on board open, and a failed load is dropped there on purpose (it stays
|
|
26
|
+
// retryable, see `models.prefetchForBoard`). The checklist is where that failure is stated, so
|
|
27
|
+
// it asks for the catalog itself and keeps the outcome.
|
|
28
|
+
const modelLoadFailed = ref(false)
|
|
29
|
+
async function loadModels() {
|
|
30
|
+
const id = workspace.workspaceId
|
|
31
|
+
if (!id) return
|
|
32
|
+
modelLoadFailed.value = false
|
|
33
|
+
try {
|
|
34
|
+
await models.ensureLoaded(id)
|
|
35
|
+
} catch {
|
|
36
|
+
modelLoadFailed.value = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ask every probe the checklist reads, so a row does not wait on a banner that happens to be
|
|
42
|
+
* mounted. Each is single-flighted or load-once in its store, so this costs nothing when the
|
|
43
|
+
* board already asked.
|
|
44
|
+
*/
|
|
45
|
+
function refresh() {
|
|
46
|
+
void loadModels()
|
|
47
|
+
void providerConnections.ensureLoaded()
|
|
48
|
+
void github.ensureProbed()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const patCheck = computed(() => github.patCheck)
|
|
52
|
+
const patProblem = computed<'rejected' | 'underscoped' | null>(() => {
|
|
53
|
+
const check = patCheck.value
|
|
54
|
+
if (!check || !githubPatCheckNeedsAttention(check)) return null
|
|
55
|
+
return check.state === 'token_rejected' ? 'rejected' : 'underscoped'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const checklist = computed(() =>
|
|
59
|
+
deriveSetupChecklist({
|
|
60
|
+
model: {
|
|
61
|
+
loaded: ai.ready.value,
|
|
62
|
+
loadFailed: modelLoadFailed.value,
|
|
63
|
+
usable: ai.hasUsableModel.value,
|
|
64
|
+
presetBroken: ai.defaultPresetBroken.value,
|
|
65
|
+
},
|
|
66
|
+
sourceControl: {
|
|
67
|
+
available: github.available,
|
|
68
|
+
unavailableCause: github.unavailableCause,
|
|
69
|
+
connected: github.connected,
|
|
70
|
+
patMissing: patMissing.value,
|
|
71
|
+
patProblem: patProblem.value,
|
|
72
|
+
},
|
|
73
|
+
infra: workspace.infraSetup ?? null,
|
|
74
|
+
workspaceLoaded: workspace.ready,
|
|
75
|
+
infraOwners: workspace.infraSetupOwners ?? null,
|
|
76
|
+
providers: {
|
|
77
|
+
loaded: providerConnections.loaded,
|
|
78
|
+
needingConfig: providerConnections.needingConfig,
|
|
79
|
+
},
|
|
80
|
+
actor: actor.value,
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Where a row's action goes, by row AND state, through the remedies the matching banner uses.
|
|
86
|
+
* Null where there is nothing to open: the deployment does not use the area, or does not report
|
|
87
|
+
* it.
|
|
88
|
+
*/
|
|
89
|
+
function actionFor(row: SetupRow): (() => void) | null {
|
|
90
|
+
if (row.status === 'not_needed' || row.status === 'checking') return null
|
|
91
|
+
if (row.cause === 'notReported') return null
|
|
92
|
+
switch (row.id) {
|
|
93
|
+
case 'model':
|
|
94
|
+
if (row.cause === 'probeFailed') return () => void loadModels()
|
|
95
|
+
if (row.status === 'missing') return remedies.openModelSetup
|
|
96
|
+
if (row.cause === 'presetBroken') return remedies.openPresetMismatch
|
|
97
|
+
return () => ui.openModelProviders()
|
|
98
|
+
case 'sourceControl': {
|
|
99
|
+
const setupUrl = remedies.patSetupUrl.value
|
|
100
|
+
if (setupUrl) return () => window.open(setupUrl, '_blank', 'noopener')
|
|
101
|
+
if (row.cause === 'rejected' || row.cause === 'underscoped') {
|
|
102
|
+
const remint = remedies.patRemintUrl.value
|
|
103
|
+
return () => window.open(remint, '_blank', 'noopener')
|
|
104
|
+
}
|
|
105
|
+
if (row.cause === 'probeFailed') return () => void github.probe()
|
|
106
|
+
return () => ui.openGitHub()
|
|
107
|
+
}
|
|
108
|
+
case 'agentExecutor':
|
|
109
|
+
case 'ephemeralEnvironments':
|
|
110
|
+
case 'binaryStorage': {
|
|
111
|
+
const area = row.id
|
|
112
|
+
return () => remedies.openInfraArea(area)
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
const unreachable: never = row.id
|
|
116
|
+
return unreachable
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { checklist, actionFor, refresh, patSource: remedies.patSource, patMissing }
|
|
122
|
+
}
|