@cat-factory/app 0.280.0 → 0.280.2
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/binaryCandidates/BinaryCandidatesWindow.vue +142 -5
- package/app/components/board/AddTaskModal.vue +9 -0
- package/app/components/board/ReviewFrictionDialog.vue +35 -4
- package/app/components/board/TaskDependencyEdges.vue +26 -15
- package/app/components/board/nodes/TaskCard.vue +6 -1
- package/app/components/brainstorm/BrainstormWindow.vue +19 -1
- package/app/components/common/AsyncViewError.vue +30 -0
- package/app/components/common/ConfirmDialog.vue +26 -0
- package/app/components/docs/DocInterviewWindow.vue +40 -38
- package/app/components/followUp/FollowUpWindow.vue +32 -2
- package/app/components/forkDecision/ForkDecisionWindow.vue +44 -5
- package/app/components/gates/GateResultView.vue +14 -1
- package/app/components/humanTest/HumanTestWindow.vue +13 -1
- package/app/components/initiative/InitiativePlanDecision.vue +16 -3
- package/app/components/initiative/InitiativePlanReview.vue +16 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +50 -52
- package/app/components/initiative/InitiativeTrackerWindow.vue +59 -1
- package/app/components/judge/JudgeResultView.vue +14 -1
- package/app/components/panels/AgentStepDetail.vue +651 -659
- package/app/components/panels/InspectorPanel.vue +6 -0
- package/app/components/panels/ResultWindowDrafts.logic.spec.ts +233 -0
- package/app/components/panels/inspector/ServiceTestSecrets.vue +17 -6
- package/app/components/pipeline/PipelineHealthModal.vue +55 -16
- package/app/components/prReview/PrReviewWindow.vue +23 -4
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +19 -1
- package/app/composables/useBoardActivity.ts +62 -6
- package/app/composables/useConfirm.spec.ts +62 -0
- package/app/composables/useConfirm.ts +6 -1
- package/app/composables/useInterviewDrafts.spec.ts +198 -0
- package/app/composables/useInterviewDrafts.ts +184 -0
- 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/stores/binaryCandidates.ts +26 -3
- package/app/stores/ui/modals.ts +11 -0
- package/app/utils/asyncView.ts +24 -0
- package/app/utils/binaryCandidates.spec.ts +45 -1
- package/app/utils/binaryCandidates.ts +33 -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 +25 -2
- package/i18n/locales/en.json +25 -2
- package/i18n/locales/es.json +25 -2
- package/i18n/locales/fr.json +25 -2
- package/i18n/locales/he.json +25 -2
- package/i18n/locales/it.json +25 -2
- package/i18n/locales/ja.json +25 -2
- package/i18n/locales/pl.json +25 -2
- package/i18n/locales/tr.json +25 -2
- package/i18n/locales/uk.json +25 -2
- package/package.json +1 -1
|
@@ -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(...)`. -->
|
|
@@ -21,6 +21,12 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
21
21
|
|
|
22
22
|
/** True while a keep call is in flight (drives the button spinner / disabled state). */
|
|
23
23
|
const keeping = ref(false)
|
|
24
|
+
/**
|
|
25
|
+
* True while the warm-up read is in flight. The window renders no state until it settles, and
|
|
26
|
+
* "still fetching" must not render as the "nothing to choose between" empty state: on this
|
|
27
|
+
* surface that empty state is a claim the run generated nothing to compare.
|
|
28
|
+
*/
|
|
29
|
+
const loading = ref(false)
|
|
24
30
|
/** The last error message from an action, surfaced inline; cleared on the next action. */
|
|
25
31
|
const error = ref<string | null>(null)
|
|
26
32
|
|
|
@@ -48,9 +54,24 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
48
54
|
if (step) step.binaryCandidates = state
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The most recent {@link load} attempt. Only the latest one may write `loading` or `error`: two
|
|
59
|
+
* reads do overlap (a Retry beside a window switching run, an open triggered while one is in
|
|
60
|
+
* flight), and a superseded attempt settling afterwards used to clear the spinner and stamp its
|
|
61
|
+
* own verdict over the newer read's. A slow failure landing after a fast success is the bad case,
|
|
62
|
+
* because it reports a load failure across candidates that are on screen.
|
|
63
|
+
*/
|
|
64
|
+
let attempt = 0
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Warm the live state from the GET (the stream also keeps it fresh). The failure is RECORDED
|
|
68
|
+
* rather than swallowed: with no state on the step, a failed read and a run that produced no
|
|
69
|
+
* candidates are the same `null` and opposite facts, and only one of them is worth a Retry.
|
|
70
|
+
*/
|
|
52
71
|
async function load(executionId: string): Promise<void> {
|
|
72
|
+
const mine = ++attempt
|
|
53
73
|
error.value = null
|
|
74
|
+
loading.value = true
|
|
54
75
|
try {
|
|
55
76
|
await execution.echoAfter(
|
|
56
77
|
executionId,
|
|
@@ -60,7 +81,9 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
60
81
|
},
|
|
61
82
|
)
|
|
62
83
|
} catch (e) {
|
|
63
|
-
error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
84
|
+
if (mine === attempt) error.value = e instanceof Error ? e.message : 'Failed to load'
|
|
85
|
+
} finally {
|
|
86
|
+
if (mine === attempt) loading.value = false
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
|
|
@@ -85,5 +108,5 @@ export const useBinaryCandidatesStore = defineStore('binaryCandidates', () => {
|
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
|
|
88
|
-
return { keeping, error, load, keep }
|
|
111
|
+
return { keeping, loading, error, load, keep }
|
|
89
112
|
})
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -74,6 +74,17 @@ export interface ReviewFrictionModalContext {
|
|
|
74
74
|
debt: ReviewDebtRow[]
|
|
75
75
|
/** Retry the create with `acknowledgeReviewDebt` — present only for the soft `warn` tier. */
|
|
76
76
|
onConfirm: (() => void) | null
|
|
77
|
+
/**
|
|
78
|
+
* Whether the opener's create is in flight right now, so "Create anyway" can show its spinner and
|
|
79
|
+
* refuse a second click (UX-78). A GETTER over the opener's own `saving` ref rather than a copied
|
|
80
|
+
* boolean: the dialog's context object is captured once at open, so a snapshot would be frozen at
|
|
81
|
+
* `false` for the whole retry. Reading it inside a `computed` keeps the reactivity.
|
|
82
|
+
*
|
|
83
|
+
* Load-bearing, not cosmetic: the retry's first await resolves the staged context attachments
|
|
84
|
+
* (a network round-trip per item), so a second click during that window filed a SECOND task and
|
|
85
|
+
* started a second pipeline run against the same request.
|
|
86
|
+
*/
|
|
87
|
+
pending?: () => boolean
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { PipelineStep } from '~/types/execution'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
binaryCandidateAbsence,
|
|
5
|
+
binaryCandidateHasWarnings,
|
|
6
|
+
binaryCandidateView,
|
|
7
|
+
} from './binaryCandidates'
|
|
4
8
|
|
|
5
9
|
function step(overrides: Record<string, unknown> = {}): PipelineStep {
|
|
6
10
|
// The candidate override MERGES into the base state rather than replacing it, so a case that
|
|
@@ -108,3 +112,43 @@ describe('binaryCandidateHasWarnings', () => {
|
|
|
108
112
|
}
|
|
109
113
|
})
|
|
110
114
|
})
|
|
115
|
+
|
|
116
|
+
describe('binaryCandidateAbsence', () => {
|
|
117
|
+
const RUN = 'exec_1'
|
|
118
|
+
|
|
119
|
+
// The window used to render a titled shell with a blank body for all four of these (UX-80).
|
|
120
|
+
// They are different facts, and only one of them is a claim about the RUN.
|
|
121
|
+
it('separates no run, a read in flight, a failed read, and a run that compared nothing', () => {
|
|
122
|
+
expect(binaryCandidateAbsence(false, null, null)).toBe('no_run')
|
|
123
|
+
expect(binaryCandidateAbsence(true, null, RUN)).toBe('loading')
|
|
124
|
+
expect(binaryCandidateAbsence(false, 'network down', RUN)).toBe('load_failed')
|
|
125
|
+
expect(binaryCandidateAbsence(false, null, RUN)).toBe('nothing_compared')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// A Retry re-enters `loading` while the PREVIOUS attempt's message is still recorded. Reporting
|
|
129
|
+
// the stale failure over the live attempt would make the button look like it did nothing.
|
|
130
|
+
it('lets a fresh attempt outrank the error it is clearing', () => {
|
|
131
|
+
expect(binaryCandidateAbsence(true, 'network down', RUN)).toBe('loading')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// 'nothing_compared' renders "nothing to compare", which is a statement about what the run
|
|
135
|
+
// produced. A request that never landed knows nothing about that, so it must never reach here.
|
|
136
|
+
it('never claims emptiness on the strength of a request that failed', () => {
|
|
137
|
+
for (const error of ['boom', 'Failed to load']) {
|
|
138
|
+
expect(binaryCandidateAbsence(false, error, RUN)).not.toBe('nothing_compared')
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// With no run the warm-up read is never started, so the store's flags belong to some OTHER
|
|
143
|
+
// window's read. Reporting them here would describe a run nobody asked about, and falling through
|
|
144
|
+
// to emptiness would claim this run compared nothing without ever having looked.
|
|
145
|
+
it('never reports a state it did not read, nor emptiness, when there is no run', () => {
|
|
146
|
+
for (const loading of [true, false]) {
|
|
147
|
+
for (const error of [null, 'network down']) {
|
|
148
|
+
for (const run of [null, undefined, '']) {
|
|
149
|
+
expect(binaryCandidateAbsence(loading, error, run)).toBe('no_run')
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -127,6 +127,39 @@ export function binaryCandidateHasWarnings(view: BinaryCandidateView): boolean {
|
|
|
127
127
|
return invalidEntries > 0 || omitted > 0 || unusablePreviews > 0
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Why the window has no comparison to render. Four different facts that a single blank body
|
|
132
|
+
* conflated (UX-80), each needing a different reaction from the reader: nothing to read from, wait,
|
|
133
|
+
* retry, or accept that the run compared nothing. `nothing_compared` is a CLAIM about the run, so
|
|
134
|
+
* it is only ever the answer once a read of that run has settled and not failed.
|
|
135
|
+
*/
|
|
136
|
+
export type BinaryCandidateAbsence = 'no_run' | 'loading' | 'load_failed' | 'nothing_compared'
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The absence to render when {@link binaryCandidateView} produced nothing.
|
|
140
|
+
*
|
|
141
|
+
* Precedence is the point, and it is stated here rather than in the template's branch order:
|
|
142
|
+
*
|
|
143
|
+
* - No RUN outranks everything. The window is keyed to an execution and the warm-up read is
|
|
144
|
+
* skipped without one, so `loading`/`error` can only be describing some other window's read;
|
|
145
|
+
* reporting either would attribute a state to a run nobody asked about, and reporting emptiness
|
|
146
|
+
* would claim this run compared nothing on the strength of never having looked.
|
|
147
|
+
* - An in-flight read outranks a stale error from the previous attempt, so a Retry does not keep
|
|
148
|
+
* showing the failure it is busy clearing.
|
|
149
|
+
* - A recorded error outranks emptiness, so a request that never landed cannot render as "this run
|
|
150
|
+
* generated nothing to compare".
|
|
151
|
+
*/
|
|
152
|
+
export function binaryCandidateAbsence(
|
|
153
|
+
loading: boolean,
|
|
154
|
+
error: string | null | undefined,
|
|
155
|
+
executionId: string | null | undefined,
|
|
156
|
+
): BinaryCandidateAbsence {
|
|
157
|
+
if (!executionId) return 'no_run'
|
|
158
|
+
if (loading) return 'loading'
|
|
159
|
+
if (error) return 'load_failed'
|
|
160
|
+
return 'nothing_compared'
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
/**
|
|
131
164
|
* State → the i18n key for the line explaining why no choice was offered.
|
|
132
165
|
*
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { measureBlocks } from './blockRects'
|
|
3
|
+
|
|
4
|
+
function card(id: string): HTMLElement {
|
|
5
|
+
const el = document.createElement('div')
|
|
6
|
+
el.setAttribute('data-block-id', id)
|
|
7
|
+
return el
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
document.body.innerHTML = ''
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('measureBlocks', () => {
|
|
15
|
+
it('resolves each rendered card by its block id', () => {
|
|
16
|
+
const a = card('a')
|
|
17
|
+
const b = card('b')
|
|
18
|
+
document.body.append(a, b)
|
|
19
|
+
|
|
20
|
+
const blocks = measureBlocks(document)
|
|
21
|
+
expect(blocks.elementFor('a')).toBe(a)
|
|
22
|
+
expect(blocks.elementFor('b')).toBe(b)
|
|
23
|
+
expect(blocks.elementFor('missing')).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('resolves the first card in document order, as a bare querySelector did', () => {
|
|
27
|
+
const onBoard = card('a')
|
|
28
|
+
const inOverlay = card('a')
|
|
29
|
+
document.body.append(onBoard, inOverlay)
|
|
30
|
+
|
|
31
|
+
expect(measureBlocks(document).elementFor('a')).toBe(onBoard)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('measures an element once per pass however many links read it', () => {
|
|
35
|
+
const a = card('a')
|
|
36
|
+
document.body.append(a)
|
|
37
|
+
const measure = vi.spyOn(a, 'getBoundingClientRect')
|
|
38
|
+
|
|
39
|
+
const blocks = measureBlocks(document)
|
|
40
|
+
const first = blocks.rectFor(a)
|
|
41
|
+
expect(blocks.rectFor(a)).toBe(first)
|
|
42
|
+
expect(measure).toHaveBeenCalledTimes(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('is a snapshot: a later pass measures again', () => {
|
|
46
|
+
const a = card('a')
|
|
47
|
+
document.body.append(a)
|
|
48
|
+
const measure = vi.spyOn(a, 'getBoundingClientRect')
|
|
49
|
+
|
|
50
|
+
measureBlocks(document).rectFor(a)
|
|
51
|
+
measureBlocks(document).rectFor(a)
|
|
52
|
+
expect(measure).toHaveBeenCalledTimes(2)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('queries nothing until something is actually looked up', () => {
|
|
56
|
+
// The edge overlay builds a pass every awake frame of a pan and, on a board with no links
|
|
57
|
+
// of any kind, asks it for no card at all. Deferring is what keeps that pass free.
|
|
58
|
+
const root = document.createElement('div')
|
|
59
|
+
root.append(card('a'))
|
|
60
|
+
document.body.append(root)
|
|
61
|
+
const query = vi.spyOn(root, 'querySelectorAll')
|
|
62
|
+
|
|
63
|
+
const blocks = measureBlocks(root)
|
|
64
|
+
expect(query).not.toHaveBeenCalled()
|
|
65
|
+
|
|
66
|
+
blocks.elementFor('a')
|
|
67
|
+
blocks.elementFor('a')
|
|
68
|
+
expect(query).toHaveBeenCalledTimes(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('scopes the pass to the root it was given', () => {
|
|
72
|
+
const outside = card('a')
|
|
73
|
+
const root = document.createElement('div')
|
|
74
|
+
const inside = card('b')
|
|
75
|
+
root.append(inside)
|
|
76
|
+
document.body.append(outside, root)
|
|
77
|
+
|
|
78
|
+
const blocks = measureBlocks(root)
|
|
79
|
+
expect(blocks.elementFor('b')).toBe(inside)
|
|
80
|
+
expect(blocks.elementFor('a')).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
})
|