@cat-factory/app 0.227.0 → 0.228.1
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 +25 -0
- package/app/components/board/AddTaskModal.vue +11 -7
- package/app/components/board/RecurringPipelineModal.vue +11 -7
- package/app/components/board/nodes/TaskCard.vue +68 -8
- package/app/components/bootstrap/BootstrapModal.vue +11 -7
- package/app/components/documents/RepoContextDocPicker.vue +4 -1
- package/app/components/fragments/FragmentLibraryManager.vue +19 -18
- package/app/components/gates/GateResultView.vue +3 -3
- package/app/components/github/AddServiceFromRepoModal.vue +10 -9
- package/app/components/outcome/OutcomeSummaryWindow.vue +614 -0
- package/app/components/panels/AgentStepDetail.vue +9 -7
- package/app/components/panels/InspectorPanel.vue +4 -4
- package/app/components/panels/ResultWindowShell.logic.spec.ts +4 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +12 -11
- package/app/components/panels/inspector/TaskExecution.vue +40 -7
- package/app/components/pipeline/PipelineProgress.vue +5 -4
- package/app/components/providers/ApiKeysSection.vue +3 -1
- package/app/components/ralph/RalphLoopResultView.vue +3 -3
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +14 -14
- package/app/composables/useRunDeepLink.ts +12 -4
- package/app/modular/result-views.ts +4 -0
- package/app/pages/index.vue +21 -18
- package/app/stores/execution.ts +6 -6
- package/app/stores/ui/resultViews.ts +28 -0
- package/app/stores/ui.dispatch.spec.ts +55 -0
- package/app/utils/runOutcome.spec.ts +492 -0
- package/app/utils/runOutcome.ts +509 -0
- package/i18n/locales/de.json +107 -0
- package/i18n/locales/en.json +107 -0
- package/i18n/locales/es.json +107 -0
- package/i18n/locales/fr.json +107 -0
- package/i18n/locales/he.json +107 -0
- package/i18n/locales/it.json +107 -0
- package/i18n/locales/ja.json +107 -0
- package/i18n/locales/pl.json +107 -0
- package/i18n/locales/tr.json +107 -0
- package/i18n/locales/uk.json +107 -0
- package/package.json +2 -2
|
@@ -190,6 +190,18 @@ function rootUnderDirectory(directory: string | null | undefined, path: string):
|
|
|
190
190
|
return segs.join('/')
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// The repo + service subdirectory backing this frame (the manifest prefills, the fixer
|
|
194
|
+
// run, and the compose-file browser all root paths under it). A monorepo service isn't
|
|
195
|
+
// on the `github_repos` blockId link (that stays null), so fall back to the service
|
|
196
|
+
// catalog mapping, which carries the repo + directory.
|
|
197
|
+
const repoContext = computed<{ githubId: number; directory?: string | null } | undefined>(() => {
|
|
198
|
+
if (props.repo) return props.repo
|
|
199
|
+
const svc = services.serviceByFrameBlock[props.block.id]
|
|
200
|
+
if (svc?.repoGithubId != null) return { githubId: svc.repoGithubId, directory: svc.directory }
|
|
201
|
+
const r = github.repoForBlock(props.block.id)
|
|
202
|
+
return r ? { githubId: r.githubId } : undefined
|
|
203
|
+
})
|
|
204
|
+
|
|
193
205
|
function setCustomManifestId(value: string) {
|
|
194
206
|
// Prefill the manifest path with the selected type's default, rooted under the service subtree
|
|
195
207
|
// (repo-root-relative, editable afterwards) so a monorepo service targets the right location
|
|
@@ -366,17 +378,6 @@ async function stopEnvTest() {
|
|
|
366
378
|
// ephemeral-environment provisioner, not commonly tuned — keep them collapsed by default.
|
|
367
379
|
const showProvisioning = ref(false)
|
|
368
380
|
|
|
369
|
-
// The repo + service subdirectory backing this frame, for the compose-file browser.
|
|
370
|
-
// A monorepo service isn't on the `github_repos` blockId link (that stays null), so
|
|
371
|
-
// fall back to the service catalog mapping, which carries the repo + directory.
|
|
372
|
-
const repoContext = computed<{ githubId: number; directory?: string | null } | undefined>(() => {
|
|
373
|
-
if (props.repo) return props.repo
|
|
374
|
-
const svc = services.serviceByFrameBlock[props.block.id]
|
|
375
|
-
if (svc?.repoGithubId != null) return { githubId: svc.repoGithubId, directory: svc.directory }
|
|
376
|
-
const r = github.repoForBlock(props.block.id)
|
|
377
|
-
return r ? { githubId: r.githubId } : undefined
|
|
378
|
-
})
|
|
379
|
-
|
|
380
381
|
// Repo-path picker, shared by the compose file (`docker compose -f <path>`) and the
|
|
381
382
|
// kubernetes colocated manifests path. The stored path is relative to the repo root (the
|
|
382
383
|
// browser starts inside the service's subdirectory for convenience).
|
|
@@ -19,6 +19,7 @@ import type { ChangeClass, ReviewEffort } from '~/types/merge'
|
|
|
19
19
|
import MergeEffortChips from '~/components/merge/MergeEffortChips.vue'
|
|
20
20
|
import InputGateNotice from '~/components/inputGate/InputGateNotice.vue'
|
|
21
21
|
import { inputGateNoticeFor } from '~/utils/inputGate'
|
|
22
|
+
import { composeRunOutcome, hasOutcomeToShow } from '~/utils/runOutcome'
|
|
22
23
|
|
|
23
24
|
const props = defineProps<{ block: Block }>()
|
|
24
25
|
|
|
@@ -55,6 +56,13 @@ const instance = computed(() => execution.getInstance(props.block.executionId))
|
|
|
55
56
|
// is that nothing will merge, and WHY is answered by the run's own notes and the merge decision.
|
|
56
57
|
const sandboxed = computed(() => isDryRun(instance.value?.mode))
|
|
57
58
|
|
|
59
|
+
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
60
|
+
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
61
|
+
const failedRun = computed(() => {
|
|
62
|
+
const run = agentRuns.byBlock[props.block.id]
|
|
63
|
+
return run && run.status === 'failed' ? run : null
|
|
64
|
+
})
|
|
65
|
+
|
|
58
66
|
// Nothing to show yet: no run, no failed run, no PR, and not awaiting a merge — render an
|
|
59
67
|
// empty state instead of a blank gap so the section reads as "no runs yet" rather than broken.
|
|
60
68
|
const isEmpty = computed(
|
|
@@ -76,17 +84,26 @@ const runFailed = computed(() => instance.value?.status === 'failed')
|
|
|
76
84
|
*/
|
|
77
85
|
const inputGateNotice = computed(() => inputGateNoticeFor(instance.value))
|
|
78
86
|
|
|
79
|
-
// A failed pipeline run surfaces the shared failure banner + retry — the
|
|
80
|
-
// execution failure surface that the old `pr_ready` flip used to hide.
|
|
81
|
-
const failedRun = computed(() => {
|
|
82
|
-
const run = agentRuns.byBlock[props.block.id]
|
|
83
|
-
return run && run.status === 'failed' ? run : null
|
|
84
|
-
})
|
|
85
|
-
|
|
86
87
|
// Failures from prior attempts, preserved across retries — shown regardless of the run's
|
|
87
88
|
// CURRENT status, so the error trail stays viewable after a restart clears the top banner.
|
|
88
89
|
const failureHistory = computed(() => agentRuns.byBlock[props.block.id]?.failureHistory ?? [])
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Whether this task has a result worth reading in product terms: a pull request, or a step that
|
|
93
|
+
* recorded evidence. Asked of the shared reduction rather than re-derived here, so the panel and
|
|
94
|
+
* the board card can never disagree about which runs have an outcome to open.
|
|
95
|
+
*
|
|
96
|
+
* The spec is deliberately not loaded for this check: it only ever adds TITLES to requirement
|
|
97
|
+
* rows, never sections, so nothing about whether there is something to show depends on it. The
|
|
98
|
+
* window loads it on open.
|
|
99
|
+
*/
|
|
100
|
+
const outcomeReadable = computed(() =>
|
|
101
|
+
hasOutcomeToShow(composeRunOutcome({ block: props.block, instance: instance.value ?? null })),
|
|
102
|
+
)
|
|
103
|
+
function openOutcome() {
|
|
104
|
+
ui.openOutcome(props.block.id, instance.value?.id ?? null)
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
const pr = computed(() => props.block.pullRequest)
|
|
91
108
|
/** A PR is merged once the block is `done`; otherwise it is open awaiting merge. */
|
|
92
109
|
const prMerged = computed(() => props.block.status === 'done')
|
|
@@ -605,6 +622,22 @@ async function mergePr() {
|
|
|
605
622
|
<!-- error trail of prior attempts (survives a retry/restart that cleared the banner) -->
|
|
606
623
|
<AgentFailureHistory :failures="failureHistory" />
|
|
607
624
|
|
|
625
|
+
<!-- Read the result: the outcome summary is the way in, and the pull request below is the
|
|
626
|
+
way to the diff. Offered whenever there is evidence or a PR to read, which includes a
|
|
627
|
+
merged task whose run instance is long gone. -->
|
|
628
|
+
<UButton
|
|
629
|
+
v-if="outcomeReadable"
|
|
630
|
+
color="primary"
|
|
631
|
+
variant="soft"
|
|
632
|
+
size="sm"
|
|
633
|
+
icon="i-lucide-clipboard-check"
|
|
634
|
+
block
|
|
635
|
+
data-testid="inspector-open-outcome"
|
|
636
|
+
@click="openOutcome"
|
|
637
|
+
>
|
|
638
|
+
{{ t('inspector.execution.readOutcome') }}
|
|
639
|
+
</UButton>
|
|
640
|
+
|
|
608
641
|
<!-- Open PR: link straight to it on GitHub -->
|
|
609
642
|
<div v-if="pr" class="space-y-2">
|
|
610
643
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
@@ -160,6 +160,11 @@ const STATUS_META = computed<Record<ExecutionInstance['status'], { label: string
|
|
|
160
160
|
const steps = computed(() => props.instance.steps)
|
|
161
161
|
const total = computed(() => steps.value.length)
|
|
162
162
|
|
|
163
|
+
// A failed run is no longer executing: a step left mid-flight (state still `working`,
|
|
164
|
+
// its container caught mid cold-boot) must stop looking live — no spinner, no pulse,
|
|
165
|
+
// no "spinning up container" phase.
|
|
166
|
+
const runFailed = computed(() => props.instance.status === 'failed')
|
|
167
|
+
|
|
163
168
|
// A shared 1s tick drives every step's live elapsed clock, so a step that hasn't yet
|
|
164
169
|
// emitted subtask counts still shows it is progressing rather than reading as hung.
|
|
165
170
|
const nowTick = useNowTick()
|
|
@@ -172,10 +177,6 @@ function stepElapsed(s: PipelineStep): string | null {
|
|
|
172
177
|
// human can see at a glance whether the fixer ran or was skipped.
|
|
173
178
|
const companionByStep = computed(() => steps.value.map((s) => gateCompanionFor(s, runFailed.value)))
|
|
174
179
|
|
|
175
|
-
// A failed run is no longer executing: a step left mid-flight (state still `working`,
|
|
176
|
-
// its container caught mid cold-boot) must stop looking live — no spinner, no pulse,
|
|
177
|
-
// no "spinning up container" phase.
|
|
178
|
-
const runFailed = computed(() => props.instance.status === 'failed')
|
|
179
180
|
/**
|
|
180
181
|
* A reviewer gate (requirements-review / clarity-review) folding the answers or
|
|
181
182
|
* re-reviewing in the durable driver: the step parks in `waiting_decision` but is actively
|
|
@@ -33,6 +33,9 @@ const { confirmAction, toastDone } = useConfirmAction()
|
|
|
33
33
|
/** Account-wide mode (single account scope) vs the default workspace/user toggle. */
|
|
34
34
|
const isAccount = computed(() => !!props.accountId)
|
|
35
35
|
|
|
36
|
+
/** Which store the form writes to: the shared workspace keys or the user's own. */
|
|
37
|
+
const scope = ref<'workspace' | 'user'>('workspace')
|
|
38
|
+
|
|
36
39
|
// "My keys" (user scope) are stored per-user, so they need a signed-in user. Block just
|
|
37
40
|
// that scope when there's none (a deployment without sign-in); workspace/account keys are
|
|
38
41
|
// unaffected. The scope toggle stays enabled so the user can switch back to a shared scope.
|
|
@@ -136,7 +139,6 @@ const PROVIDERS = computed(() =>
|
|
|
136
139
|
)
|
|
137
140
|
const ALL_PROVIDERS = computed(() => [...DIRECT_PROVIDERS.value, ...PROXY_PROVIDERS.value])
|
|
138
141
|
|
|
139
|
-
const scope = ref<'workspace' | 'user'>('workspace')
|
|
140
142
|
const provider = ref<ApiKeyProvider>(props.category === 'proxy' ? 'openrouter' : 'openai')
|
|
141
143
|
const label = ref('')
|
|
142
144
|
const key = ref('')
|
|
@@ -19,9 +19,6 @@ const { t, d } = useI18n()
|
|
|
19
19
|
// overlay behaviour.
|
|
20
20
|
const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop')
|
|
21
21
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
22
|
-
const headerTitle = computed(
|
|
23
|
-
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
24
|
-
)
|
|
25
22
|
const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
|
|
26
23
|
|
|
27
24
|
const instance = computed(() =>
|
|
@@ -33,6 +30,9 @@ const step = computed(() => {
|
|
|
33
30
|
})
|
|
34
31
|
const ralph = computed<RalphStepState | null>(() => step.value?.ralph ?? null)
|
|
35
32
|
const meta = computed(() => agentKindMeta('ralph'))
|
|
33
|
+
const headerTitle = computed(
|
|
34
|
+
() => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
35
|
+
)
|
|
36
36
|
|
|
37
37
|
// Iterations, newest-first for the timeline.
|
|
38
38
|
const attempts = computed(() => [...(ralph.value?.attemptLog ?? [])].reverse())
|
|
@@ -131,6 +131,20 @@ function buildFindings(): { text: string; structured: { view?: string; note: str
|
|
|
131
131
|
return { text: blocks.join('\n\n'), structured }
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// Degraded-basis approval guard (no capture / a fix landed after these shots): require an
|
|
135
|
+
// explicit "I reviewed this another way" acknowledgement before the one-click approve.
|
|
136
|
+
const ackDegraded = ref(false)
|
|
137
|
+
watch(
|
|
138
|
+
() => vc.value?.degradedReason ?? null,
|
|
139
|
+
() => {
|
|
140
|
+
ackDegraded.value = false
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
const needsAck = computed(() => !!vc.value?.degradedReason)
|
|
144
|
+
const canApprove = computed(
|
|
145
|
+
() => awaitingHuman.value && !busy.value && (!needsAck.value || ackDegraded.value),
|
|
146
|
+
)
|
|
147
|
+
|
|
134
148
|
async function approve() {
|
|
135
149
|
if (!blockId.value || !canApprove.value) return
|
|
136
150
|
await visualConfirm.approve(blockId.value)
|
|
@@ -169,20 +183,6 @@ async function onFilePicked(e: Event) {
|
|
|
169
183
|
uploadView.value = ''
|
|
170
184
|
if (fileInput.value) fileInput.value.value = ''
|
|
171
185
|
}
|
|
172
|
-
|
|
173
|
-
// Degraded-basis approval guard (no capture / a fix landed after these shots): require an
|
|
174
|
-
// explicit "I reviewed this another way" acknowledgement before the one-click approve.
|
|
175
|
-
const ackDegraded = ref(false)
|
|
176
|
-
watch(
|
|
177
|
-
() => vc.value?.degradedReason ?? null,
|
|
178
|
-
() => {
|
|
179
|
-
ackDegraded.value = false
|
|
180
|
-
},
|
|
181
|
-
)
|
|
182
|
-
const needsAck = computed(() => !!vc.value?.degradedReason)
|
|
183
|
-
const canApprove = computed(
|
|
184
|
-
() => awaitingHuman.value && !busy.value && (!needsAck.value || ackDegraded.value),
|
|
185
|
-
)
|
|
186
186
|
</script>
|
|
187
187
|
|
|
188
188
|
<template>
|
|
@@ -82,10 +82,18 @@ export function useRunDeepLink(): void {
|
|
|
82
82
|
if (!ready || applied) return
|
|
83
83
|
applied = true
|
|
84
84
|
if (link.blockId) ui.select(link.blockId)
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
85
|
+
// Three views are served: the run's outcome summary (the non-code answer to "what did this
|
|
86
|
+
// change"), the observability panel, and the Tester's result window (where the screenshots
|
|
87
|
+
// the report's environment-lifecycle section lists are rendered). An unknown view still
|
|
88
|
+
// lands the user on the right board and task rather than failing the navigation.
|
|
89
|
+
//
|
|
90
|
+
// `outcome` is passed the block as well as the run: it is the one of the three that stays
|
|
91
|
+
// readable on a task whose run the snapshot no longer carries, and a link followed weeks
|
|
92
|
+
// after the work merged is exactly that case. The engine emits no `view=outcome` link
|
|
93
|
+
// today (its report links a REVIEWER to the two run-scoped panels); this is the entry
|
|
94
|
+
// point for one, and for a URL a person shares.
|
|
95
|
+
if (link.view === 'outcome') ui.openRunOutcome(link.runId, link.blockId)
|
|
96
|
+
else if (link.view === 'observability') ui.openObservability(link.runId)
|
|
89
97
|
else if (link.view === 'test-evidence') ui.openTestEvidence(link.runId)
|
|
90
98
|
stop?.()
|
|
91
99
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Component } from 'vue'
|
|
2
2
|
import { defineModule } from '@modular-vue/core'
|
|
3
3
|
import { RESULT_VIEW_IDS, type ResultViewId } from '@cat-factory/contracts'
|
|
4
|
+
import OutcomeSummaryWindow from '~/components/outcome/OutcomeSummaryWindow.vue'
|
|
4
5
|
import RequirementsReviewWindow from '~/components/requirements/RequirementsReviewWindow.vue'
|
|
5
6
|
import ClarityReviewWindow from '~/components/clarity/ClarityReviewWindow.vue'
|
|
6
7
|
import BrainstormWindow from '~/components/brainstorm/BrainstormWindow.vue'
|
|
@@ -51,6 +52,9 @@ import type { ResultViewContribution } from './slots'
|
|
|
51
52
|
* that could ship. Consumer namespaced ids are validated separately by `pairById`.
|
|
52
53
|
*/
|
|
53
54
|
const BUILT_IN_RESULT_VIEWS: Record<ResultViewId, Component> = {
|
|
55
|
+
// The run's non-code outcome summary: what changed in product terms, with the captured
|
|
56
|
+
// evidence, and the diff one click away. RUN-keyed (no step), opened by `ui.openOutcome`.
|
|
57
|
+
outcome: OutcomeSummaryWindow,
|
|
54
58
|
'requirements-review': RequirementsReviewWindow,
|
|
55
59
|
'clarity-review': ClarityReviewWindow,
|
|
56
60
|
// Shared by both brainstorm stages (requirements + architecture); the window reads the stage.
|
package/app/pages/index.vue
CHANGED
|
@@ -285,6 +285,27 @@ watch(
|
|
|
285
285
|
{ immediate: true },
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
+
// Probe the GitHub integration as soon as a board is active (re-probe per board —
|
|
289
|
+
// connections are per workspace). The result drives the onboarding gate in the template
|
|
290
|
+
// before the board mounts, so an unconnected user can't slip past it. `ensureProbed`
|
|
291
|
+
// single-flights per board (app-startup initiative, item 12), so this and the SideBar's
|
|
292
|
+
// probe collapse to one request on a cold open instead of two.
|
|
293
|
+
watch(
|
|
294
|
+
() => workspace.workspaceId,
|
|
295
|
+
(id) => {
|
|
296
|
+
if (id) void github.ensureProbed()
|
|
297
|
+
},
|
|
298
|
+
{ immediate: true },
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Hard gate: the App is enabled on the backend but this workspace has no
|
|
302
|
+
// installation yet. `available === null` means the probe is still in flight.
|
|
303
|
+
// Both are declared here, ahead of the tutorial offer below, because that offer reads them
|
|
304
|
+
// from a watcher that runs synchronously during setup (`immediate: true`). Declared after
|
|
305
|
+
// it, they'd still be in their TDZ and the first run would throw.
|
|
306
|
+
const needsGitHubInstall = computed(() => github.available === true && !github.connected)
|
|
307
|
+
const githubProbePending = computed(() => github.available === null)
|
|
308
|
+
|
|
288
309
|
// Offer the tutorial on launch, once the board is up. Yields to every other startup
|
|
289
310
|
// surface — the GitHub onboarding gate and the advisory/onboarding modals above — so a
|
|
290
311
|
// first launch never stacks the tour prompt on top of a dialog that needs answering
|
|
@@ -347,24 +368,6 @@ useTutorialNudge()
|
|
|
347
368
|
// simply has nothing to talk to, and the browser-persisted store carries on alone.
|
|
348
369
|
useTutorialSync()
|
|
349
370
|
|
|
350
|
-
// Probe the GitHub integration as soon as a board is active (re-probe per board —
|
|
351
|
-
// connections are per workspace). The result drives the onboarding gate below
|
|
352
|
-
// before the board mounts, so an unconnected user can't slip past it. `ensureProbed`
|
|
353
|
-
// single-flights per board (app-startup initiative, item 12), so this and the SideBar's
|
|
354
|
-
// probe collapse to one request on a cold open instead of two.
|
|
355
|
-
watch(
|
|
356
|
-
() => workspace.workspaceId,
|
|
357
|
-
(id) => {
|
|
358
|
-
if (id) void github.ensureProbed()
|
|
359
|
-
},
|
|
360
|
-
{ immediate: true },
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
// Hard gate: the App is enabled on the backend but this workspace has no
|
|
364
|
-
// installation yet. `available === null` means the probe is still in flight.
|
|
365
|
-
const needsGitHubInstall = computed(() => github.available === true && !github.connected)
|
|
366
|
-
const githubProbePending = computed(() => github.available === null)
|
|
367
|
-
|
|
368
371
|
// Subscribe to the backend's real-time event stream and (re)connect whenever the
|
|
369
372
|
// active workspace changes. Runs advance durably server-side; progress arrives as
|
|
370
373
|
// pushed events rather than by polling.
|
package/app/stores/execution.ts
CHANGED
|
@@ -131,6 +131,12 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
131
131
|
} else instances.value.push(instance)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
const byId = computed(() => {
|
|
135
|
+
const map = new Map<string, ExecutionInstance>()
|
|
136
|
+
for (const e of instances.value) map.set(e.id, e)
|
|
137
|
+
return map
|
|
138
|
+
})
|
|
139
|
+
|
|
134
140
|
/**
|
|
135
141
|
* Run an action that returns a run's authoritative sub-state and apply that state to the cached
|
|
136
142
|
* run as an OPTIMISTIC ECHO — but only when the event stream has not delivered a newer revision
|
|
@@ -169,12 +175,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
169
175
|
return state
|
|
170
176
|
}
|
|
171
177
|
|
|
172
|
-
const byId = computed(() => {
|
|
173
|
-
const map = new Map<string, ExecutionInstance>()
|
|
174
|
-
for (const e of instances.value) map.set(e.id, e)
|
|
175
|
-
return map
|
|
176
|
-
})
|
|
177
|
-
|
|
178
178
|
function getInstance(id: string | null | undefined) {
|
|
179
179
|
return id ? byId.value.get(id) : undefined
|
|
180
180
|
}
|
|
@@ -113,6 +113,32 @@ export function createUiResultViews() {
|
|
|
113
113
|
stepDetail.value = { instanceId, stepIndex }
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Open a task's NON-CODE OUTCOME summary — what the run changed in product terms, with the
|
|
118
|
+
* evidence behind it, and the diff one click away. BLOCK-keyed rather than run-keyed because a
|
|
119
|
+
* merged task keeps its pull request long after its run instance is gone, and that task is
|
|
120
|
+
* exactly the one somebody comes back to read. The run rides along when there is one, which is
|
|
121
|
+
* where every piece of evidence comes from; `stepIndex` stays null because the summary is
|
|
122
|
+
* composed from the WHOLE run and there is no step for it to be about.
|
|
123
|
+
*/
|
|
124
|
+
function openOutcome(blockId: string, instanceId: string | null = null) {
|
|
125
|
+
resultView.value = { view: 'outcome', blockId, instanceId, stepIndex: null }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Open the outcome summary from a caller that knows the RUN: the `outcome` deep link.
|
|
130
|
+
*
|
|
131
|
+
* The run id is a LOOKUP here, not the key — the window is block-keyed (above) — so a link
|
|
132
|
+
* naming its block opens on it even when the store never hydrated that run, which is the
|
|
133
|
+
* normal state of following a link into a task that finished long ago. Falling back to the
|
|
134
|
+
* run's own `blockId` keeps a link that carries only `run=` working. Only a link that
|
|
135
|
+
* resolves neither is a silent no-op, matching the run-step openers.
|
|
136
|
+
*/
|
|
137
|
+
function openRunOutcome(instanceId: string, blockId: string | null = null) {
|
|
138
|
+
const resolved = blockId ?? useExecutionStore().getInstance(instanceId)?.blockId ?? null
|
|
139
|
+
if (resolved) openOutcome(resolved, instanceId)
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
function openRequirementReview(blockId: string) {
|
|
117
143
|
resultView.value = { view: 'requirements-review', blockId, instanceId: null, stepIndex: null }
|
|
118
144
|
}
|
|
@@ -185,6 +211,8 @@ export function createUiResultViews() {
|
|
|
185
211
|
openForkDecision,
|
|
186
212
|
openPrReview,
|
|
187
213
|
openTestEvidence,
|
|
214
|
+
openOutcome,
|
|
215
|
+
openRunOutcome,
|
|
188
216
|
closeResultView,
|
|
189
217
|
closeRequirementReview,
|
|
190
218
|
openStepDetail,
|
|
@@ -134,4 +134,59 @@ describe('dispatchStepView routing', () => {
|
|
|
134
134
|
expect(ui.stepDetail).toBeNull()
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
|
+
|
|
138
|
+
// The outcome summary is the RUN's, not a step's, and it has two entry points: the board and
|
|
139
|
+
// inspector know the block (and a merged task's run may be gone), while a deep link knows only
|
|
140
|
+
// the run id.
|
|
141
|
+
describe('openOutcome', () => {
|
|
142
|
+
it('opens block-keyed with no step, and with no run when the task has none', () => {
|
|
143
|
+
ui.openOutcome('b1')
|
|
144
|
+
|
|
145
|
+
expect(ui.resultView).toEqual({
|
|
146
|
+
view: 'outcome',
|
|
147
|
+
blockId: 'b1',
|
|
148
|
+
instanceId: null,
|
|
149
|
+
stepIndex: null,
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('carries the run when the caller knows it', () => {
|
|
154
|
+
ui.openOutcome('b1', 'e1')
|
|
155
|
+
|
|
156
|
+
expect(ui.resultView).toMatchObject({ view: 'outcome', blockId: 'b1', instanceId: 'e1' })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('resolves the block from the run for a run-only caller (the deep link)', () => {
|
|
160
|
+
execution.hydrate([instance('e1', 'b1', [{ agentKind: 'coder' }])], 'ws1')
|
|
161
|
+
|
|
162
|
+
ui.openRunOutcome('e1')
|
|
163
|
+
|
|
164
|
+
expect(ui.resultView).toEqual({
|
|
165
|
+
view: 'outcome',
|
|
166
|
+
blockId: 'b1',
|
|
167
|
+
instanceId: 'e1',
|
|
168
|
+
stepIndex: null,
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// The link the deep-link consumer passes carries BOTH ids, and the run is only a lookup:
|
|
173
|
+
// following one into a task that finished long ago is the normal case, and the snapshot
|
|
174
|
+
// that hydrates the board is not obliged to still carry that run.
|
|
175
|
+
it('opens on the block the link names even when the run was never hydrated', () => {
|
|
176
|
+
ui.openRunOutcome('missing', 'b1')
|
|
177
|
+
|
|
178
|
+
expect(ui.resultView).toEqual({
|
|
179
|
+
view: 'outcome',
|
|
180
|
+
blockId: 'b1',
|
|
181
|
+
instanceId: 'missing',
|
|
182
|
+
stepIndex: null,
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('does nothing for a run the store has not hydrated and a link with no block', () => {
|
|
187
|
+
ui.openRunOutcome('missing')
|
|
188
|
+
|
|
189
|
+
expect(ui.resultView).toBeNull()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
137
192
|
})
|