@cat-factory/app 0.136.1 → 0.137.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/app/components/board/AddTaskModal.vue +73 -1
- package/app/components/panels/MergerResultView.vue +113 -139
- package/app/components/panels/ResultWindowShell.vue +141 -0
- package/app/components/pipeline/PipelineProgress.vue +2 -0
- package/app/composables/useResultView.ts +13 -3
- package/i18n/locales/de.json +2 -0
- package/i18n/locales/en.json +2 -0
- package/i18n/locales/es.json +2 -0
- package/i18n/locales/fr.json +2 -0
- package/i18n/locales/he.json +2 -0
- package/i18n/locales/it.json +2 -0
- package/i18n/locales/ja.json +2 -0
- package/i18n/locales/pl.json +2 -0
- package/i18n/locales/tr.json +2 -0
- package/i18n/locales/uk.json +2 -0
- package/package.json +4 -4
|
@@ -337,9 +337,37 @@ const pendingContext = ref<PendingContext[]>([])
|
|
|
337
337
|
|
|
338
338
|
// The Context documents / Context issues sections mirror the task inspector but are
|
|
339
339
|
// always shown (ungated): when the relevant integration isn't connected the Attach
|
|
340
|
-
// button
|
|
340
|
+
// button becomes a "Connect a source" action instead. Connecting opens the source's
|
|
341
|
+
// connect modal OVER this one (both are root-mounted with independent open flags), so
|
|
342
|
+
// the user's in-progress task data is preserved rather than lost to a navigation away.
|
|
341
343
|
const docsConnected = computed(() => documents.available && documents.anyConnected)
|
|
342
344
|
const issuesConnected = computed(() => tasks.available && tasks.anyOffered)
|
|
345
|
+
|
|
346
|
+
// Sources the user could connect right now to unlock the picker, when none is connected
|
|
347
|
+
// yet: for documents, every configured source without a live connection (GitHub docs are
|
|
348
|
+
// already implicitly connected via the App, so they never appear here); for issues, every
|
|
349
|
+
// configured tracker not yet available (a connected credentialed source, or the installed
|
|
350
|
+
// GitHub App). The connect modals are the same ones the Integrations hub opens.
|
|
351
|
+
const connectableDocSources = computed(() =>
|
|
352
|
+
documents.available ? documents.sources.filter((s) => !documents.isConnected(s.source)) : [],
|
|
353
|
+
)
|
|
354
|
+
const connectableIssueSources = computed(() =>
|
|
355
|
+
tasks.available ? tasks.sources.filter((s) => !s.available) : [],
|
|
356
|
+
)
|
|
357
|
+
const connectDocMenu = computed(() => [
|
|
358
|
+
connectableDocSources.value.map((s) => ({
|
|
359
|
+
label: s.label,
|
|
360
|
+
icon: s.icon,
|
|
361
|
+
onSelect: () => ui.openDocumentConnect(s.source),
|
|
362
|
+
})),
|
|
363
|
+
])
|
|
364
|
+
const connectIssueMenu = computed(() => [
|
|
365
|
+
connectableIssueSources.value.map((s) => ({
|
|
366
|
+
label: s.label,
|
|
367
|
+
icon: s.icon,
|
|
368
|
+
onSelect: () => ui.openTaskConnect(s.source),
|
|
369
|
+
})),
|
|
370
|
+
])
|
|
343
371
|
const pendingDocs = computed(() => pendingContext.value.filter((c) => c.kind === 'document'))
|
|
344
372
|
const pendingIssues = computed(() => pendingContext.value.filter((c) => c.kind === 'task'))
|
|
345
373
|
|
|
@@ -963,6 +991,27 @@ async function add() {
|
|
|
963
991
|
>
|
|
964
992
|
{{ showDocPicker ? t('board.addTask.done') : t('board.addTask.attach') }}
|
|
965
993
|
</UButton>
|
|
994
|
+
<UDropdownMenu
|
|
995
|
+
v-else-if="connectableDocSources.length > 1"
|
|
996
|
+
:items="connectDocMenu"
|
|
997
|
+
:content="{ side: 'bottom', align: 'end' }"
|
|
998
|
+
>
|
|
999
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plug">
|
|
1000
|
+
{{ t('board.addTask.connectSource') }}
|
|
1001
|
+
</UButton>
|
|
1002
|
+
</UDropdownMenu>
|
|
1003
|
+
<UButton
|
|
1004
|
+
v-else-if="connectableDocSources.length === 1"
|
|
1005
|
+
color="neutral"
|
|
1006
|
+
variant="soft"
|
|
1007
|
+
size="xs"
|
|
1008
|
+
icon="i-lucide-plug"
|
|
1009
|
+
@click="ui.openDocumentConnect(connectableDocSources[0]!.source)"
|
|
1010
|
+
>
|
|
1011
|
+
{{
|
|
1012
|
+
t('board.addTask.connectSourceNamed', { source: connectableDocSources[0]!.label })
|
|
1013
|
+
}}
|
|
1014
|
+
</UButton>
|
|
966
1015
|
<UButton
|
|
967
1016
|
v-else
|
|
968
1017
|
color="neutral"
|
|
@@ -1038,6 +1087,29 @@ async function add() {
|
|
|
1038
1087
|
>
|
|
1039
1088
|
{{ showIssuePicker ? t('board.addTask.done') : t('board.addTask.attach') }}
|
|
1040
1089
|
</UButton>
|
|
1090
|
+
<UDropdownMenu
|
|
1091
|
+
v-else-if="connectableIssueSources.length > 1"
|
|
1092
|
+
:items="connectIssueMenu"
|
|
1093
|
+
:content="{ side: 'bottom', align: 'end' }"
|
|
1094
|
+
>
|
|
1095
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plug">
|
|
1096
|
+
{{ t('board.addTask.connectSource') }}
|
|
1097
|
+
</UButton>
|
|
1098
|
+
</UDropdownMenu>
|
|
1099
|
+
<UButton
|
|
1100
|
+
v-else-if="connectableIssueSources.length === 1"
|
|
1101
|
+
color="neutral"
|
|
1102
|
+
variant="soft"
|
|
1103
|
+
size="xs"
|
|
1104
|
+
icon="i-lucide-plug"
|
|
1105
|
+
@click="ui.openTaskConnect(connectableIssueSources[0]!.source)"
|
|
1106
|
+
>
|
|
1107
|
+
{{
|
|
1108
|
+
t('board.addTask.connectSourceNamed', {
|
|
1109
|
+
source: connectableIssueSources[0]!.label,
|
|
1110
|
+
})
|
|
1111
|
+
}}
|
|
1112
|
+
</UButton>
|
|
1041
1113
|
<UButton
|
|
1042
1114
|
v-else
|
|
1043
1115
|
color="neutral"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
import type { MergeAxis, MergeDecision } from '@cat-factory/contracts'
|
|
11
11
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
12
|
-
import
|
|
12
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
13
13
|
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
14
14
|
|
|
15
15
|
const board = useBoardStore()
|
|
@@ -17,9 +17,12 @@ const execution = useExecutionStore()
|
|
|
17
17
|
const agents = useAgentsStore()
|
|
18
18
|
const { t, n } = useI18n()
|
|
19
19
|
|
|
20
|
-
// Shared seam contract (open/blockId/close
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// Shared seam contract (open/blockId/close). No loader: the verdict is read straight off
|
|
21
|
+
// the execution step. `manageEscape: false` — `ResultWindowShell` owns Escape (and focus
|
|
22
|
+
// trap + scroll lock + stacking) via the shared overlay behaviour.
|
|
23
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('merger', {
|
|
24
|
+
manageEscape: false,
|
|
25
|
+
})
|
|
23
26
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
24
27
|
|
|
25
28
|
const instance = computed(() =>
|
|
@@ -119,151 +122,122 @@ const reasonText = computed(() => {
|
|
|
119
122
|
</script>
|
|
120
123
|
|
|
121
124
|
<template>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
:instance-id="instanceId"
|
|
146
|
-
:step-index="stepIndex"
|
|
147
|
-
@restarted="close"
|
|
148
|
-
/>
|
|
149
|
-
<button
|
|
150
|
-
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
151
|
-
@click="close"
|
|
125
|
+
<ResultWindowShell
|
|
126
|
+
:open="open"
|
|
127
|
+
:icon="meta?.icon ?? 'i-lucide-git-pull-request'"
|
|
128
|
+
icon-class="bg-lime-500/15 text-lime-300"
|
|
129
|
+
:title="headerTitle"
|
|
130
|
+
:subtitle="t('panels.mergerResult.description')"
|
|
131
|
+
:step-ref="{ instanceId, stepIndex }"
|
|
132
|
+
width="3xl"
|
|
133
|
+
@close="close"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex min-h-0 flex-1">
|
|
136
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
137
|
+
<template v-if="decision">
|
|
138
|
+
<!-- Decision banner: auto-merged (success) vs awaiting human review (warning). -->
|
|
139
|
+
<div
|
|
140
|
+
class="mb-4 flex items-start gap-3 rounded-lg border p-3"
|
|
141
|
+
:class="
|
|
142
|
+
merged
|
|
143
|
+
? 'border-emerald-800/70 bg-emerald-500/10'
|
|
144
|
+
: 'border-amber-800/70 bg-amber-500/10'
|
|
145
|
+
"
|
|
146
|
+
data-testid="merger-decision"
|
|
147
|
+
:data-outcome="decision.outcome"
|
|
152
148
|
>
|
|
153
|
-
<UIcon
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class="mb-4 flex items-start gap-3 rounded-lg border p-3"
|
|
163
|
-
:class="
|
|
164
|
-
merged
|
|
165
|
-
? 'border-emerald-800/70 bg-emerald-500/10'
|
|
166
|
-
: 'border-amber-800/70 bg-amber-500/10'
|
|
167
|
-
"
|
|
168
|
-
data-testid="merger-decision"
|
|
169
|
-
:data-outcome="decision.outcome"
|
|
149
|
+
<UIcon
|
|
150
|
+
:name="merged ? 'i-lucide-git-merge' : 'i-lucide-user-round-check'"
|
|
151
|
+
class="mt-0.5 h-5 w-5 shrink-0"
|
|
152
|
+
:class="merged ? 'text-emerald-300' : 'text-amber-300'"
|
|
153
|
+
/>
|
|
154
|
+
<div class="min-w-0">
|
|
155
|
+
<p
|
|
156
|
+
class="text-sm font-semibold"
|
|
157
|
+
:class="merged ? 'text-emerald-200' : 'text-amber-200'"
|
|
170
158
|
>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<div class="min-w-0">
|
|
177
|
-
<p
|
|
178
|
-
class="text-sm font-semibold"
|
|
179
|
-
:class="merged ? 'text-emerald-200' : 'text-amber-200'"
|
|
180
|
-
>
|
|
181
|
-
{{ outcomeText }}
|
|
182
|
-
</p>
|
|
183
|
-
<p class="mt-0.5 text-[13px] leading-relaxed text-slate-300">{{ reasonText }}</p>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
159
|
+
{{ outcomeText }}
|
|
160
|
+
</p>
|
|
161
|
+
<p class="mt-0.5 text-[13px] leading-relaxed text-slate-300">{{ reasonText }}</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
186
164
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
</div>
|
|
202
|
-
<span
|
|
203
|
-
class="w-11 shrink-0 text-end text-xs tabular-nums"
|
|
204
|
-
:class="exceeded.has(axis.key) ? 'text-rose-300' : 'text-slate-300'"
|
|
205
|
-
>
|
|
206
|
-
{{ n(axis.score, { key: 'percent' }) }}
|
|
207
|
-
</span>
|
|
208
|
-
<span class="w-24 shrink-0 text-end text-[10px] tabular-nums text-slate-500">
|
|
209
|
-
{{
|
|
210
|
-
t('panels.mergerResult.ceiling', {
|
|
211
|
-
value: n(axis.ceiling, { key: 'percent' }),
|
|
212
|
-
})
|
|
213
|
-
}}
|
|
214
|
-
</span>
|
|
215
|
-
</div>
|
|
165
|
+
<!-- Scores vs the resolved preset's ceilings. -->
|
|
166
|
+
<template v-if="axes.length">
|
|
167
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
168
|
+
{{ t('panels.mergerResult.scores') }}
|
|
169
|
+
</h3>
|
|
170
|
+
<div class="space-y-2 rounded-lg border border-slate-800 bg-slate-950/40 p-3">
|
|
171
|
+
<div v-for="axis in axes" :key="axis.key" class="flex items-center gap-2">
|
|
172
|
+
<span class="w-20 shrink-0 text-xs text-slate-400">{{ axis.label }}</span>
|
|
173
|
+
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-800">
|
|
174
|
+
<div
|
|
175
|
+
class="h-full rounded-full"
|
|
176
|
+
:class="exceeded.has(axis.key) ? 'bg-rose-500' : 'bg-emerald-500'"
|
|
177
|
+
:style="{ width: `${Math.round(axis.score * 100)}%` }"
|
|
178
|
+
/>
|
|
216
179
|
</div>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<template v-if="decision.assessment?.rationale">
|
|
221
|
-
<h3
|
|
222
|
-
class="mb-2 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
180
|
+
<span
|
|
181
|
+
class="w-11 shrink-0 text-end text-xs tabular-nums"
|
|
182
|
+
:class="exceeded.has(axis.key) ? 'text-rose-300' : 'text-slate-300'"
|
|
223
183
|
>
|
|
224
|
-
{{
|
|
225
|
-
</
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</
|
|
234
|
-
</
|
|
184
|
+
{{ n(axis.score, { key: 'percent' }) }}
|
|
185
|
+
</span>
|
|
186
|
+
<span class="w-24 shrink-0 text-end text-[10px] tabular-nums text-slate-500">
|
|
187
|
+
{{
|
|
188
|
+
t('panels.mergerResult.ceiling', {
|
|
189
|
+
value: n(axis.ceiling, { key: 'percent' }),
|
|
190
|
+
})
|
|
191
|
+
}}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
235
196
|
|
|
236
|
-
|
|
197
|
+
<!-- The agent's prose justification. -->
|
|
198
|
+
<template v-if="decision.assessment?.rationale">
|
|
199
|
+
<h3 class="mb-2 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
200
|
+
{{ t('panels.mergerResult.rationale') }}
|
|
201
|
+
</h3>
|
|
237
202
|
<MarkdownProse
|
|
238
|
-
|
|
239
|
-
:text="step.output"
|
|
203
|
+
:text="decision.assessment.rationale"
|
|
240
204
|
class="text-[13px] leading-relaxed text-slate-300"
|
|
241
205
|
/>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
:total-steps="instance?.steps.length"
|
|
261
|
-
:run-failed="instance?.status === 'failed'"
|
|
262
|
-
:failure-at="instance?.failure?.occurredAt"
|
|
263
|
-
/>
|
|
264
|
-
</aside>
|
|
206
|
+
</template>
|
|
207
|
+
<p v-else class="text-[13px] italic leading-relaxed text-slate-500">
|
|
208
|
+
{{ t('panels.mergerResult.noAssessment') }}
|
|
209
|
+
</p>
|
|
210
|
+
</template>
|
|
211
|
+
|
|
212
|
+
<!-- Pre-structured runs kept only the raw prose output. -->
|
|
213
|
+
<MarkdownProse
|
|
214
|
+
v-else-if="step?.output"
|
|
215
|
+
:text="step.output"
|
|
216
|
+
class="text-[13px] leading-relaxed text-slate-300"
|
|
217
|
+
/>
|
|
218
|
+
<div
|
|
219
|
+
v-else
|
|
220
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
221
|
+
>
|
|
222
|
+
<UIcon name="i-lucide-git-pull-request" class="h-8 w-8 opacity-40" />
|
|
223
|
+
<p class="text-sm">{{ t('panels.mergerResult.noResult') }}</p>
|
|
265
224
|
</div>
|
|
266
225
|
</div>
|
|
226
|
+
|
|
227
|
+
<!-- Sidebar: shared run metadata. -->
|
|
228
|
+
<aside
|
|
229
|
+
class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
230
|
+
>
|
|
231
|
+
<StepRunMeta
|
|
232
|
+
v-if="step"
|
|
233
|
+
:step="step"
|
|
234
|
+
:instance-id="instanceId ?? undefined"
|
|
235
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
236
|
+
:total-steps="instance?.steps.length"
|
|
237
|
+
:run-failed="instance?.status === 'failed'"
|
|
238
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
239
|
+
/>
|
|
240
|
+
</aside>
|
|
267
241
|
</div>
|
|
268
|
-
</
|
|
242
|
+
</ResultWindowShell>
|
|
269
243
|
</template>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared modal shell for the agent-run result windows (slice 5 of the modular-vue
|
|
3
|
+
// adoption — docs/initiatives/modular-vue-adoption.md; progress in
|
|
4
|
+
// docs/initiatives/modular-vue-slice5-progress.md).
|
|
5
|
+
//
|
|
6
|
+
// Every result window (the merger verdict, the tester report, the requirements-review
|
|
7
|
+
// loop, the gates, …) used to hand-roll the SAME modal chrome — `<Teleport>`, a
|
|
8
|
+
// backdrop, a bordered card, a header row with an icon/title/close — and, worse,
|
|
9
|
+
// re-implemented the modal *behaviour* inconsistently: only 2 of ~18 trapped focus, each
|
|
10
|
+
// registered its own global Escape listener, and every one hard-coded `z-50` with no
|
|
11
|
+
// stacking. This shell centralises the chrome AND delegates the behaviour to the upstream
|
|
12
|
+
// `useModalBehavior` (`@modular-vue/core`, the slice-5 overlay-host release): focus-trap
|
|
13
|
+
// + focus-return, body-scroll lock, and a shared overlay STACK so the top overlay closes
|
|
14
|
+
// first on Escape. A window becomes body-only markup wrapped in `<ResultWindowShell>`; it
|
|
15
|
+
// keeps its `useResultView` seam but passes `manageEscape: false` (the shell owns Escape).
|
|
16
|
+
//
|
|
17
|
+
// The pick-one SELECTION of which window is active stays exactly the slice-2
|
|
18
|
+
// `resolveComponentRegistry` in `StepResultViewHost.vue` — this shell only owns the
|
|
19
|
+
// per-window chrome + behaviour, so windows convert one at a time behind it.
|
|
20
|
+
import { computed } from 'vue'
|
|
21
|
+
import { useModalBehavior } from '@modular-vue/core'
|
|
22
|
+
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
23
|
+
|
|
24
|
+
/** A pipeline step reference — passed by step-result windows to surface the shared
|
|
25
|
+
* "restart from here" control. `StepRestartControl` self-hides for an off-path open
|
|
26
|
+
* (null ids), so a block-keyed window simply omits this prop. */
|
|
27
|
+
type StepRef = { instanceId: string | null; stepIndex: number | null }
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
/** Whether the window is open — drives the modal behaviour's activation. */
|
|
32
|
+
open: boolean
|
|
33
|
+
/** Header icon (a `UIcon` name) + its badge colour classes. */
|
|
34
|
+
icon?: string
|
|
35
|
+
iconClass?: string
|
|
36
|
+
/** Header title (the accessible dialog name) + optional secondary line. */
|
|
37
|
+
title: string
|
|
38
|
+
subtitle?: string
|
|
39
|
+
/** Card width bucket + backdrop layout (the two pre-slice-5 chrome variants). */
|
|
40
|
+
width?: '3xl' | '4xl' | '5xl'
|
|
41
|
+
variant?: 'stretch' | 'centered'
|
|
42
|
+
/** Provide on step-result windows to show the shared restart control; omit on gates
|
|
43
|
+
* and block-keyed windows (no restart mid-gate / pre-run). */
|
|
44
|
+
stepRef?: StepRef
|
|
45
|
+
/** `data-testid` on the dialog root — pass a window's existing id to preserve e2e
|
|
46
|
+
* selectors; defaults to `result-window`. */
|
|
47
|
+
testid?: string
|
|
48
|
+
}>(),
|
|
49
|
+
{
|
|
50
|
+
icon: 'i-lucide-square',
|
|
51
|
+
iconClass: 'bg-slate-500/15 text-slate-300',
|
|
52
|
+
subtitle: undefined,
|
|
53
|
+
width: '3xl',
|
|
54
|
+
variant: 'stretch',
|
|
55
|
+
stepRef: undefined,
|
|
56
|
+
testid: undefined,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const emit = defineEmits<{ close: [] }>()
|
|
61
|
+
const { t } = useI18n()
|
|
62
|
+
|
|
63
|
+
function requestClose() {
|
|
64
|
+
emit('close')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Managed modal behaviour (focus-trap + return, scroll lock, shared-stack Escape). The
|
|
68
|
+
// window unmounts on close, so deactivation + cleanup fire via `active` going false and
|
|
69
|
+
// unmount — no manual teardown here.
|
|
70
|
+
const { dialogRef } = useModalBehavior({
|
|
71
|
+
active: () => props.open,
|
|
72
|
+
onClose: requestClose,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const WIDTH: Record<'3xl' | '4xl' | '5xl', string> = {
|
|
76
|
+
'3xl': 'max-w-3xl',
|
|
77
|
+
'4xl': 'max-w-4xl',
|
|
78
|
+
'5xl': 'max-w-5xl',
|
|
79
|
+
}
|
|
80
|
+
const backdropClass = computed(() => [
|
|
81
|
+
'fixed inset-0 z-50 flex max-h-[100dvh] justify-center bg-slate-950/70 backdrop-blur-sm',
|
|
82
|
+
props.variant === 'centered' ? 'items-center p-4' : 'items-stretch',
|
|
83
|
+
])
|
|
84
|
+
const panelClass = computed(() => [
|
|
85
|
+
'flex w-full flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl',
|
|
86
|
+
WIDTH[props.width],
|
|
87
|
+
props.variant === 'centered' ? 'max-h-[90dvh]' : 'm-4',
|
|
88
|
+
])
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<Teleport to="body">
|
|
93
|
+
<div
|
|
94
|
+
v-if="open"
|
|
95
|
+
:class="backdropClass"
|
|
96
|
+
data-testid="result-window-backdrop"
|
|
97
|
+
@click.self="requestClose"
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
ref="dialogRef"
|
|
101
|
+
tabindex="-1"
|
|
102
|
+
:class="panelClass"
|
|
103
|
+
role="dialog"
|
|
104
|
+
aria-modal="true"
|
|
105
|
+
:aria-label="title"
|
|
106
|
+
:data-testid="testid ?? 'result-window'"
|
|
107
|
+
>
|
|
108
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
|
|
109
|
+
<span
|
|
110
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
|
111
|
+
:class="iconClass"
|
|
112
|
+
>
|
|
113
|
+
<UIcon :name="icon" class="h-4 w-4" />
|
|
114
|
+
</span>
|
|
115
|
+
<div class="min-w-0 flex-1">
|
|
116
|
+
<h2 class="truncate text-sm font-semibold text-slate-100">{{ title }}</h2>
|
|
117
|
+
<p v-if="subtitle" class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<!-- Window-specific header content (status badges, counts). -->
|
|
120
|
+
<slot name="header-extras" />
|
|
121
|
+
<StepRestartControl
|
|
122
|
+
v-if="stepRef"
|
|
123
|
+
:instance-id="stepRef.instanceId"
|
|
124
|
+
:step-index="stepRef.stepIndex"
|
|
125
|
+
@restarted="requestClose"
|
|
126
|
+
/>
|
|
127
|
+
<button
|
|
128
|
+
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
129
|
+
data-testid="result-window-close"
|
|
130
|
+
:aria-label="t('common.close')"
|
|
131
|
+
@click="requestClose"
|
|
132
|
+
>
|
|
133
|
+
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
134
|
+
</button>
|
|
135
|
+
</header>
|
|
136
|
+
<!-- The window body. -->
|
|
137
|
+
<slot />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</Teleport>
|
|
141
|
+
</template>
|
|
@@ -300,6 +300,8 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
300
300
|
>
|
|
301
301
|
<div
|
|
302
302
|
class="group flex cursor-pointer items-center gap-2"
|
|
303
|
+
data-testid="pipeline-step"
|
|
304
|
+
:data-step-kind="s.agentKind"
|
|
303
305
|
:title="
|
|
304
306
|
s.output
|
|
305
307
|
? t('pipeline.progress.viewDetailsOutput')
|
|
@@ -18,10 +18,18 @@
|
|
|
18
18
|
* review windows) can flush it in one place instead of every caller having to remember to.
|
|
19
19
|
* It runs synchronously; if it kicks off async work it must capture whatever it needs first,
|
|
20
20
|
* because `blockId`/the derived state go null the moment the view closes.
|
|
21
|
+
*
|
|
22
|
+
* `manageEscape` (default `true`) owns the global Escape-to-close listener. A window that
|
|
23
|
+
* renders through `ResultWindowShell` (slice 5 of the modular-vue adoption) passes `false`:
|
|
24
|
+
* the shell's `useModalBehavior` owns Escape via the shared overlay stack (so the top
|
|
25
|
+
* overlay closes first, and focus/scroll are managed too), and a second listener here would
|
|
26
|
+
* double-fire `close`. Un-converted windows keep the default so they still close on Escape.
|
|
27
|
+
* Remove this option once every result window is on the shell (the listener moves out
|
|
28
|
+
* entirely).
|
|
21
29
|
*/
|
|
22
30
|
export function useResultView(
|
|
23
31
|
viewId: string,
|
|
24
|
-
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void },
|
|
32
|
+
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void; manageEscape?: boolean },
|
|
25
33
|
) {
|
|
26
34
|
const ui = useUiStore()
|
|
27
35
|
|
|
@@ -42,8 +50,10 @@ export function useResultView(
|
|
|
42
50
|
function onKey(e: KeyboardEvent) {
|
|
43
51
|
if (e.key === 'Escape' && open.value) close()
|
|
44
52
|
}
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
if (opts?.manageEscape !== false) {
|
|
54
|
+
onMounted(() => window.addEventListener('keydown', onKey))
|
|
55
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
|
56
|
+
}
|
|
47
57
|
|
|
48
58
|
// The load-on-open contract: fire immediately on mount and on any later block switch.
|
|
49
59
|
if (opts?.onOpen) {
|
package/i18n/locales/de.json
CHANGED
|
@@ -2176,6 +2176,8 @@
|
|
|
2176
2176
|
"contextDocuments": "Kontextdokumente",
|
|
2177
2177
|
"contextIssues": "Kontext-Issues",
|
|
2178
2178
|
"attach": "Anhängen",
|
|
2179
|
+
"connectSource": "Quelle verbinden",
|
|
2180
|
+
"connectSourceNamed": "{source} verbinden",
|
|
2179
2181
|
"done": "Fertig",
|
|
2180
2182
|
"attachDocDisabledConnect": "Verbinden Sie zuerst eine Dokumentquelle (Integrationen)",
|
|
2181
2183
|
"attachDocDisabledEnable": "Aktivieren Sie zuerst die Dokumente-Integration",
|
package/i18n/locales/en.json
CHANGED
|
@@ -294,6 +294,8 @@
|
|
|
294
294
|
"contextDocuments": "Context documents",
|
|
295
295
|
"contextIssues": "Context issues",
|
|
296
296
|
"attach": "Attach",
|
|
297
|
+
"connectSource": "Connect a source",
|
|
298
|
+
"connectSourceNamed": "Connect {source}",
|
|
297
299
|
"done": "Done",
|
|
298
300
|
"attachDocDisabledConnect": "Connect a document source first (Integrations)",
|
|
299
301
|
"attachDocDisabledEnable": "Enable the documents integration first",
|
package/i18n/locales/es.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "Documentos de contexto",
|
|
274
274
|
"contextIssues": "Incidencias de contexto",
|
|
275
275
|
"attach": "Adjuntar",
|
|
276
|
+
"connectSource": "Conectar una fuente",
|
|
277
|
+
"connectSourceNamed": "Conectar {source}",
|
|
276
278
|
"done": "Hecho",
|
|
277
279
|
"attachDocDisabledConnect": "Conecta primero una fuente de documentos (Integraciones)",
|
|
278
280
|
"attachDocDisabledEnable": "Activa primero la integración de documentos",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "Documents de contexte",
|
|
274
274
|
"contextIssues": "Tickets de contexte",
|
|
275
275
|
"attach": "Joindre",
|
|
276
|
+
"connectSource": "Connecter une source",
|
|
277
|
+
"connectSourceNamed": "Connecter {source}",
|
|
276
278
|
"done": "Terminé",
|
|
277
279
|
"attachDocDisabledConnect": "Connectez d’abord une source de documents (Intégrations)",
|
|
278
280
|
"attachDocDisabledEnable": "Activez d’abord l’intégration de documents",
|
package/i18n/locales/he.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "מסמכי הקשר",
|
|
274
274
|
"contextIssues": "אישיו הקשר",
|
|
275
275
|
"attach": "צרף",
|
|
276
|
+
"connectSource": "חבר מקור",
|
|
277
|
+
"connectSourceNamed": "חבר את {source}",
|
|
276
278
|
"done": "סיום",
|
|
277
279
|
"attachDocDisabledConnect": "חבר תחילה מקור מסמכים (אינטגרציות)",
|
|
278
280
|
"attachDocDisabledEnable": "הפעל תחילה את אינטגרציית המסמכים",
|
package/i18n/locales/it.json
CHANGED
|
@@ -2176,6 +2176,8 @@
|
|
|
2176
2176
|
"contextDocuments": "Documenti di contesto",
|
|
2177
2177
|
"contextIssues": "Issue di contesto",
|
|
2178
2178
|
"attach": "Allega",
|
|
2179
|
+
"connectSource": "Collega una fonte",
|
|
2180
|
+
"connectSourceNamed": "Collega {source}",
|
|
2179
2181
|
"done": "Fatto",
|
|
2180
2182
|
"attachDocDisabledConnect": "Connetti prima una sorgente di documenti (Integrazioni)",
|
|
2181
2183
|
"attachDocDisabledEnable": "Abilita prima l'integrazione dei documenti",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "コンテキストドキュメント",
|
|
274
274
|
"contextIssues": "コンテキスト issue",
|
|
275
275
|
"attach": "添付",
|
|
276
|
+
"connectSource": "ソースを接続",
|
|
277
|
+
"connectSourceNamed": "{source} を接続",
|
|
276
278
|
"done": "完了",
|
|
277
279
|
"attachDocDisabledConnect": "まずドキュメントソースを接続してください(連携)",
|
|
278
280
|
"attachDocDisabledEnable": "まずドキュメント連携を有効にしてください",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "Dokumenty kontekstu",
|
|
274
274
|
"contextIssues": "Zgłoszenia kontekstu",
|
|
275
275
|
"attach": "Dołącz",
|
|
276
|
+
"connectSource": "Połącz źródło",
|
|
277
|
+
"connectSourceNamed": "Połącz {source}",
|
|
276
278
|
"done": "Gotowe",
|
|
277
279
|
"attachDocDisabledConnect": "Najpierw połącz źródło dokumentów (Integracje)",
|
|
278
280
|
"attachDocDisabledEnable": "Najpierw włącz integrację dokumentów",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "Bağlam belgeleri",
|
|
274
274
|
"contextIssues": "Bağlam sorunları",
|
|
275
275
|
"attach": "Ekle",
|
|
276
|
+
"connectSource": "Bir kaynak bağla",
|
|
277
|
+
"connectSourceNamed": "{source} bağla",
|
|
276
278
|
"done": "Tamam",
|
|
277
279
|
"attachDocDisabledConnect": "Önce bir belge kaynağı bağlayın (Entegrasyonlar)",
|
|
278
280
|
"attachDocDisabledEnable": "Önce belge entegrasyonunu etkinleştirin",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
"contextDocuments": "Документи контексту",
|
|
274
274
|
"contextIssues": "Звернення контексту",
|
|
275
275
|
"attach": "Прикріпити",
|
|
276
|
+
"connectSource": "Підключити джерело",
|
|
277
|
+
"connectSourceNamed": "Підключити {source}",
|
|
276
278
|
"done": "Готово",
|
|
277
279
|
"attachDocDisabledConnect": "Спочатку підключіть джерело документів (Інтеграції)",
|
|
278
280
|
"attachDocDisabledEnable": "Спочатку увімкніть інтеграцію документів",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.137.1",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@modular-frontend/core": "^0.
|
|
22
|
-
"@modular-vue/core": "^1.
|
|
21
|
+
"@modular-frontend/core": "^0.5.0",
|
|
22
|
+
"@modular-vue/core": "^1.4.0",
|
|
23
23
|
"@modular-vue/journeys": "^1.3.0",
|
|
24
24
|
"@modular-vue/nuxt": "^0.4.0",
|
|
25
25
|
"@modular-vue/runtime": "^1.4.1",
|
|
26
|
-
"@modular-vue/vue": "^1.
|
|
26
|
+
"@modular-vue/vue": "^1.4.0",
|
|
27
27
|
"@nuxt/ui": "^4.10.0",
|
|
28
28
|
"@nuxtjs/i18n": "^10.4.1",
|
|
29
29
|
"@pinia/nuxt": "^1.0.1",
|