@cat-factory/app 0.111.3 → 0.113.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/app/components/forkDecision/ForkDecisionWindow.vue +300 -0
- package/app/components/layout/NotificationsInbox.vue +15 -0
- package/app/components/panels/StepResultViewHost.vue +5 -0
- package/app/components/panels/inspector/TaskAprioriBranches.vue +247 -0
- package/app/components/panels/inspector/TaskExecution.vue +21 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +5 -0
- package/app/components/pipeline/PipelineProgress.vue +41 -1
- package/app/components/settings/RiskPolicyPanel.vue +96 -0
- package/app/components/slack/SlackPanel.vue +1 -0
- package/app/composables/api/forkDecision.ts +29 -0
- package/app/composables/useApi.ts +2 -0
- package/app/stores/forkDecision.ts +84 -0
- package/app/stores/ui.ts +26 -0
- package/app/types/domain.ts +1 -0
- package/app/types/execution.ts +5 -0
- package/app/utils/catalog.ts +11 -0
- package/i18n/locales/de.json +64 -3
- package/i18n/locales/en.json +64 -3
- package/i18n/locales/es.json +64 -3
- package/i18n/locales/fr.json +64 -3
- package/i18n/locales/he.json +64 -3
- package/i18n/locales/it.json +64 -3
- package/i18n/locales/ja.json +64 -3
- package/i18n/locales/pl.json +64 -3
- package/i18n/locales/tr.json +64 -3
- package/i18n/locales/uk.json +64 -3
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { AgentState, ExecutionInstance } from '~/types/domain'
|
|
3
3
|
import type { PipelineStep } from '~/types/execution'
|
|
4
|
-
import { agentKindMeta, FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
|
|
4
|
+
import { agentKindMeta, FOLLOW_UP_COMPANION_META, FORK_DECISION_META } from '~/utils/catalog'
|
|
5
5
|
import {
|
|
6
6
|
subtaskIconClass,
|
|
7
7
|
gateCompanionFor,
|
|
@@ -68,6 +68,12 @@ function followUpLabel(step: PipelineStep): string {
|
|
|
68
68
|
: t('pipeline.progress.followUp.allDecided')
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The active fork-decision phase status on a coder step (proposing / awaiting a choice). */
|
|
72
|
+
function forkPhase(step: PipelineStep): 'proposing' | 'awaiting_choice' | null {
|
|
73
|
+
const status = step.forkDecision?.status
|
|
74
|
+
return status === 'proposing' || status === 'awaiting_choice' ? status : null
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
// --- restart from a step -----------------------------------------------------
|
|
72
78
|
// Re-run the pipeline from a chosen step onward: the server resets that step +
|
|
73
79
|
// every later step's iteration counters and re-drives a fresh run, keeping the
|
|
@@ -543,6 +549,40 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
543
549
|
</span>
|
|
544
550
|
</button>
|
|
545
551
|
|
|
552
|
+
<!-- Implementation-fork decision phase (Coder step): a spinner while the proposer
|
|
553
|
+
surfaces approaches, then a clickable chip to choose one. -->
|
|
554
|
+
<button
|
|
555
|
+
v-if="forkPhase(s)"
|
|
556
|
+
type="button"
|
|
557
|
+
class="mt-3 flex w-full items-center gap-2 rounded-lg border border-dashed px-2.5 py-1.5 text-start transition hover:border-violet-400/60"
|
|
558
|
+
:class="
|
|
559
|
+
forkPhase(s) === 'awaiting_choice'
|
|
560
|
+
? 'border-violet-500/50 bg-violet-500/10 followup-blink'
|
|
561
|
+
: 'border-slate-700/70 bg-slate-900/40'
|
|
562
|
+
"
|
|
563
|
+
:disabled="forkPhase(s) === 'proposing'"
|
|
564
|
+
@click="ui.openForkDecision(instance.id, i)"
|
|
565
|
+
>
|
|
566
|
+
<span
|
|
567
|
+
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md border border-violet-500/40 bg-violet-500/15"
|
|
568
|
+
>
|
|
569
|
+
<UIcon
|
|
570
|
+
:name="
|
|
571
|
+
forkPhase(s) === 'proposing' ? 'i-lucide-loader-circle' : FORK_DECISION_META.icon
|
|
572
|
+
"
|
|
573
|
+
class="h-3 w-3 text-violet-300"
|
|
574
|
+
:class="forkPhase(s) === 'proposing' ? 'animate-spin' : ''"
|
|
575
|
+
/>
|
|
576
|
+
</span>
|
|
577
|
+
<span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
|
|
578
|
+
{{
|
|
579
|
+
forkPhase(s) === 'proposing'
|
|
580
|
+
? t('pipeline.progress.forkDecision.proposing')
|
|
581
|
+
: t('pipeline.progress.forkDecision.choose')
|
|
582
|
+
}}
|
|
583
|
+
</span>
|
|
584
|
+
</button>
|
|
585
|
+
|
|
546
586
|
<!-- reviewer gate folding/re-reviewing in the background: a working indicator,
|
|
547
587
|
NOT a "Review & approve" gate (the human is summoned only if needed) -->
|
|
548
588
|
<div
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// default; it cannot be deleted or un-defaulted (the backend enforces this too).
|
|
7
7
|
import { computed, reactive, ref, watch } from 'vue'
|
|
8
8
|
import type { RiskPolicy, RequirementConcernLevel } from '~/types/merge'
|
|
9
|
+
import type { StepGating } from '@cat-factory/contracts'
|
|
9
10
|
|
|
10
11
|
const { t } = useI18n()
|
|
11
12
|
|
|
@@ -42,9 +43,32 @@ interface Draft {
|
|
|
42
43
|
maxRequirementIterations: number
|
|
43
44
|
maxRequirementConcernAllowed: RequirementConcernLevel
|
|
44
45
|
autoMergeEnabled: boolean
|
|
46
|
+
// Implementation-fork decision gating (edited 0..100, stored 0..1); disabled ⇒ off in `auto`.
|
|
47
|
+
forkEnabled: boolean
|
|
48
|
+
forkMinComplexity: number
|
|
49
|
+
forkMinRisk: number
|
|
50
|
+
forkMinImpact: number
|
|
51
|
+
forkOnMissing: 'run' | 'skip'
|
|
45
52
|
}
|
|
46
53
|
const drafts = reactive<Record<string, Draft>>({})
|
|
47
54
|
|
|
55
|
+
// On-missing-estimate options for the fork gating group (fail toward asking / skipping).
|
|
56
|
+
const ON_MISSING_OPTIONS = computed<{ value: 'run' | 'skip'; label: string }[]>(() => [
|
|
57
|
+
{ value: 'run', label: t('settings.riskPolicy.forkDecision.onMissing.run') },
|
|
58
|
+
{ value: 'skip', label: t('settings.riskPolicy.forkDecision.onMissing.skip') },
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
/** Build the `StepGating` payload for the fork-decision gate from a draft (or null when off). */
|
|
62
|
+
function forkGating(d: Draft): StepGating {
|
|
63
|
+
return {
|
|
64
|
+
enabled: d.forkEnabled,
|
|
65
|
+
minComplexity: d.forkMinComplexity / 100,
|
|
66
|
+
minRisk: d.forkMinRisk / 100,
|
|
67
|
+
minImpact: d.forkMinImpact / 100,
|
|
68
|
+
onMissingEstimate: d.forkOnMissing,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
function toDraft(p: RiskPolicy): Draft {
|
|
49
73
|
return {
|
|
50
74
|
name: p.name,
|
|
@@ -55,6 +79,11 @@ function toDraft(p: RiskPolicy): Draft {
|
|
|
55
79
|
maxRequirementIterations: p.maxRequirementIterations,
|
|
56
80
|
maxRequirementConcernAllowed: p.maxRequirementConcernAllowed,
|
|
57
81
|
autoMergeEnabled: p.autoMergeEnabled,
|
|
82
|
+
forkEnabled: p.forkDecision?.enabled ?? false,
|
|
83
|
+
forkMinComplexity: Math.round((p.forkDecision?.minComplexity ?? 0.5) * 100),
|
|
84
|
+
forkMinRisk: Math.round((p.forkDecision?.minRisk ?? 0.4) * 100),
|
|
85
|
+
forkMinImpact: Math.round((p.forkDecision?.minImpact ?? 0.4) * 100),
|
|
86
|
+
forkOnMissing: p.forkDecision?.onMissingEstimate ?? 'run',
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
|
|
@@ -92,6 +121,7 @@ async function save(p: RiskPolicy) {
|
|
|
92
121
|
maxRequirementIterations: d.maxRequirementIterations,
|
|
93
122
|
maxRequirementConcernAllowed: d.maxRequirementConcernAllowed,
|
|
94
123
|
autoMergeEnabled: d.autoMergeEnabled,
|
|
124
|
+
forkDecision: forkGating(d),
|
|
95
125
|
})
|
|
96
126
|
toast.add({
|
|
97
127
|
title: t('settings.riskPolicy.toast.saved'),
|
|
@@ -146,6 +176,11 @@ const draft = reactive<Draft>({
|
|
|
146
176
|
maxRequirementIterations: 6,
|
|
147
177
|
maxRequirementConcernAllowed: 'none',
|
|
148
178
|
autoMergeEnabled: true,
|
|
179
|
+
forkEnabled: false,
|
|
180
|
+
forkMinComplexity: 50,
|
|
181
|
+
forkMinRisk: 40,
|
|
182
|
+
forkMinImpact: 40,
|
|
183
|
+
forkOnMissing: 'run',
|
|
149
184
|
})
|
|
150
185
|
|
|
151
186
|
async function create() {
|
|
@@ -161,6 +196,7 @@ async function create() {
|
|
|
161
196
|
maxRequirementIterations: draft.maxRequirementIterations,
|
|
162
197
|
maxRequirementConcernAllowed: draft.maxRequirementConcernAllowed,
|
|
163
198
|
autoMergeEnabled: draft.autoMergeEnabled,
|
|
199
|
+
forkDecision: forkGating(draft),
|
|
164
200
|
})
|
|
165
201
|
draft.name = ''
|
|
166
202
|
draft.autoMergeEnabled = true
|
|
@@ -305,6 +341,61 @@ async function create() {
|
|
|
305
341
|
</label>
|
|
306
342
|
</div>
|
|
307
343
|
|
|
344
|
+
<!-- Implementation-fork decision gate: propose materially different approaches before the
|
|
345
|
+
Coder writes code (in `auto` tri-state, gated on the task estimate). -->
|
|
346
|
+
<div class="mt-3 rounded-md border border-slate-800 bg-slate-900/40 p-3">
|
|
347
|
+
<USwitch
|
|
348
|
+
v-model="drafts[p.id]!.forkEnabled"
|
|
349
|
+
size="sm"
|
|
350
|
+
:label="t('settings.riskPolicy.forkDecision.label')"
|
|
351
|
+
:description="t('settings.riskPolicy.forkDecision.hint')"
|
|
352
|
+
/>
|
|
353
|
+
<div v-if="drafts[p.id]!.forkEnabled" class="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
354
|
+
<label class="block">
|
|
355
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
356
|
+
{{ t('settings.riskPolicy.forkDecision.minComplexity') }}
|
|
357
|
+
</span>
|
|
358
|
+
<UInput
|
|
359
|
+
v-model.number="drafts[p.id]!.forkMinComplexity"
|
|
360
|
+
type="number"
|
|
361
|
+
size="sm"
|
|
362
|
+
:min="0"
|
|
363
|
+
:max="100"
|
|
364
|
+
/>
|
|
365
|
+
</label>
|
|
366
|
+
<label class="block">
|
|
367
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
368
|
+
{{ t('settings.riskPolicy.forkDecision.minRisk') }}
|
|
369
|
+
</span>
|
|
370
|
+
<UInput
|
|
371
|
+
v-model.number="drafts[p.id]!.forkMinRisk"
|
|
372
|
+
type="number"
|
|
373
|
+
size="sm"
|
|
374
|
+
:min="0"
|
|
375
|
+
:max="100"
|
|
376
|
+
/>
|
|
377
|
+
</label>
|
|
378
|
+
<label class="block">
|
|
379
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
380
|
+
{{ t('settings.riskPolicy.forkDecision.minImpact') }}
|
|
381
|
+
</span>
|
|
382
|
+
<UInput
|
|
383
|
+
v-model.number="drafts[p.id]!.forkMinImpact"
|
|
384
|
+
type="number"
|
|
385
|
+
size="sm"
|
|
386
|
+
:min="0"
|
|
387
|
+
:max="100"
|
|
388
|
+
/>
|
|
389
|
+
</label>
|
|
390
|
+
<label class="block">
|
|
391
|
+
<span class="mb-1 block text-[10px] uppercase tracking-wide text-slate-500">
|
|
392
|
+
{{ t('settings.riskPolicy.forkDecision.onMissingLabel') }}
|
|
393
|
+
</span>
|
|
394
|
+
<USelect v-model="drafts[p.id]!.forkOnMissing" :items="ON_MISSING_OPTIONS" size="sm" />
|
|
395
|
+
</label>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
308
399
|
<div class="mt-3 flex items-center justify-between gap-3">
|
|
309
400
|
<USwitch
|
|
310
401
|
v-model="drafts[p.id]!.autoMergeEnabled"
|
|
@@ -403,6 +494,11 @@ async function create() {
|
|
|
403
494
|
size="sm"
|
|
404
495
|
:label="t('settings.riskPolicy.field.autoMerge')"
|
|
405
496
|
/>
|
|
497
|
+
<USwitch
|
|
498
|
+
v-model="draft.forkEnabled"
|
|
499
|
+
size="sm"
|
|
500
|
+
:label="t('settings.riskPolicy.forkDecision.label')"
|
|
501
|
+
/>
|
|
406
502
|
<UButton
|
|
407
503
|
color="primary"
|
|
408
504
|
size="sm"
|
|
@@ -56,6 +56,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
56
56
|
visual_confirmation_ready: { enabled: false, channel: '' },
|
|
57
57
|
human_review: { enabled: false, channel: '' },
|
|
58
58
|
followup_pending: { enabled: false, channel: '' },
|
|
59
|
+
fork_decision_pending: { enabled: false, channel: '' },
|
|
59
60
|
initiative: { enabled: false, channel: '' },
|
|
60
61
|
})
|
|
61
62
|
const mentionsEnabled = ref(false)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { chooseForkContract, getForkDecisionContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The implementation-fork decision phase: before the Coder writes code the read-only
|
|
6
|
+
* proposer surfaces materially different approaches on the run's coder step and the run
|
|
7
|
+
* parks. These endpoints read the surfaced approaches and record the human's choice (a
|
|
8
|
+
* proposed fork or their own free-text approach); choosing re-runs the Coder with the chosen
|
|
9
|
+
* approach folded in. The read returns null when no coder step carries fork state.
|
|
10
|
+
*/
|
|
11
|
+
export function forkDecisionApi({ send, ws }: ApiContext) {
|
|
12
|
+
return {
|
|
13
|
+
// The live fork-decision state for a run (null when no coder step carries one).
|
|
14
|
+
getForkDecision: (workspaceId: string, executionId: string) =>
|
|
15
|
+
send(getForkDecisionContract, { pathPrefix: ws(workspaceId), pathParams: { executionId } }),
|
|
16
|
+
|
|
17
|
+
// Choose an implementation approach — a proposed fork id or a custom approach (+ note).
|
|
18
|
+
chooseFork: (
|
|
19
|
+
workspaceId: string,
|
|
20
|
+
executionId: string,
|
|
21
|
+
body: { forkId?: string | null; custom?: string | null; note?: string | null },
|
|
22
|
+
) =>
|
|
23
|
+
send(chooseForkContract, {
|
|
24
|
+
pathPrefix: ws(workspaceId),
|
|
25
|
+
pathParams: { executionId },
|
|
26
|
+
body,
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -8,6 +8,7 @@ import { boardApi } from './api/board'
|
|
|
8
8
|
import { documentsApi } from './api/documents'
|
|
9
9
|
import { executionApi } from './api/execution'
|
|
10
10
|
import { followUpsApi } from './api/followUps'
|
|
11
|
+
import { forkDecisionApi } from './api/forkDecision'
|
|
11
12
|
import { fragmentsApi } from './api/fragments'
|
|
12
13
|
import { githubApi } from './api/github'
|
|
13
14
|
import { humanReviewApi } from './api/humanReview'
|
|
@@ -108,6 +109,7 @@ export function useApi() {
|
|
|
108
109
|
...tasksApi(ctx),
|
|
109
110
|
...reviewsApi(ctx),
|
|
110
111
|
...followUpsApi(ctx),
|
|
112
|
+
...forkDecisionApi(ctx),
|
|
111
113
|
...humanTestApi(ctx),
|
|
112
114
|
...visualConfirmApi(ctx),
|
|
113
115
|
...humanReviewApi(ctx),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { ForkDecisionStepState } from '~/types/execution'
|
|
4
|
+
import { useApi } from '~/composables/useApi'
|
|
5
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
6
|
+
import { useExecutionStore } from '~/stores/execution'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The implementation-fork decision action surface. The live fork state lives on the run's
|
|
10
|
+
* Coder step (`step.forkDecision`) and is kept fresh by the execution stream, so the window
|
|
11
|
+
* reads it straight off the execution store — this store only wraps the `choose` action (and
|
|
12
|
+
* a warm-up `load`), tracks the in-flight state so the window can disable its controls, and
|
|
13
|
+
* reflects the returned state back onto the execution store so the UI updates immediately even
|
|
14
|
+
* before the stream echoes the change. Keyed by executionId, mirroring the follow-ups store.
|
|
15
|
+
*/
|
|
16
|
+
export const useForkDecisionStore = defineStore('forkDecision', () => {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const workspace = useWorkspaceStore()
|
|
19
|
+
const execution = useExecutionStore()
|
|
20
|
+
|
|
21
|
+
/** True while a choose call is in flight (drives the Choose button spinner / disabled state). */
|
|
22
|
+
const choosing = ref(false)
|
|
23
|
+
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
24
|
+
const error = ref<string | null>(null)
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reflect an authoritative fork-decision state onto the run's Coder step. A pipeline may
|
|
28
|
+
* carry more than one `coder` step, so target the step this decision is about rather than
|
|
29
|
+
* the first one that happens to hold fork state: prefer the step that is still live
|
|
30
|
+
* (proposing / awaiting the choice / answering), then the current step, and only then fall
|
|
31
|
+
* back to the first step carrying fork state. The stream corrects any mismatch, but this
|
|
32
|
+
* keeps the immediate optimistic echo on the right step.
|
|
33
|
+
*/
|
|
34
|
+
function reflect(executionId: string, state: ForkDecisionStepState | null): void {
|
|
35
|
+
if (!state) return
|
|
36
|
+
const instance = execution.getInstance(executionId)
|
|
37
|
+
if (!instance) return
|
|
38
|
+
const isLive = (s: (typeof instance.steps)[number]) =>
|
|
39
|
+
s.agentKind === 'coder' &&
|
|
40
|
+
(s.forkDecision?.status === 'awaiting_choice' ||
|
|
41
|
+
s.forkDecision?.status === 'answering' ||
|
|
42
|
+
s.forkDecision?.status === 'proposing')
|
|
43
|
+
const current = instance.steps[instance.currentStep]
|
|
44
|
+
const step =
|
|
45
|
+
instance.steps.find(isLive) ??
|
|
46
|
+
(current?.agentKind === 'coder' && current.forkDecision ? current : undefined) ??
|
|
47
|
+
instance.steps.find((s) => s.forkDecision)
|
|
48
|
+
if (step) step.forkDecision = state
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Warm the live state from the GET (the stream also keeps it fresh). Best-effort. */
|
|
52
|
+
async function load(executionId: string): Promise<void> {
|
|
53
|
+
error.value = null
|
|
54
|
+
try {
|
|
55
|
+
const state = await api.getForkDecision(workspace.requireId(), executionId)
|
|
56
|
+
reflect(executionId, state as ForkDecisionStepState | null)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Choose an implementation approach: a proposed fork id OR a custom free-text approach
|
|
64
|
+
* (with an optional steering note). The Coder then re-runs with the choice folded in.
|
|
65
|
+
*/
|
|
66
|
+
async function choose(
|
|
67
|
+
executionId: string,
|
|
68
|
+
choice: { forkId?: string; custom?: string; note?: string },
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
error.value = null
|
|
71
|
+
choosing.value = true
|
|
72
|
+
try {
|
|
73
|
+
const state = await api.chooseFork(workspace.requireId(), executionId, choice)
|
|
74
|
+
reflect(executionId, state as ForkDecisionStepState)
|
|
75
|
+
} catch (e) {
|
|
76
|
+
error.value = e instanceof Error ? e.message : 'Failed to choose'
|
|
77
|
+
throw e
|
|
78
|
+
} finally {
|
|
79
|
+
choosing.value = false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { choosing, error, load, choose }
|
|
84
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -809,6 +809,31 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
809
809
|
stepIndex: idx,
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
+
// Open the implementation-fork decision window for a run's coder step (from the inspector /
|
|
813
|
+
// pipeline chip / `fork_decision_pending` notification). Resolves the coder step index from
|
|
814
|
+
// the run when not given, preferring the step parked awaiting a choice.
|
|
815
|
+
function openForkDecision(instanceId: string, stepIndex: number | null = null) {
|
|
816
|
+
const execution = useExecutionStore()
|
|
817
|
+
const instance = execution.getInstance(instanceId)
|
|
818
|
+
if (!instance) return
|
|
819
|
+
const resolveIdx = () => {
|
|
820
|
+
const awaiting = instance.steps.findIndex(
|
|
821
|
+
(s) => s.agentKind === 'coder' && s.forkDecision?.status === 'awaiting_choice',
|
|
822
|
+
)
|
|
823
|
+
if (awaiting >= 0) return awaiting
|
|
824
|
+
const current = instance.steps[instance.currentStep]
|
|
825
|
+
if (current?.agentKind === 'coder' && current.forkDecision) return instance.currentStep
|
|
826
|
+
return instance.steps.findIndex((s) => s.agentKind === 'coder' && s.forkDecision)
|
|
827
|
+
}
|
|
828
|
+
const idx = stepIndex ?? resolveIdx()
|
|
829
|
+
if (idx < 0) return
|
|
830
|
+
resultView.value = {
|
|
831
|
+
view: 'fork-decision',
|
|
832
|
+
blockId: instance.blockId,
|
|
833
|
+
instanceId,
|
|
834
|
+
stepIndex: idx,
|
|
835
|
+
}
|
|
836
|
+
}
|
|
812
837
|
function closeResultView() {
|
|
813
838
|
resultView.value = null
|
|
814
839
|
}
|
|
@@ -1006,6 +1031,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
1006
1031
|
openInitiativeTracker,
|
|
1007
1032
|
openInitiativePlanning,
|
|
1008
1033
|
openFollowUps,
|
|
1034
|
+
openForkDecision,
|
|
1009
1035
|
closeRequirementReview,
|
|
1010
1036
|
openStepDetail,
|
|
1011
1037
|
closeStepDetail,
|
package/app/types/domain.ts
CHANGED
package/app/types/execution.ts
CHANGED
package/app/utils/catalog.ts
CHANGED
|
@@ -708,6 +708,17 @@ export const FOLLOW_UP_COMPANION_META = {
|
|
|
708
708
|
color: '#f472b6',
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Display metadata for the implementation-fork decision phase on a Coder step (a per-step
|
|
713
|
+
* phase, not an agent kind of its own — the read-only `fork-proposer` is never a palette
|
|
714
|
+
* block). Drives the fork-decision window header + the pipeline phase chip.
|
|
715
|
+
*/
|
|
716
|
+
export const FORK_DECISION_META = {
|
|
717
|
+
label: 'Implementation-fork decision',
|
|
718
|
+
icon: 'i-lucide-git-fork',
|
|
719
|
+
color: '#a78bfa',
|
|
720
|
+
}
|
|
721
|
+
|
|
711
722
|
/**
|
|
712
723
|
* Whether a Coder step has the Follow-up companion enabled, given the pipeline's per-step
|
|
713
724
|
* `followUps` toggle at index `i`. Enabled by default on a `coder` step (only `false`
|
package/i18n/locales/de.json
CHANGED
|
@@ -438,6 +438,18 @@
|
|
|
438
438
|
"createFailed": "Richtlinie konnte nicht erstellt werden",
|
|
439
439
|
"defaultFailed": "Standard konnte nicht festgelegt werden",
|
|
440
440
|
"deleteFailed": "Richtlinie konnte nicht gelöscht werden"
|
|
441
|
+
},
|
|
442
|
+
"forkDecision": {
|
|
443
|
+
"label": "Implementierungs-Weichenentscheidung",
|
|
444
|
+
"hint": "Im Auto-Modus grundlegend verschiedene Ansätze vorschlagen (und für eine Wahl pausieren), wenn die Aufgabenschätzung einen Schwellenwert erreicht.",
|
|
445
|
+
"minComplexity": "Min. Komplexität",
|
|
446
|
+
"minRisk": "Min. Risiko",
|
|
447
|
+
"minImpact": "Min. Auswirkung",
|
|
448
|
+
"onMissingLabel": "Keine Schätzung",
|
|
449
|
+
"onMissing": {
|
|
450
|
+
"run": "Trotzdem vorschlagen",
|
|
451
|
+
"skip": "Überspringen"
|
|
452
|
+
}
|
|
441
453
|
}
|
|
442
454
|
},
|
|
443
455
|
"observabilityConnection": {
|
|
@@ -1161,7 +1173,8 @@
|
|
|
1161
1173
|
"companionOf": "{label} (Begleiter)",
|
|
1162
1174
|
"merged": "Gemergt",
|
|
1163
1175
|
"open": "Öffnen",
|
|
1164
|
-
"mergePr": "PR mergen"
|
|
1176
|
+
"mergePr": "PR mergen",
|
|
1177
|
+
"chooseApproach": "Ansatz wählen"
|
|
1165
1178
|
},
|
|
1166
1179
|
"structure": {
|
|
1167
1180
|
"title": "Struktur",
|
|
@@ -1227,6 +1240,20 @@
|
|
|
1227
1240
|
"involvedServicesEmpty": "Keine verbundenen Services. Verbinde Services am Service-Frame, um sie hier auszuwählen.",
|
|
1228
1241
|
"involvedServiceStale": "Nicht mehr mit dem Service dieser Aufgabe verbunden; wird bei der nächsten Änderung entfernt."
|
|
1229
1242
|
},
|
|
1243
|
+
"aprioriBranches": {
|
|
1244
|
+
"title": "Vorhandene Branches",
|
|
1245
|
+
"mode": {
|
|
1246
|
+
"reference": "Referenz",
|
|
1247
|
+
"working": "Arbeitsbranch"
|
|
1248
|
+
},
|
|
1249
|
+
"remove": "{branch} entfernen",
|
|
1250
|
+
"searchPlaceholder": "Vorhandenen Branch hinzufügen…",
|
|
1251
|
+
"connectFirst": "Verbinde GitHub, um vorhandene Branches hinzuzufügen.",
|
|
1252
|
+
"protectedWarning": "{branch} ist ein geschützter Branch. Pushes aus dem Lauf werden möglicherweise abgelehnt.",
|
|
1253
|
+
"multiRepoHint": "Der Arbeitsmodus ist nicht verfügbar, solange diese Aufgabe mehr als einen Service umfasst.",
|
|
1254
|
+
"frozenHint": "Der Arbeitsbranch ist gesperrt, da für diese Aufgabe bereits ein Pull Request existiert.",
|
|
1255
|
+
"hint": "Übergib dieser Aufgabe vorhandene Branches ihres Repositorys. Ein Referenzbranch ist schreibgeschützter Kontext, den die Agenten einsehen dürfen; im einzelnen Arbeitsbranch baut der Lauf weiter, statt einen neuen Branch anzulegen."
|
|
1256
|
+
},
|
|
1230
1257
|
"referenceRepos": {
|
|
1231
1258
|
"title": "Referenz-Repositories",
|
|
1232
1259
|
"remove": "{repo} entfernen",
|
|
@@ -1649,7 +1676,8 @@
|
|
|
1649
1676
|
"human_review": "Als gelesen markieren",
|
|
1650
1677
|
"followup_pending": "Als gelesen markieren",
|
|
1651
1678
|
"initiative": "Als gelesen markieren",
|
|
1652
|
-
"markRead": "Als gelesen markieren"
|
|
1679
|
+
"markRead": "Als gelesen markieren",
|
|
1680
|
+
"fork_decision_pending": "Als gelesen markieren"
|
|
1653
1681
|
}
|
|
1654
1682
|
},
|
|
1655
1683
|
"aiProvidersBanner": {
|
|
@@ -2979,7 +3007,11 @@
|
|
|
2979
3007
|
"subtasksInProgress": "· {count} in Arbeit",
|
|
2980
3008
|
"clickToRead": "Klicken, um die Ausgabe dieses Agenten zu lesen",
|
|
2981
3009
|
"reviewApprove": "Vorschlag von {agent} prüfen & freigeben",
|
|
2982
|
-
"resolve": "Klären: {question}"
|
|
3010
|
+
"resolve": "Klären: {question}",
|
|
3011
|
+
"forkDecision": {
|
|
3012
|
+
"proposing": "Ansätze werden vorgeschlagen…",
|
|
3013
|
+
"choose": "Ansatz wählen"
|
|
3014
|
+
}
|
|
2983
3015
|
},
|
|
2984
3016
|
"health": {
|
|
2985
3017
|
"title": "Pipeline-Zustand",
|
|
@@ -4551,5 +4583,34 @@
|
|
|
4551
4583
|
"palette": {
|
|
4552
4584
|
"hint": "Klicke auf einen Agenten, um ihn an die Pipeline anzuhängen.",
|
|
4553
4585
|
"customAgents": "Benutzerdefinierte Agenten"
|
|
4586
|
+
},
|
|
4587
|
+
"forkDecision": {
|
|
4588
|
+
"title": "Implementierungsansatz wählen",
|
|
4589
|
+
"titleWithBlock": "Ansatz für {title} wählen",
|
|
4590
|
+
"subtitle": "Grundlegend verschiedene Wege, diese Aufgabe umzusetzen, bevor Code geschrieben wird.",
|
|
4591
|
+
"seam": "Betroffene Stelle:",
|
|
4592
|
+
"recommended": "Empfohlen",
|
|
4593
|
+
"riskNotes": "Risiko:",
|
|
4594
|
+
"noteLabel": "Hinweis (optional)",
|
|
4595
|
+
"notePlaceholder": "Was die Umsetzung berücksichtigen soll…",
|
|
4596
|
+
"choose": "Diesen Ansatz verwenden",
|
|
4597
|
+
"proposing": {
|
|
4598
|
+
"title": "Ansätze werden ermittelt…",
|
|
4599
|
+
"hint": "Der Code wird gelesen, um die grundlegend verschiedenen Umsetzungswege zu finden."
|
|
4600
|
+
},
|
|
4601
|
+
"singlePath": {
|
|
4602
|
+
"title": "Ein klarer Ansatz"
|
|
4603
|
+
},
|
|
4604
|
+
"chosen": {
|
|
4605
|
+
"title": "Ansatz gewählt",
|
|
4606
|
+
"note": "Hinweis: {note}"
|
|
4607
|
+
},
|
|
4608
|
+
"custom": {
|
|
4609
|
+
"title": "Eigenen Ansatz eingeben",
|
|
4610
|
+
"placeholder": "Beschreibe, wie dies umgesetzt werden soll…"
|
|
4611
|
+
},
|
|
4612
|
+
"empty": {
|
|
4613
|
+
"title": "Nichts zu entscheiden"
|
|
4614
|
+
}
|
|
4554
4615
|
}
|
|
4555
4616
|
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -911,7 +911,8 @@
|
|
|
911
911
|
"companionOf": "{label} (companion)",
|
|
912
912
|
"merged": "Merged",
|
|
913
913
|
"open": "Open",
|
|
914
|
-
"mergePr": "Merge PR"
|
|
914
|
+
"mergePr": "Merge PR",
|
|
915
|
+
"chooseApproach": "Choose approach"
|
|
915
916
|
},
|
|
916
917
|
"structure": {
|
|
917
918
|
"title": "Structure",
|
|
@@ -977,6 +978,20 @@
|
|
|
977
978
|
"involvedServicesEmpty": "No connected services. Connect services on the service frame to select them here.",
|
|
978
979
|
"involvedServiceStale": "No longer connected to this task's service; it is dropped on the next change."
|
|
979
980
|
},
|
|
981
|
+
"aprioriBranches": {
|
|
982
|
+
"title": "Existing branches",
|
|
983
|
+
"mode": {
|
|
984
|
+
"reference": "Reference",
|
|
985
|
+
"working": "Working"
|
|
986
|
+
},
|
|
987
|
+
"remove": "Remove {branch}",
|
|
988
|
+
"searchPlaceholder": "Add an existing branch…",
|
|
989
|
+
"connectFirst": "Connect GitHub to add existing branches.",
|
|
990
|
+
"protectedWarning": "{branch} is a protected branch. Pushes from the run may be rejected.",
|
|
991
|
+
"multiRepoHint": "Working mode is unavailable while this task involves more than one service.",
|
|
992
|
+
"frozenHint": "The working branch is locked because a pull request already exists for this task.",
|
|
993
|
+
"hint": "Hand this task pre-existing branches of its repository. A reference branch is read-only context the agents may inspect; the single working branch is where the run keeps building instead of a new branch."
|
|
994
|
+
},
|
|
980
995
|
"referenceRepos": {
|
|
981
996
|
"title": "Reference repositories",
|
|
982
997
|
"remove": "Remove {repo}",
|
|
@@ -1539,7 +1554,8 @@
|
|
|
1539
1554
|
"human_review": "Mark read",
|
|
1540
1555
|
"followup_pending": "Mark read",
|
|
1541
1556
|
"initiative": "Mark read",
|
|
1542
|
-
"markRead": "Mark read"
|
|
1557
|
+
"markRead": "Mark read",
|
|
1558
|
+
"fork_decision_pending": "Mark read"
|
|
1543
1559
|
}
|
|
1544
1560
|
},
|
|
1545
1561
|
"aiProvidersBanner": {
|
|
@@ -2213,6 +2229,18 @@
|
|
|
2213
2229
|
"createFailed": "Could not create policy",
|
|
2214
2230
|
"defaultFailed": "Could not set default",
|
|
2215
2231
|
"deleteFailed": "Could not delete policy"
|
|
2232
|
+
},
|
|
2233
|
+
"forkDecision": {
|
|
2234
|
+
"label": "Implementation-fork decision",
|
|
2235
|
+
"hint": "In auto mode, propose materially different approaches (and pause for a choice) when the task estimate meets a threshold.",
|
|
2236
|
+
"minComplexity": "Min complexity",
|
|
2237
|
+
"minRisk": "Min risk",
|
|
2238
|
+
"minImpact": "Min impact",
|
|
2239
|
+
"onMissingLabel": "No estimate",
|
|
2240
|
+
"onMissing": {
|
|
2241
|
+
"run": "Propose anyway",
|
|
2242
|
+
"skip": "Skip"
|
|
2243
|
+
}
|
|
2216
2244
|
}
|
|
2217
2245
|
},
|
|
2218
2246
|
"observabilityConnection": {
|
|
@@ -3291,7 +3319,11 @@
|
|
|
3291
3319
|
"subtasksInProgress": "· {count} in progress",
|
|
3292
3320
|
"clickToRead": "Click to read this agent's output",
|
|
3293
3321
|
"reviewApprove": "Review & approve {agent}'s proposal",
|
|
3294
|
-
"resolve": "Resolve: {question}"
|
|
3322
|
+
"resolve": "Resolve: {question}",
|
|
3323
|
+
"forkDecision": {
|
|
3324
|
+
"proposing": "Proposing approaches…",
|
|
3325
|
+
"choose": "Choose an approach"
|
|
3326
|
+
}
|
|
3295
3327
|
},
|
|
3296
3328
|
"health": {
|
|
3297
3329
|
"title": "Pipeline health",
|
|
@@ -4668,5 +4700,34 @@
|
|
|
4668
4700
|
"hint": "Optionally provision this environment now to test the recipe.",
|
|
4669
4701
|
"run": "Trial provision"
|
|
4670
4702
|
}
|
|
4703
|
+
},
|
|
4704
|
+
"forkDecision": {
|
|
4705
|
+
"title": "Choose an implementation approach",
|
|
4706
|
+
"titleWithBlock": "Choose an approach for {title}",
|
|
4707
|
+
"subtitle": "Materially different ways to implement this task, before any code is written.",
|
|
4708
|
+
"seam": "Where it lands:",
|
|
4709
|
+
"recommended": "Recommended",
|
|
4710
|
+
"riskNotes": "Risk:",
|
|
4711
|
+
"noteLabel": "Steering note (optional)",
|
|
4712
|
+
"notePlaceholder": "Anything the implementer should keep in mind…",
|
|
4713
|
+
"choose": "Use this approach",
|
|
4714
|
+
"proposing": {
|
|
4715
|
+
"title": "Surfacing approaches…",
|
|
4716
|
+
"hint": "Reading the code to find the materially different ways to build this."
|
|
4717
|
+
},
|
|
4718
|
+
"singlePath": {
|
|
4719
|
+
"title": "One clear approach"
|
|
4720
|
+
},
|
|
4721
|
+
"chosen": {
|
|
4722
|
+
"title": "Approach chosen",
|
|
4723
|
+
"note": "Note: {note}"
|
|
4724
|
+
},
|
|
4725
|
+
"custom": {
|
|
4726
|
+
"title": "Enter your own approach",
|
|
4727
|
+
"placeholder": "Describe how you want this implemented…"
|
|
4728
|
+
},
|
|
4729
|
+
"empty": {
|
|
4730
|
+
"title": "Nothing to decide"
|
|
4731
|
+
}
|
|
4671
4732
|
}
|
|
4672
4733
|
}
|