@cat-factory/app 0.312.0 → 0.314.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 +12 -0
- package/app/components/board/AddTaskModal.vue +29 -30
- package/app/components/board/ReviewFrictionDialog.vue +8 -5
- 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/pipeline/PipelinePicker.vue +8 -2
- package/app/components/queue/DescribeWorkModal.vue +367 -0
- package/app/components/queue/QueueView.vue +18 -0
- package/app/components/setup/SetupChecklistPanel.vue +340 -0
- package/app/composables/useNavContributions.ts +2 -0
- package/app/composables/useSetupChecklist.ts +122 -0
- package/app/composables/useSetupRemedies.ts +72 -0
- package/app/composables/useTaskActions.ts +51 -28
- package/app/modular/nav-contributions.spec.ts +3 -0
- package/app/modular/nav-contributions.ts +46 -0
- package/app/pages/index.vue +6 -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 +42 -0
- package/app/utils/describeWork.spec.ts +76 -0
- package/app/utils/describeWork.ts +71 -0
- package/app/utils/reviewFriction.ts +47 -0
- package/app/utils/setupChecklist.spec.ts +184 -0
- package/app/utils/setupChecklist.ts +229 -0
- package/i18n/locales/de.json +97 -3
- package/i18n/locales/en.json +97 -3
- package/i18n/locales/es.json +97 -3
- package/i18n/locales/fr.json +97 -3
- package/i18n/locales/he.json +97 -3
- package/i18n/locales/it.json +97 -3
- package/i18n/locales/ja.json +97 -3
- package/i18n/locales/pl.json +97 -3
- package/i18n/locales/tr.json +97 -3
- package/i18n/locales/uk.json +97 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1492,6 +1492,18 @@ 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
|
+
- **Describe new work** (`components/queue/DescribeWorkModal.vue`, `utils/describeWork.ts`): the
|
|
1496
|
+
one-sentence intake, from Create, the palette and the queue header. It files a feature or a bug
|
|
1497
|
+
from a sentence and a service, titled by the first sentence; the overrides sit behind one
|
|
1498
|
+
disclosure, and the merge policy and model preset follow the full form's tier rule
|
|
1499
|
+
(`showOverrideField`), so basic mode and the designer role see the pipeline only.
|
|
1500
|
+
It rebuilds no other kind: "More kinds" and "Attach context" hand the text to `AddTaskModal`,
|
|
1501
|
+
which stays the one home of those rules. Both share `utils/reviewFriction.ts` for the review-debt 409.
|
|
1502
|
+
- **Setup checklist** (`components/setup/SetupChecklistPanel.vue`, `utils/setupChecklist.ts`):
|
|
1503
|
+
every prerequisite for "a task can run and merge" in one list, for people who can change the
|
|
1504
|
+
setup. It adds no probe of its own: each row reads what its banner or startup dialog already
|
|
1505
|
+
reads (`useSetupChecklist`) and opens the same surface. "Checking" and "missing" are separate
|
|
1506
|
+
states, because an unanswered probe and an absent capability are opposite facts.
|
|
1495
1507
|
- **Inspector** (`components/panels` + `panels/inspector`): per-block tabs:
|
|
1496
1508
|
structure, dependencies, model + fragment picker, live execution, and linked
|
|
1497
1509
|
docs/issues/scenarios. Decisions resolve via `DecisionModal`. A `review` task
|
|
@@ -36,6 +36,7 @@ import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
|
36
36
|
import ReviewSkillQueue from '~/components/skills/ReviewSkillQueue.vue'
|
|
37
37
|
import RiskPolicyPicker from '~/components/riskPolicy/RiskPolicyPicker.vue'
|
|
38
38
|
import { parseConflict } from '~/composables/usePipelineErrorToast'
|
|
39
|
+
import { isReviewDebtConflict, reviewFrictionContext } from '~/utils/reviewFriction'
|
|
39
40
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
40
41
|
import type { ReviewTargetReason } from '@cat-factory/contracts'
|
|
41
42
|
import {
|
|
@@ -526,6 +527,8 @@ function defaultPipelineIdFor(type: TaskTypeChoice): string {
|
|
|
526
527
|
return offered ? preset : ''
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
// Set while a prefill's type is being seeded with a carried pipeline (see `onModalOpen` below).
|
|
531
|
+
let seedingPrefill = false
|
|
529
532
|
watch(taskType, (next) => {
|
|
530
533
|
const custom = customTaskTypes.value.find((tt) => tt.taskType === next)
|
|
531
534
|
// A custom type owns a fresh field bag on every switch (its descriptors differ per type), seeded
|
|
@@ -533,6 +536,10 @@ watch(taskType, (next) => {
|
|
|
533
536
|
customFieldValues.value = descriptorFieldDefaults(custom?.fields ?? [])
|
|
534
537
|
// An unresolvable preset leaves the current selection alone rather than blanking it: a type
|
|
535
538
|
// switch is an edit to a form the user is already filling in, not a reset.
|
|
539
|
+
if (seedingPrefill) {
|
|
540
|
+
seedingPrefill = false
|
|
541
|
+
return
|
|
542
|
+
}
|
|
536
543
|
const preset = defaultPipelineIdFor(next)
|
|
537
544
|
if (preset) pipelineId.value = preset
|
|
538
545
|
})
|
|
@@ -667,13 +674,23 @@ onModalOpen(open, () => {
|
|
|
667
674
|
agentConfigValues.value = {}
|
|
668
675
|
pendingContext.value = []
|
|
669
676
|
// Seed from a prefill when opened from another surface (e.g. "create task from
|
|
670
|
-
// issue" sets the title + stages the issue as linked context). Pipeline / preset
|
|
671
|
-
//
|
|
677
|
+
// issue" sets the title + stages the issue as linked context). Pipeline / preset stay at their
|
|
678
|
+
// defaults unless the opener carries a choice the person already made (the one-sentence
|
|
679
|
+
// intake's kind and overrides), which the hand-off must not reset.
|
|
672
680
|
const prefill = ui.addTaskPrefill
|
|
673
681
|
if (prefill) {
|
|
674
682
|
if (prefill.title) title.value = prefill.title
|
|
675
683
|
if (prefill.description) description.value = prefill.description
|
|
676
684
|
if (prefill.context?.length) pendingContext.value = [...prefill.context]
|
|
685
|
+
if (prefill.taskType && prefill.taskType !== taskType.value) {
|
|
686
|
+
taskType.value = prefill.taskType
|
|
687
|
+
// The type watcher runs after this block and would replace a carried pipeline with the
|
|
688
|
+
// new type's default; a seeded type is not the edit that watcher is for.
|
|
689
|
+
seedingPrefill = !!prefill.pipelineId
|
|
690
|
+
}
|
|
691
|
+
if (prefill.pipelineId) pipelineId.value = prefill.pipelineId
|
|
692
|
+
if (prefill.riskPolicyId) riskPolicyId.value = prefill.riskPolicyId
|
|
693
|
+
if (prefill.modelPresetId) modelPresetId.value = prefill.modelPresetId
|
|
677
694
|
}
|
|
678
695
|
documents.loadDocuments().catch(() => {})
|
|
679
696
|
tasks.loadTasks().catch(() => {})
|
|
@@ -831,8 +848,15 @@ async function submitCreate(acknowledgeReviewDebt: boolean) {
|
|
|
831
848
|
ui.closeAddTask()
|
|
832
849
|
} catch (e) {
|
|
833
850
|
const conflict = parseConflict(e)
|
|
834
|
-
if (conflict
|
|
835
|
-
|
|
851
|
+
if (isReviewDebtConflict(conflict)) {
|
|
852
|
+
ui.openReviewFriction(
|
|
853
|
+
reviewFrictionContext(
|
|
854
|
+
conflict,
|
|
855
|
+
() => void submitCreate(true),
|
|
856
|
+
() => saving.value,
|
|
857
|
+
() => ui.closeAddTask(),
|
|
858
|
+
),
|
|
859
|
+
)
|
|
836
860
|
return
|
|
837
861
|
}
|
|
838
862
|
const refusal = createRefusalMessage(e)
|
|
@@ -883,32 +907,6 @@ function createRefusalMessage(error: unknown): string | null {
|
|
|
883
907
|
if (reason === 'task_type_fields_invalid') return t('board.addTask.customFieldsInvalid')
|
|
884
908
|
return REVIEW_TARGET_MESSAGES[reason as ReviewTargetReason]?.(details) ?? null
|
|
885
909
|
}
|
|
886
|
-
|
|
887
|
-
/** Turn a parsed review-debt friction 409 into the dialog context (see ReviewFrictionDialog.vue). */
|
|
888
|
-
function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseConflict>>) {
|
|
889
|
-
const details = conflict.details
|
|
890
|
-
const rawDebt = Array.isArray(details.debt) ? details.debt : []
|
|
891
|
-
const debt = rawDebt.map((d) => {
|
|
892
|
-
const row = (d ?? {}) as { blockId?: unknown; title?: unknown; waitingMinutes?: unknown }
|
|
893
|
-
return {
|
|
894
|
-
blockId: typeof row.blockId === 'string' ? row.blockId : '',
|
|
895
|
-
title: typeof row.title === 'string' ? row.title : null,
|
|
896
|
-
waitingMinutes: typeof row.waitingMinutes === 'number' ? row.waitingMinutes : 0,
|
|
897
|
-
}
|
|
898
|
-
})
|
|
899
|
-
const isWarn = conflict.reason === 'review_debt_warn'
|
|
900
|
-
ui.openReviewFriction({
|
|
901
|
-
kind: isWarn ? 'warn' : 'blocked',
|
|
902
|
-
reason:
|
|
903
|
-
details.friction === 'count' || details.friction === 'stuck' ? details.friction : undefined,
|
|
904
|
-
threshold: typeof details.threshold === 'number' ? details.threshold : null,
|
|
905
|
-
debt,
|
|
906
|
-
onConfirm: isWarn ? () => void submitCreate(true) : null,
|
|
907
|
-
// A getter, not a snapshot: the dialog reads it inside a computed so its button spins and
|
|
908
|
-
// locks for as long as the retry actually runs (UX-78).
|
|
909
|
-
pending: () => saving.value,
|
|
910
|
-
})
|
|
911
|
-
}
|
|
912
910
|
</script>
|
|
913
911
|
|
|
914
912
|
<template>
|
|
@@ -954,6 +952,7 @@ function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseC
|
|
|
954
952
|
:icon="ty.icon"
|
|
955
953
|
size="xs"
|
|
956
954
|
:title="ty.description"
|
|
955
|
+
:aria-pressed="taskType === ty.value"
|
|
957
956
|
:data-testid="`task-type-${ty.value}`"
|
|
958
957
|
@click="
|
|
959
958
|
() => {
|
|
@@ -53,17 +53,20 @@ const body = computed(() => {
|
|
|
53
53
|
/** Deep-link to a waiting task's block and dismiss the whole friction flow. */
|
|
54
54
|
function goToBlock(blockId: string) {
|
|
55
55
|
if (blockId) ui.select(blockId)
|
|
56
|
+
leave()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Close the friction and the form that raised it. */
|
|
60
|
+
function leave() {
|
|
61
|
+
const onLeave = ctx.value?.onLeave
|
|
56
62
|
ui.closeReviewFriction()
|
|
57
|
-
|
|
63
|
+
onLeave?.()
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function goReview() {
|
|
61
67
|
const first = ctx.value?.debt[0]
|
|
62
68
|
if (first) goToBlock(first.blockId)
|
|
63
|
-
else
|
|
64
|
-
ui.closeReviewFriction()
|
|
65
|
-
ui.closeAddTask()
|
|
66
|
-
}
|
|
69
|
+
else leave()
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function createAnyway() {
|
|
@@ -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'
|
|
@@ -21,6 +21,12 @@ const props = withDefaults(
|
|
|
21
21
|
* chosen row — and the row is left out entirely.
|
|
22
22
|
*/
|
|
23
23
|
noneLabel?: string
|
|
24
|
+
/**
|
|
25
|
+
* What the preview pane says about the "none" row. Defaults to the full form's meaning ("no
|
|
26
|
+
* default; you choose when you run it"); a surface whose "none" row means the workspace default
|
|
27
|
+
* names what that default resolves to instead.
|
|
28
|
+
*/
|
|
29
|
+
noneHint?: string
|
|
24
30
|
/**
|
|
25
31
|
* Default-trigger text while nothing is selected. Defaults to {@link noneLabel}, which reads
|
|
26
32
|
* correctly when the "none" row IS the empty state; a surface without that row supplies its
|
|
@@ -30,7 +36,7 @@ const props = withDefaults(
|
|
|
30
36
|
/** Extra classes for the default trigger button (e.g. full-width in the modal). */
|
|
31
37
|
triggerClass?: string
|
|
32
38
|
}>(),
|
|
33
|
-
{ noneLabel: undefined, placeholder: undefined, triggerClass: '' },
|
|
39
|
+
{ noneLabel: undefined, noneHint: undefined, placeholder: undefined, triggerClass: '' },
|
|
34
40
|
)
|
|
35
41
|
|
|
36
42
|
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
@@ -59,7 +65,7 @@ const previewPipeline = computed<Pipeline | null>(() => {
|
|
|
59
65
|
*/
|
|
60
66
|
const emptyHint = computed(() =>
|
|
61
67
|
props.noneLabel !== undefined && (hoverId.value ?? props.modelValue) === ''
|
|
62
|
-
? t('pipeline.picker.noneHint')
|
|
68
|
+
? (props.noneHint ?? t('pipeline.picker.noneHint'))
|
|
63
69
|
: t('pipeline.picker.hoverHint'),
|
|
64
70
|
)
|
|
65
71
|
|