@cat-factory/app 0.280.0 → 0.280.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 +26 -1
- package/app/components/board/TaskDependencyEdges.vue +26 -15
- package/app/components/board/nodes/TaskCard.vue +6 -1
- package/app/components/common/AsyncViewError.vue +30 -0
- package/app/components/panels/AgentStepDetail.vue +651 -659
- package/app/components/panels/InspectorPanel.vue +6 -0
- package/app/composables/useBoardActivity.ts +62 -6
- package/app/composables/useTaskExpansion.ts +10 -25
- package/app/docs/consumer-extensions.md +9 -0
- package/app/modular/result-views.ts +58 -21
- package/app/pages/index.vue +68 -64
- package/app/utils/asyncView.ts +24 -0
- package/app/utils/blockRects.spec.ts +82 -0
- package/app/utils/blockRects.ts +61 -0
- package/app/utils/boardWakeGate.spec.ts +101 -0
- package/app/utils/boardWakeGate.ts +78 -0
- package/i18n/locales/de.json +5 -0
- package/i18n/locales/en.json +5 -0
- package/i18n/locales/es.json +5 -0
- package/i18n/locales/fr.json +5 -0
- package/i18n/locales/he.json +5 -0
- package/i18n/locales/it.json +5 -0
- package/i18n/locales/ja.json +5 -0
- package/i18n/locales/pl.json +5 -0
- package/i18n/locales/tr.json +5 -0
- package/i18n/locales/uk.json +5 -0
- package/package.json +1 -1
|
@@ -332,9 +332,15 @@ const showOriginalDescription = ref(false)
|
|
|
332
332
|
with the panel on top; the region now paints above at `z-40`, which only swaps which side
|
|
333
333
|
loses). Sitting the panel below fixes it whichever way the stacking goes, and needs no
|
|
334
334
|
left/right arithmetic to stay correct under RTL. -->
|
|
335
|
+
<!-- `data-inspector-block` names WHICH block the panel is showing, so a caller can tell a
|
|
336
|
+
panel left open on the previous selection from one that followed a new click. Deliberately
|
|
337
|
+
NOT `data-block-id`: that attribute is the board's card selector, which the canvas drivers
|
|
338
|
+
measure geometry through (`utils/blockRects.ts`), and a panel answering to it would offer
|
|
339
|
+
the arrows a rect that is not on the canvas at all. -->
|
|
335
340
|
<div
|
|
336
341
|
v-if="block && statusMeta && typeMeta"
|
|
337
342
|
data-testid="inspector-panel"
|
|
343
|
+
:data-inspector-block="block.id"
|
|
338
344
|
class="fixed inset-x-0 bottom-0 z-20 overflow-hidden rounded-t-2xl border border-slate-700 bg-slate-900/95 shadow-2xl backdrop-blur lg:absolute lg:inset-x-auto lg:bottom-auto lg:end-4 lg:top-16 lg:w-80 lg:rounded-2xl"
|
|
339
345
|
>
|
|
340
346
|
<div class="h-1.5 w-full" :style="{ backgroundColor: statusMeta.color }" />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { inject, onBeforeUnmount, onMounted, provide, type InjectionKey, type Ref } from 'vue'
|
|
2
|
+
import { createWakeGate } from '~/utils/boardWakeGate'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The board's shared "something may have moved" pulse.
|
|
@@ -16,9 +17,13 @@ import { inject, onBeforeUnmount, onMounted, provide, type InjectionKey, type Re
|
|
|
16
17
|
* - a `MutationObserver` over the canvas subtree, watching structure plus `style` / `class`.
|
|
17
18
|
* That is every Vue-driven render change on the board, Vue Flow's own pan/zoom transform
|
|
18
19
|
* included. Attribute changes the drivers themselves write (`x1`/`y1` on the edge overlay)
|
|
19
|
-
* are outside the filter, so a driver cannot pulse itself awake forever.
|
|
20
|
+
* are outside the filter, so a driver cannot pulse itself awake forever. These wakes are
|
|
21
|
+
* RATE-LIMITED (see `boardWakeGate`): a live board re-renders its cards on every execution
|
|
22
|
+
* event, and admitting each one kept the measuring loops from ever parking on exactly the
|
|
23
|
+
* board where measuring costs the most. The gesture and camera signals below are admitted
|
|
24
|
+
* unthrottled, so nothing the user is actually moving waits on an interval.
|
|
20
25
|
* - a `ResizeObserver` on the canvas, plus window `resize`: layout changes with no mutation.
|
|
21
|
-
* - pointer, wheel and scroll gestures on the
|
|
26
|
+
* - pointer, wheel and scroll gestures, listened for on the WINDOW: the user moving something.
|
|
22
27
|
*
|
|
23
28
|
* What it does NOT catch is a reflow with no mutation and no gesture, such as a late-loading
|
|
24
29
|
* image or font resizing a card. Those settle on the next pulse of any kind.
|
|
@@ -28,6 +33,14 @@ export type BoardActivity = {
|
|
|
28
33
|
subscribe: (onPulse: () => void) => () => void
|
|
29
34
|
/** Fire the pulse from a signal the observers above cannot see. */
|
|
30
35
|
pulse: () => void
|
|
36
|
+
/**
|
|
37
|
+
* Where the pointer last was over the canvas (viewport coordinates), or null once it left.
|
|
38
|
+
*
|
|
39
|
+
* Owned here because the pulse already listens for the same gestures: a driver that wants the
|
|
40
|
+
* position registered a SECOND `pointermove` listener on the same element to learn what this
|
|
41
|
+
* one had just seen. Read inside a measurement pass, never subscribed to.
|
|
42
|
+
*/
|
|
43
|
+
pointer: () => { x: number; y: number } | null
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
const boardActivityKey: InjectionKey<BoardActivity> = Symbol('boardActivity')
|
|
@@ -42,19 +55,62 @@ export function provideBoardActivity(container: Ref<HTMLElement | null>): BoardA
|
|
|
42
55
|
for (const onPulse of subscribers) onPulse()
|
|
43
56
|
}
|
|
44
57
|
|
|
58
|
+
// Renders reach the pulse through the gate; everything the user is moving goes straight to it.
|
|
59
|
+
const renderWakes = createWakeGate({
|
|
60
|
+
wake: pulse,
|
|
61
|
+
// `window.setTimeout` rather than the bare global: the DOM overload returns the numeric
|
|
62
|
+
// handle the gate's scheduler is typed on, where Node's returns a `Timeout` object.
|
|
63
|
+
scheduler: {
|
|
64
|
+
schedule: (run, delayMs) => window.setTimeout(run, delayMs),
|
|
65
|
+
cancel: (handle) => window.clearTimeout(handle),
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
let pointer: { x: number; y: number } | null = null
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Track the pointer and pulse, in that order, off the SAME listener.
|
|
73
|
+
*
|
|
74
|
+
* `pointerleave` does not bubble, but a CAPTURE-phase listener sees one fired at any element
|
|
75
|
+
* below it, and the pointer moving from a card onto the canvas around it is exactly that
|
|
76
|
+
* event. So only the canvas's OWN leave clears the position; treating a descendant's as "the
|
|
77
|
+
* pointer is gone" would collapse the hovered card the moment the pointer crossed one of its
|
|
78
|
+
* inner elements. That check is on the TARGET, so it reads the same from the window as it did
|
|
79
|
+
* from the canvas.
|
|
80
|
+
*/
|
|
81
|
+
const onGesture = (event: Event) => {
|
|
82
|
+
if (event.type === 'pointerleave') {
|
|
83
|
+
if (event.target === container.value) pointer = null
|
|
84
|
+
} else if (event.type === 'pointermove' || event.type === 'pointerdown') {
|
|
85
|
+
const { clientX, clientY } = event as PointerEvent
|
|
86
|
+
pointer = { x: clientX, y: clientY }
|
|
87
|
+
}
|
|
88
|
+
pulse()
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
const activity: BoardActivity = {
|
|
46
92
|
subscribe(onPulse) {
|
|
47
93
|
subscribers.add(onPulse)
|
|
48
94
|
return () => subscribers.delete(onPulse)
|
|
49
95
|
},
|
|
50
96
|
pulse,
|
|
97
|
+
pointer: () => pointer,
|
|
51
98
|
}
|
|
52
99
|
provide(boardActivityKey, activity)
|
|
53
100
|
|
|
54
|
-
const mutations = new MutationObserver(
|
|
101
|
+
const mutations = new MutationObserver(renderWakes.request)
|
|
55
102
|
const resizes = new ResizeObserver(pulse)
|
|
56
103
|
// `scroll` does not bubble, so it is caught in the capture phase; the gestures are
|
|
57
104
|
// passive listeners because the pulse never wants to cancel one.
|
|
105
|
+
//
|
|
106
|
+
// They are bound to the WINDOW rather than to the canvas, because a drag does not end at the
|
|
107
|
+
// canvas's edge: `useBlockDrag` tracks the pointer on the window precisely so a card keeps
|
|
108
|
+
// following it, and the toolbar region and the inspector are SIBLINGS painted over the canvas,
|
|
109
|
+
// not descendants of it. Bound to the canvas, a drag whose cursor crossed one of them stopped
|
|
110
|
+
// delivering the gesture that keeps the measuring loops awake, and the arrows fell back to the
|
|
111
|
+
// rate-limited mutation wake for as long as the cursor was over it: a visible lag in the one
|
|
112
|
+
// interaction this pulse exists to keep smooth. Capture on the window sees every one of those
|
|
113
|
+
// events wherever it is dispatched, so nothing else about the handler changes.
|
|
58
114
|
const gestures = [
|
|
59
115
|
'pointerdown',
|
|
60
116
|
'pointermove',
|
|
@@ -77,15 +133,15 @@ export function provideBoardActivity(container: Ref<HTMLElement | null>): BoardA
|
|
|
77
133
|
attributeFilter: ['style', 'class'],
|
|
78
134
|
})
|
|
79
135
|
resizes.observe(el)
|
|
80
|
-
for (const type of gestures)
|
|
136
|
+
for (const type of gestures) window.addEventListener(type, onGesture, gestureOptions)
|
|
81
137
|
window.addEventListener('resize', pulse)
|
|
82
138
|
})
|
|
83
139
|
|
|
84
140
|
onBeforeUnmount(() => {
|
|
85
141
|
mutations.disconnect()
|
|
86
142
|
resizes.disconnect()
|
|
87
|
-
|
|
88
|
-
for (const type of gestures)
|
|
143
|
+
renderWakes.cancel()
|
|
144
|
+
for (const type of gestures) window.removeEventListener(type, onGesture, gestureOptions)
|
|
89
145
|
window.removeEventListener('resize', pulse)
|
|
90
146
|
subscribers.clear()
|
|
91
147
|
})
|
|
@@ -3,6 +3,7 @@ import { onMounted, onBeforeUnmount } from 'vue'
|
|
|
3
3
|
import { lodAtLeast } from '~/composables/useSemanticZoom'
|
|
4
4
|
import { onBoardActivity, type BoardActivity } from '~/composables/useBoardActivity'
|
|
5
5
|
import { useSettlingRaf } from '~/composables/useSettlingRaf'
|
|
6
|
+
import { measureBlocks, type BlockMeasurements } from '~/utils/blockRects'
|
|
6
7
|
import { headerDistanceSq, type Rect } from '~/utils/taskExpansionRanking'
|
|
7
8
|
|
|
8
9
|
function intersects(a: Rect, b: Rect) {
|
|
@@ -54,21 +55,6 @@ export function useTaskExpansion(container: Ref<HTMLElement | null>, activity: B
|
|
|
54
55
|
// card is still tested at its expanded extent and stays denied. Stable.
|
|
55
56
|
const expandedHeight = new Map<string, number>()
|
|
56
57
|
|
|
57
|
-
// Last pointer position over the board (viewport coords), or null when the pointer has
|
|
58
|
-
// left it. The card under the pointer is expanded on hover (see `hoveredTaskId`).
|
|
59
|
-
let pointer: { x: number; y: number } | null = null
|
|
60
|
-
function onPointerMove(e: PointerEvent) {
|
|
61
|
-
pointer = { x: e.clientX, y: e.clientY }
|
|
62
|
-
}
|
|
63
|
-
function onPointerLeave() {
|
|
64
|
-
pointer = null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function rectOf(id: string): DOMRect | null {
|
|
68
|
-
const el = document.querySelector(`[data-block-id="${id}"]`) as HTMLElement | null
|
|
69
|
-
return el ? el.getBoundingClientRect() : null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
58
|
// The task whose card is topmost at the pointer, or null. Using elementFromPoint (not a
|
|
73
59
|
// rect test) means an open pipeline stacked above a neighbour wins the hit, so hovering
|
|
74
60
|
// a region obscured by another pipeline doesn't switch to the card hidden beneath it.
|
|
@@ -77,6 +63,9 @@ export function useTaskExpansion(container: Ref<HTMLElement | null>, activity: B
|
|
|
77
63
|
// a frame, a module, or a task with no run expands to nothing, and granting it would
|
|
78
64
|
// still lift an empty card over its neighbours (see LaneTask's z-index).
|
|
79
65
|
function hoveredTaskId(): string | null {
|
|
66
|
+
// Where the pointer is comes from the pulse, which already listens for the same gestures on
|
|
67
|
+
// the same element (see `BoardActivity.pointer`).
|
|
68
|
+
const pointer = activity.pointer()
|
|
80
69
|
if (!pointer) return null
|
|
81
70
|
const hit = document.elementFromPoint(pointer.x, pointer.y)
|
|
82
71
|
const id = hit?.closest('[data-block-id]')?.getAttribute('data-block-id') ?? null
|
|
@@ -107,6 +96,8 @@ export function useTaskExpansion(container: Ref<HTMLElement | null>, activity: B
|
|
|
107
96
|
}
|
|
108
97
|
const view = container.value?.getBoundingClientRect()
|
|
109
98
|
if (!view) return changed
|
|
99
|
+
// One DOM query for the whole sweep instead of one per candidate task (see `measureBlocks`).
|
|
100
|
+
const blocks: BlockMeasurements = measureBlocks()
|
|
110
101
|
const cx = view.left + view.width / 2
|
|
111
102
|
const cy = view.top + view.height / 2
|
|
112
103
|
|
|
@@ -115,8 +106,9 @@ export function useTaskExpansion(container: Ref<HTMLElement | null>, activity: B
|
|
|
115
106
|
for (const t of board.allTasks) {
|
|
116
107
|
// Only tasks whose run actually has steps would expand a pipeline list.
|
|
117
108
|
if (!execution.getByBlock(t.id)?.steps.length) continue
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
109
|
+
const el = blocks.elementFor(t.id)
|
|
110
|
+
if (!el) continue
|
|
111
|
+
const rect = blocks.rectFor(el)
|
|
120
112
|
liveIds.add(t.id)
|
|
121
113
|
// While a card is granted it's rendered expanded, so its live height is its
|
|
122
114
|
// expanded footprint — cache it. A denied card keeps its last cached value.
|
|
@@ -166,19 +158,12 @@ export function useTaskExpansion(container: Ref<HTMLElement | null>, activity: B
|
|
|
166
158
|
}
|
|
167
159
|
|
|
168
160
|
const { poke } = useSettlingRaf(recompute)
|
|
169
|
-
// The
|
|
170
|
-
// same gestures) is what schedules the frame that acts on it.
|
|
161
|
+
// The pulse both records where the pointer is and schedules the frame that acts on it.
|
|
171
162
|
onBoardActivity(activity, poke)
|
|
172
163
|
onMounted(() => {
|
|
173
164
|
store.setDriverActive(true)
|
|
174
|
-
const el = container.value
|
|
175
|
-
el?.addEventListener('pointermove', onPointerMove)
|
|
176
|
-
el?.addEventListener('pointerleave', onPointerLeave)
|
|
177
165
|
})
|
|
178
166
|
onBeforeUnmount(() => {
|
|
179
|
-
const el = container.value
|
|
180
|
-
el?.removeEventListener('pointermove', onPointerMove)
|
|
181
|
-
el?.removeEventListener('pointerleave', onPointerLeave)
|
|
182
167
|
store.setDriverActive(false)
|
|
183
168
|
})
|
|
184
169
|
}
|
|
@@ -102,6 +102,15 @@ An unpaired id degrades to the generic prose panel (a dev-console warning names
|
|
|
102
102
|
id); a structured kind with no bespoke window gets the built-in `generic-structured` viewer
|
|
103
103
|
for free.
|
|
104
104
|
|
|
105
|
+
**Contribute the component asynchronously.** Every first-party window is registered as
|
|
106
|
+
`defineAsyncView(() => import('...'))`, so its code is fetched on the click that opens it rather
|
|
107
|
+
than on every board load; an async component is an ordinary `Component`, so the slot entry and the
|
|
108
|
+
host's mount are identical either way. A window is a modal opened deliberately, so copy that shape
|
|
109
|
+
unless yours is small enough not to matter. Prefer the seam over a bare `defineAsyncComponent`: it
|
|
110
|
+
attaches the shared failure notice, so a chunk that 404s (which is what a deploy landing under an
|
|
111
|
+
open tab makes of every chunk the session had not yet fetched) states itself instead of opening
|
|
112
|
+
your window onto a blank screen.
|
|
113
|
+
|
|
105
114
|
### Navigation
|
|
106
115
|
|
|
107
116
|
A consumer nav item carries its own `run` closure (first-party items use a typed `action`
|
|
@@ -1,28 +1,65 @@
|
|
|
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'
|
|
5
|
-
import RequirementsReviewWindow from '~/components/requirements/RequirementsReviewWindow.vue'
|
|
6
|
-
import ClarityReviewWindow from '~/components/clarity/ClarityReviewWindow.vue'
|
|
7
|
-
import BrainstormWindow from '~/components/brainstorm/BrainstormWindow.vue'
|
|
8
|
-
import TestReportWindow from '~/components/testing/TestReportWindow.vue'
|
|
9
|
-
import HumanTestWindow from '~/components/humanTest/HumanTestWindow.vue'
|
|
10
|
-
import VisualConfirmationWindow from '~/components/visualConfirm/VisualConfirmationWindow.vue'
|
|
11
|
-
import GateResultView from '~/components/gates/GateResultView.vue'
|
|
12
|
-
import ConsensusSessionWindow from '~/components/consensus/ConsensusSessionWindow.vue'
|
|
13
|
-
import GenericStructuredResultView from '~/components/panels/GenericStructuredResultView.vue'
|
|
14
|
-
import ServiceSpecWindow from '~/components/spec/ServiceSpecWindow.vue'
|
|
15
|
-
import FollowUpWindow from '~/components/followUp/FollowUpWindow.vue'
|
|
16
|
-
import BinaryCandidatesWindow from '~/components/binaryCandidates/BinaryCandidatesWindow.vue'
|
|
17
|
-
import ForkDecisionWindow from '~/components/forkDecision/ForkDecisionWindow.vue'
|
|
18
|
-
import PrReviewWindow from '~/components/prReview/PrReviewWindow.vue'
|
|
19
|
-
import MergerResultView from '~/components/panels/MergerResultView.vue'
|
|
20
|
-
import InitiativeTrackerWindow from '~/components/initiative/InitiativeTrackerWindow.vue'
|
|
21
|
-
import InitiativePlanningWindow from '~/components/initiative/InitiativePlanningWindow.vue'
|
|
22
|
-
import DocInterviewWindow from '~/components/docs/DocInterviewWindow.vue'
|
|
23
|
-
import RalphLoopResultView from '~/components/ralph/RalphLoopResultView.vue'
|
|
24
|
-
import JudgeResultView from '~/components/judge/JudgeResultView.vue'
|
|
25
4
|
import type { ResultViewContribution } from './slots'
|
|
5
|
+
import { defineAsyncView } from '~/utils/asyncView'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Every built-in window is CODE-SPLIT: a result window opens on a deliberate click (an agent
|
|
9
|
+
* step, a gate, an outcome card), and there are more than twenty of them, so importing the
|
|
10
|
+
* catalog statically put all of them plus their dependencies (markdown-it, the review windows'
|
|
11
|
+
* prose readers) into the initial bundle of a board that may open none. `defineAsyncView`
|
|
12
|
+
* returns an ordinary `Component`, so the slot entry, the exhaustiveness check below and
|
|
13
|
+
* `StepResultViewHost`'s `<component :is>` mount are unchanged; only the fetch moves to the
|
|
14
|
+
* open. It wraps `defineAsyncComponent` with the shared failure notice, so a window whose chunk
|
|
15
|
+
* 404s after a deploy says so rather than opening onto nothing. A consumer window may be
|
|
16
|
+
* contributed either way, and should copy this one.
|
|
17
|
+
*/
|
|
18
|
+
const OutcomeSummaryWindow = defineAsyncView(
|
|
19
|
+
() => import('~/components/outcome/OutcomeSummaryWindow.vue'),
|
|
20
|
+
)
|
|
21
|
+
const RequirementsReviewWindow = defineAsyncView(
|
|
22
|
+
() => import('~/components/requirements/RequirementsReviewWindow.vue'),
|
|
23
|
+
)
|
|
24
|
+
const ClarityReviewWindow = defineAsyncView(
|
|
25
|
+
() => import('~/components/clarity/ClarityReviewWindow.vue'),
|
|
26
|
+
)
|
|
27
|
+
const BrainstormWindow = defineAsyncView(
|
|
28
|
+
() => import('~/components/brainstorm/BrainstormWindow.vue'),
|
|
29
|
+
)
|
|
30
|
+
const TestReportWindow = defineAsyncView(() => import('~/components/testing/TestReportWindow.vue'))
|
|
31
|
+
const HumanTestWindow = defineAsyncView(() => import('~/components/humanTest/HumanTestWindow.vue'))
|
|
32
|
+
const VisualConfirmationWindow = defineAsyncView(
|
|
33
|
+
() => import('~/components/visualConfirm/VisualConfirmationWindow.vue'),
|
|
34
|
+
)
|
|
35
|
+
const GateResultView = defineAsyncView(() => import('~/components/gates/GateResultView.vue'))
|
|
36
|
+
const ConsensusSessionWindow = defineAsyncView(
|
|
37
|
+
() => import('~/components/consensus/ConsensusSessionWindow.vue'),
|
|
38
|
+
)
|
|
39
|
+
const GenericStructuredResultView = defineAsyncView(
|
|
40
|
+
() => import('~/components/panels/GenericStructuredResultView.vue'),
|
|
41
|
+
)
|
|
42
|
+
const ServiceSpecWindow = defineAsyncView(() => import('~/components/spec/ServiceSpecWindow.vue'))
|
|
43
|
+
const FollowUpWindow = defineAsyncView(() => import('~/components/followUp/FollowUpWindow.vue'))
|
|
44
|
+
const BinaryCandidatesWindow = defineAsyncView(
|
|
45
|
+
() => import('~/components/binaryCandidates/BinaryCandidatesWindow.vue'),
|
|
46
|
+
)
|
|
47
|
+
const ForkDecisionWindow = defineAsyncView(
|
|
48
|
+
() => import('~/components/forkDecision/ForkDecisionWindow.vue'),
|
|
49
|
+
)
|
|
50
|
+
const PrReviewWindow = defineAsyncView(() => import('~/components/prReview/PrReviewWindow.vue'))
|
|
51
|
+
const MergerResultView = defineAsyncView(() => import('~/components/panels/MergerResultView.vue'))
|
|
52
|
+
const InitiativeTrackerWindow = defineAsyncView(
|
|
53
|
+
() => import('~/components/initiative/InitiativeTrackerWindow.vue'),
|
|
54
|
+
)
|
|
55
|
+
const InitiativePlanningWindow = defineAsyncView(
|
|
56
|
+
() => import('~/components/initiative/InitiativePlanningWindow.vue'),
|
|
57
|
+
)
|
|
58
|
+
const DocInterviewWindow = defineAsyncView(() => import('~/components/docs/DocInterviewWindow.vue'))
|
|
59
|
+
const RalphLoopResultView = defineAsyncView(
|
|
60
|
+
() => import('~/components/ralph/RalphLoopResultView.vue'),
|
|
61
|
+
)
|
|
62
|
+
const JudgeResultView = defineAsyncView(() => import('~/components/judge/JudgeResultView.vue'))
|
|
26
63
|
|
|
27
64
|
/**
|
|
28
65
|
* The first-party result-view registry (slice 2 of the modular-vue adoption —
|
package/app/pages/index.vue
CHANGED
|
@@ -8,7 +8,6 @@ import TranslationWarningBanner from '~/components/layout/TranslationWarningBann
|
|
|
8
8
|
import PipelineBuilder from '~/components/pipeline/PipelineBuilder.vue'
|
|
9
9
|
import InspectorPanel from '~/components/panels/InspectorPanel.vue'
|
|
10
10
|
import DecisionModal from '~/components/panels/DecisionModal.vue'
|
|
11
|
-
import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
|
|
12
11
|
import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
|
|
13
12
|
import AppOverlayHost from '~/components/panels/AppOverlayHost.vue'
|
|
14
13
|
import AddTaskModal from '~/components/board/AddTaskModal.vue'
|
|
@@ -18,126 +17,127 @@ import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
|
|
|
18
17
|
import CommandBar from '~/components/layout/CommandBar.vue'
|
|
19
18
|
import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
|
|
20
19
|
import ConfirmDialog from '~/components/common/ConfirmDialog.vue'
|
|
20
|
+
import { defineAsyncView } from '~/utils/asyncView'
|
|
21
21
|
import KeyboardShortcutsHelp from '~/components/common/KeyboardShortcutsHelp.vue'
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
23
|
+
// The step-detail reader, mounted only while a step is open. It is the one always-mounted
|
|
24
|
+
// surface whose chunk dominated the initial bundle: its prose reader pulls markdown-it (~200 kB
|
|
25
|
+
// raw of the ~2.1 MB eager graph) purely to render an agent's output, which nothing can be
|
|
26
|
+
// reading until a step has been opened. Store-gated like the panels below, so the reader loads
|
|
27
|
+
// on the click that needs it.
|
|
28
|
+
const AgentStepDetail = defineAsyncView(() => import('~/components/panels/AgentStepDetail.vue'))
|
|
29
|
+
|
|
30
|
+
// Heavy, rarely-open panels, code-split into their own chunks via `defineAsyncView` and
|
|
31
|
+
// mounted only while their ui open-flag is set (the v-if gates in the template), so they stay
|
|
32
|
+
// out of the initial bundle and don't run setup/watchers while closed. The seam (rather than a
|
|
33
|
+
// bare `defineAsyncComponent`) is what makes a chunk that 404s after a deploy say so.
|
|
34
|
+
const ObservabilityPanel = defineAsyncView(
|
|
27
35
|
() => import('~/components/panels/ObservabilityPanel.vue'),
|
|
28
36
|
)
|
|
29
|
-
const OperatorDashboardPanel =
|
|
37
|
+
const OperatorDashboardPanel = defineAsyncView(
|
|
30
38
|
() => import('~/components/panels/OperatorDashboardPanel.vue'),
|
|
31
39
|
)
|
|
32
|
-
const ReportsPanel =
|
|
33
|
-
const KaizenPanel =
|
|
40
|
+
const ReportsPanel = defineAsyncView(() => import('~/components/panels/ReportsPanel.vue'))
|
|
41
|
+
const KaizenPanel = defineAsyncView(() => import('~/components/kaizen/KaizenPanel.vue'))
|
|
34
42
|
// Occasional, externally store-gated surfaces — deferred to their own chunks like the
|
|
35
43
|
// sibling document modals above. Each mounts only while its ui open-flag is set, so it
|
|
36
44
|
// loads on first open instead of bloating the initial bundle.
|
|
37
|
-
const BlockFocusView =
|
|
38
|
-
const TaskSourceConnectModal =
|
|
45
|
+
const BlockFocusView = defineAsyncView(() => import('~/components/focus/BlockFocusView.vue'))
|
|
46
|
+
const TaskSourceConnectModal = defineAsyncView(
|
|
39
47
|
() => import('~/components/tasks/TaskSourceConnectModal.vue'),
|
|
40
48
|
)
|
|
41
|
-
const TaskImportModal =
|
|
42
|
-
const BugHuntModal =
|
|
43
|
-
const RecurringPipelineModal =
|
|
49
|
+
const TaskImportModal = defineAsyncView(() => import('~/components/tasks/TaskImportModal.vue'))
|
|
50
|
+
const BugHuntModal = defineAsyncView(() => import('~/components/tasks/BugHuntModal.vue'))
|
|
51
|
+
const RecurringPipelineModal = defineAsyncView(
|
|
44
52
|
() => import('~/components/board/RecurringPipelineModal.vue'),
|
|
45
53
|
)
|
|
46
|
-
const DocumentSourceConnectModal =
|
|
54
|
+
const DocumentSourceConnectModal = defineAsyncView(
|
|
47
55
|
() => import('~/components/documents/DocumentSourceConnectModal.vue'),
|
|
48
56
|
)
|
|
49
|
-
const DocumentImportModal =
|
|
57
|
+
const DocumentImportModal = defineAsyncView(
|
|
50
58
|
() => import('~/components/documents/DocumentImportModal.vue'),
|
|
51
59
|
)
|
|
52
|
-
const DocumentTemplatesModal =
|
|
60
|
+
const DocumentTemplatesModal = defineAsyncView(
|
|
53
61
|
() => import('~/components/documents/DocumentTemplatesModal.vue'),
|
|
54
62
|
)
|
|
55
|
-
const SpawnPreviewModal =
|
|
63
|
+
const SpawnPreviewModal = defineAsyncView(
|
|
56
64
|
() => import('~/components/documents/SpawnPreviewModal.vue'),
|
|
57
65
|
)
|
|
58
|
-
const StartFromDesignModal =
|
|
66
|
+
const StartFromDesignModal = defineAsyncView(
|
|
59
67
|
() => import('~/components/documents/StartFromDesignModal.vue'),
|
|
60
68
|
)
|
|
61
|
-
const BootstrapModal =
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
const AddServiceFromRepoModal = defineAsyncComponent(
|
|
69
|
+
const BootstrapModal = defineAsyncView(() => import('~/components/bootstrap/BootstrapModal.vue'))
|
|
70
|
+
const AddServiceFromRepoModal = defineAsyncView(
|
|
65
71
|
() => import('~/components/github/AddServiceFromRepoModal.vue'),
|
|
66
72
|
)
|
|
67
|
-
const GitHubPanel =
|
|
68
|
-
const SlackPanel =
|
|
69
|
-
const NotificationSettingsPanel =
|
|
73
|
+
const GitHubPanel = defineAsyncView(() => import('~/components/github/GitHubPanel.vue'))
|
|
74
|
+
const SlackPanel = defineAsyncView(() => import('~/components/slack/SlackPanel.vue'))
|
|
75
|
+
const NotificationSettingsPanel = defineAsyncView(
|
|
70
76
|
() => import('~/components/notifications/NotificationSettingsPanel.vue'),
|
|
71
77
|
)
|
|
72
|
-
const FragmentLibraryPanel =
|
|
78
|
+
const FragmentLibraryPanel = defineAsyncView(
|
|
73
79
|
() => import('~/components/fragments/FragmentLibraryPanel.vue'),
|
|
74
80
|
)
|
|
75
|
-
const FoundationalServicePanel =
|
|
81
|
+
const FoundationalServicePanel = defineAsyncView(
|
|
76
82
|
() => import('~/components/foundational/FoundationalServicePanel.vue'),
|
|
77
83
|
)
|
|
78
84
|
// Startup advisory for invalid / outdated pipelines — only mounted while open (auto-opened
|
|
79
85
|
// at most once per session by the watcher below), so it stays out of the initial bundle.
|
|
80
|
-
const PipelineHealthModal =
|
|
86
|
+
const PipelineHealthModal = defineAsyncView(
|
|
81
87
|
() => import('~/components/pipeline/PipelineHealthModal.vue'),
|
|
82
88
|
)
|
|
83
89
|
// Startup advisory for new / outdated built-in merge presets — same once-per-session pattern.
|
|
84
|
-
const RiskPolicyHealthModal =
|
|
90
|
+
const RiskPolicyHealthModal = defineAsyncView(
|
|
85
91
|
() => import('~/components/settings/RiskPolicyHealthModal.vue'),
|
|
86
92
|
)
|
|
87
93
|
// Startup advisory for new / outdated built-in model presets — same once-per-session pattern.
|
|
88
|
-
const ModelPresetHealthModal =
|
|
94
|
+
const ModelPresetHealthModal = defineAsyncView(
|
|
89
95
|
() => import('~/components/settings/ModelPresetHealthModal.vue'),
|
|
90
96
|
)
|
|
91
|
-
const IntegrationsHub =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const ModelProvidersHub = defineAsyncComponent(
|
|
95
|
-
() => import('~/components/layout/ModelProvidersHub.vue'),
|
|
96
|
-
)
|
|
97
|
-
const PersonalSetupModal = defineAsyncComponent(
|
|
97
|
+
const IntegrationsHub = defineAsyncView(() => import('~/components/layout/IntegrationsHub.vue'))
|
|
98
|
+
const ModelProvidersHub = defineAsyncView(() => import('~/components/layout/ModelProvidersHub.vue'))
|
|
99
|
+
const PersonalSetupModal = defineAsyncView(
|
|
98
100
|
() => import('~/components/layout/PersonalSetupModal.vue'),
|
|
99
101
|
)
|
|
100
|
-
const WorkspaceSettingsPanel =
|
|
102
|
+
const WorkspaceSettingsPanel = defineAsyncView(
|
|
101
103
|
() => import('~/components/settings/WorkspaceSettingsPanel.vue'),
|
|
102
104
|
)
|
|
103
|
-
const AccountSettingsPanel =
|
|
105
|
+
const AccountSettingsPanel = defineAsyncView(
|
|
104
106
|
() => import('~/components/settings/AccountSettingsPanel.vue'),
|
|
105
107
|
)
|
|
106
|
-
const ObservabilityConnectionPanel =
|
|
108
|
+
const ObservabilityConnectionPanel = defineAsyncView(
|
|
107
109
|
() => import('~/components/settings/ObservabilityConnectionPanel.vue'),
|
|
108
110
|
)
|
|
109
|
-
const PackageRegistriesPanel =
|
|
111
|
+
const PackageRegistriesPanel = defineAsyncView(
|
|
110
112
|
() => import('~/components/settings/PackageRegistriesPanel.vue'),
|
|
111
113
|
)
|
|
112
|
-
const ApiTokensPanel =
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
const InfrastructureWindow = defineAsyncComponent(
|
|
114
|
+
const ApiTokensPanel = defineAsyncView(() => import('~/components/settings/ApiTokensPanel.vue'))
|
|
115
|
+
const InfrastructureWindow = defineAsyncView(
|
|
116
116
|
() => import('~/components/settings/InfrastructureWindow.vue'),
|
|
117
117
|
)
|
|
118
|
-
const EnvironmentSetupWizard =
|
|
118
|
+
const EnvironmentSetupWizard = defineAsyncView(
|
|
119
119
|
() => import('~/components/environments/EnvironmentSetupWizard.vue'),
|
|
120
120
|
)
|
|
121
|
-
const ModelConfigurationPanel =
|
|
121
|
+
const ModelConfigurationPanel = defineAsyncView(
|
|
122
122
|
() => import('~/components/settings/ModelConfigurationPanel.vue'),
|
|
123
123
|
)
|
|
124
|
-
const LocalModelEndpointsPanel =
|
|
124
|
+
const LocalModelEndpointsPanel = defineAsyncView(
|
|
125
125
|
() => import('~/components/settings/LocalModelEndpointsPanel.vue'),
|
|
126
126
|
)
|
|
127
|
-
const SandboxPanel =
|
|
128
|
-
const UserSecretsSection =
|
|
127
|
+
const SandboxPanel = defineAsyncView(() => import('~/components/sandbox/SandboxPanel.vue'))
|
|
128
|
+
const UserSecretsSection = defineAsyncView(
|
|
129
129
|
() => import('~/components/settings/UserSecretsSection.vue'),
|
|
130
130
|
)
|
|
131
|
-
const OpenRouterCatalogPanel =
|
|
131
|
+
const OpenRouterCatalogPanel = defineAsyncView(
|
|
132
132
|
() => import('~/components/settings/OpenRouterCatalogPanel.vue'),
|
|
133
133
|
)
|
|
134
|
-
const VendorCredentialsModal =
|
|
134
|
+
const VendorCredentialsModal = defineAsyncView(
|
|
135
135
|
() => import('~/components/providers/VendorCredentialsModal.vue'),
|
|
136
136
|
)
|
|
137
|
-
const AiProviderOnboardingModal =
|
|
137
|
+
const AiProviderOnboardingModal = defineAsyncView(
|
|
138
138
|
() => import('~/components/providers/AiProviderOnboardingModal.vue'),
|
|
139
139
|
)
|
|
140
|
-
const AiPresetMismatchDialog =
|
|
140
|
+
const AiPresetMismatchDialog = defineAsyncView(
|
|
141
141
|
() => import('~/components/providers/AiPresetMismatchDialog.vue'),
|
|
142
142
|
)
|
|
143
143
|
// The in-app tutorial: the launch prompt (auto-opened once for a user who never answered
|
|
@@ -145,19 +145,15 @@ const AiPresetMismatchDialog = defineAsyncComponent(
|
|
|
145
145
|
// section or the palette, at any time), the coach-mark overlay that runs a tour, and the
|
|
146
146
|
// contextual offer that raises the ONE walkthrough this board just made takeable. All
|
|
147
147
|
// mount only while their store flag is set, so they cost the initial bundle nothing.
|
|
148
|
-
const TutorialPrompt =
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
const TutorialCatalogue = defineAsyncComponent(
|
|
148
|
+
const TutorialPrompt = defineAsyncView(() => import('~/components/tutorial/TutorialPrompt.vue'))
|
|
149
|
+
const TutorialCatalogue = defineAsyncView(
|
|
152
150
|
() => import('~/components/tutorial/TutorialCatalogue.vue'),
|
|
153
151
|
)
|
|
154
|
-
const TutorialOverlay =
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
const TutorialNudge = defineAsyncComponent(() => import('~/components/tutorial/TutorialNudge.vue'))
|
|
152
|
+
const TutorialOverlay = defineAsyncView(() => import('~/components/tutorial/TutorialOverlay.vue'))
|
|
153
|
+
const TutorialNudge = defineAsyncView(() => import('~/components/tutorial/TutorialNudge.vue'))
|
|
158
154
|
// The first-run role question (Engineer / Product manager / Designer). Same shape as the tutorial
|
|
159
155
|
// launch prompt: mounted only while its store flag is set, so an answered question costs nothing.
|
|
160
|
-
const RolePrompt =
|
|
156
|
+
const RolePrompt = defineAsyncView(() => import('~/components/layout/RolePrompt.vue'))
|
|
161
157
|
|
|
162
158
|
const workspace = useWorkspaceStore()
|
|
163
159
|
const github = useGitHubStore()
|
|
@@ -479,7 +475,15 @@ watch(
|
|
|
479
475
|
<!-- Always-mounted, fast-path surfaces. -->
|
|
480
476
|
<PipelineBuilder />
|
|
481
477
|
<DecisionModal />
|
|
482
|
-
|
|
478
|
+
<!-- Code-split step-detail reader. Teleport + fade live here, not inside the component,
|
|
479
|
+
for the same reason the focus view's do: an inner Transition never animates the
|
|
480
|
+
mount its own v-if gate causes, and is unmounted before it could animate the
|
|
481
|
+
unmount. -->
|
|
482
|
+
<Teleport to="body">
|
|
483
|
+
<Transition name="reader-fade">
|
|
484
|
+
<AgentStepDetail v-if="ui.stepDetail" />
|
|
485
|
+
</Transition>
|
|
486
|
+
</Teleport>
|
|
483
487
|
<StepResultViewHost />
|
|
484
488
|
<!-- Consumer-contributed top-level overlays (extension slice D). Renders nothing until a
|
|
485
489
|
consumer opens one via `ui.openOverlay` / `useAppOverlays().open(...)`. -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineAsyncComponent, type AsyncComponentLoader, type Component } from 'vue'
|
|
2
|
+
import AsyncViewError from '~/components/common/AsyncViewError.vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Define a code-split surface (a window, a panel, a modal, the step reader) that STATES it when
|
|
6
|
+
* its chunk fails to load.
|
|
7
|
+
*
|
|
8
|
+
* A bare `defineAsyncComponent` renders nothing on a rejected loader, and the rejection is
|
|
9
|
+
* routine rather than exotic: the SPA is a hashed-chunk build, so a deployment that lands while
|
|
10
|
+
* a tab is open makes every not-yet-fetched chunk a 404, and the first click on a window the
|
|
11
|
+
* session had never opened resolves to a blank screen with no message. On the surfaces these
|
|
12
|
+
* windows serve, that blank is the one a person approves or rejects a run from, so it reads as
|
|
13
|
+
* "there is nothing to review" rather than as a failure. Same rule as the backend's: absent and
|
|
14
|
+
* empty must never render the same.
|
|
15
|
+
*
|
|
16
|
+
* The remedy is a reload rather than a retry, because the chunk the running document is asking
|
|
17
|
+
* for is gone from the origin and re-requesting the same URL cannot bring it back.
|
|
18
|
+
*
|
|
19
|
+
* Use this instead of `defineAsyncComponent` for every surface the app splits out, so a
|
|
20
|
+
* consumer copying the nearest example copies the loud one.
|
|
21
|
+
*/
|
|
22
|
+
export function defineAsyncView(loader: AsyncComponentLoader): Component {
|
|
23
|
+
return defineAsyncComponent({ loader, errorComponent: AsyncViewError })
|
|
24
|
+
}
|