@cat-factory/app 0.315.4 → 0.315.7
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 +3 -1
- package/app/components/bootstrap/BootstrapModal.vue +1 -1
- package/app/components/common/ConfirmDialog.vue +5 -28
- package/app/components/common/TakeoverShell.vue +97 -0
- package/app/components/focus/BlockFocusView.vue +24 -3
- package/app/components/github/AddServiceFromRepoModal.vue +2 -2
- package/app/components/github/GitHubPanel.vue +1 -1
- package/app/components/kaizen/KaizenPanel.vue +6 -11
- package/app/components/media/ArtifactLightbox.vue +13 -21
- package/app/components/observability/StepModelActivity.vue +1 -0
- package/app/components/panels/AgentStepDetail.vue +8 -12
- package/app/components/panels/InspectorPanel.vue +6 -1
- package/app/components/panels/ObservabilityPanel.vue +7 -11
- package/app/components/panels/OperatorDashboardPanel.vue +6 -11
- package/app/components/panels/ReportsPanel.vue +6 -11
- package/app/components/panels/inspector/ServiceTestConfig.vue +2 -2
- package/app/components/pipeline/PipelineBuilder.vue +4 -0
- package/app/components/settings/ModelConfigurationPanel.vue +21 -13
- package/app/plugins/foreign-layers.client.ts +8 -0
- package/app/stores/github/connection.ts +2 -4
- package/app/stores/github/context.ts +5 -1
- package/app/stores/github/projectionLoad.ts +127 -0
- package/app/stores/github/repoActions.ts +7 -2
- package/app/stores/github.spec.ts +355 -1
- package/app/stores/github.ts +21 -22
- package/app/stores/ui/builderModelConfigHandoff.spec.ts +88 -0
- package/app/stores/ui/modals.ts +78 -12
- package/app/utils/foreignLayers.spec.ts +100 -0
- package/app/utils/foreignLayers.ts +71 -0
- package/i18n/locales/de.json +1 -0
- package/i18n/locales/en.json +1 -0
- package/i18n/locales/es.json +1 -0
- package/i18n/locales/fr.json +1 -0
- package/i18n/locales/he.json +1 -0
- package/i18n/locales/it.json +1 -0
- package/i18n/locales/ja.json +1 -0
- package/i18n/locales/pl.json +1 -0
- package/i18n/locales/tr.json +1 -0
- package/i18n/locales/uk.json +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -388,7 +388,9 @@ A phone's notch and home indicator cover whatever sits at the screen edge, and o
|
|
|
388
388
|
- **`FULL_BLEED_TAKEOVER_CLASS`** for a takeover that fills the screen (`AgentStepDetail`, the model configuration takeover, the observability, reports, operator dashboard and Kaizen panels, the artifact lightbox). It pads by the inset alone, so a screen without one keeps the edge-to-edge layout.
|
|
389
389
|
- **`WINDOWED_OVERLAY_CLASS`** for a window on a backdrop (`ResultWindowShell`). Its gutter is 1rem or the inset, whichever is larger.
|
|
390
390
|
|
|
391
|
-
`
|
|
391
|
+
Each class list belongs to ONE shell, and a surface renders inside that shell instead of binding the class itself: a takeover inside [`common/TakeoverShell.vue`](./app/components/common/TakeoverShell.vue), a window inside [`panels/ResultWindowShell.vue`](./app/components/panels/ResultWindowShell.vue). The shell binds the padding AND the modal behaviour (`useModalBehavior`: a focus trap, focus return, a body-scroll lock, and a ticket on the shared overlay stack), so focus stays inside and Escape closes only the top overlay. A takeover gates the shell with its own `v-if` inside its own `Teleport` and `Transition`, passes a `label` (the dialog's accessible name), and handles the shell's `close` event; it never adds an Escape listener of its own. A Nuxt UI menu, select, popover or modal opened inside a shell holds its own ticket on the same stack while it is open ([`utils/foreignLayers.ts`](./app/utils/foreignLayers.ts), wired by a client plugin), so Escape and Tab are its keys until it closes. No site pushes a ticket by hand. Why this is not `UModal fullscreen`, and the rest of the overlay consolidation: [`docs/initiatives/overlay-shells.md`](https://github.com/kibertoad/cat-factory/blob/main/docs/initiatives/overlay-shells.md).
|
|
392
|
+
|
|
393
|
+
`scripts/check-frontend-safe-area.mjs` (CI) refuses any other `fixed` surface that spans a whole side of the viewport, in `.vue`, `.ts` and `.css`. A surface with no edge content (a scrim, a drawing layer) or one that pads its own edges (the sidebar drawer, the inspector's bottom sheet) says why with a `safe-area-ok:` comment, on the line or the one above it, or above the element's opening tag. The same guard refuses either class list named outside its owning shell, with no waiver.
|
|
392
394
|
|
|
393
395
|
### Modals open at a named width
|
|
394
396
|
|
|
@@ -2,39 +2,16 @@
|
|
|
2
2
|
// The single, app-wide confirmation dialog. Mounted once in `pages/index.vue` and driven
|
|
3
3
|
// entirely by the `useConfirm()` singleton, so any caller can `await confirm({...})` without
|
|
4
4
|
// rendering its own modal. UModal already provides the focus trap, Escape-to-close and
|
|
5
|
-
// backdrop, so this component adds none of that
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// backdrop, so this component adds none of that. It only resolves the pending promise, and
|
|
6
|
+
// resolves `false` whenever the modal closes without an explicit choice. Its place on
|
|
7
|
+
// the shared overlay stack, which makes a takeover or window under it leave Escape to it, comes
|
|
8
|
+
// from `utils/foreignLayers.ts`, as for every Nuxt UI layer.
|
|
9
|
+
import { computed } from 'vue'
|
|
9
10
|
import AppModal from '~/components/common/AppModal.vue'
|
|
10
11
|
|
|
11
12
|
const { t } = useI18n()
|
|
12
13
|
const { open, current, accept, cancel, dismissed } = useConfirm()
|
|
13
14
|
|
|
14
|
-
// Register on the app's ONE overlay stack for as long as the dialog is up.
|
|
15
|
-
//
|
|
16
|
-
// The stack is what `useModalBehavior` (`ResultWindowShell`, the lightbox, every modular overlay)
|
|
17
|
-
// asks before it handles Escape, and this dialog is a Nuxt UI modal, so without a registration it
|
|
18
|
-
// is invisible to that question. A confirm opened FROM a result window then lost the race for its
|
|
19
|
-
// own Escape key: the shell's capture-phase listener still believed it was topmost, so it
|
|
20
|
-
// preventDefault-ed and re-entered its close request, which supersedes the confirm the user was
|
|
21
|
-
// trying to cancel. Pushing a ticket makes the shell stand down while this is on top, which is
|
|
22
|
-
// exactly the ordering the stack exists to state.
|
|
23
|
-
let ticket: OverlayStackTicket | null = null
|
|
24
|
-
function release(): void {
|
|
25
|
-
ticket?.release()
|
|
26
|
-
ticket = null
|
|
27
|
-
}
|
|
28
|
-
watch(
|
|
29
|
-
open,
|
|
30
|
-
(isOpen) => {
|
|
31
|
-
if (isOpen) ticket ??= sharedOverlayStack.push()
|
|
32
|
-
else release()
|
|
33
|
-
},
|
|
34
|
-
{ immediate: true },
|
|
35
|
-
)
|
|
36
|
-
onUnmounted(release)
|
|
37
|
-
|
|
38
15
|
const model = computed({
|
|
39
16
|
get: () => open.value,
|
|
40
17
|
set: (v: boolean) => {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The one root of every full-bleed takeover: the step reader (`AgentStepDetail`), the model
|
|
3
|
+
// configuration takeover, the observability, reports, operator dashboard and Kaizen panels, and the
|
|
4
|
+
// artifact lightbox. It owns the root element and the modal BEHAVIOUR; each takeover keeps its own
|
|
5
|
+
// layout, header and backdrop inside it.
|
|
6
|
+
//
|
|
7
|
+
// The behaviour is the upstream `useModalBehavior` (`@modular-vue/core`), the same composable
|
|
8
|
+
// `ResultWindowShell` uses: a focus trap, focus return, a body-scroll lock, and a ticket on the
|
|
9
|
+
// shared overlay STACK, so Escape closes only the top overlay. Before this shell, six of the seven
|
|
10
|
+
// takeovers hand-rolled a window Escape listener instead. Tab then walked out to the board behind
|
|
11
|
+
// them, and one Escape closed every overlay that listened, so a result window or the observability
|
|
12
|
+
// panel opened over the step reader took the reader down with it.
|
|
13
|
+
//
|
|
14
|
+
// Why not `UModal fullscreen` (the shape `GuidedReviewModal` uses): a Reka modal makes the body
|
|
15
|
+
// inert while it is up (`pointer-events: none` on `<body>`, a `focusin` trap, and a dismiss on any
|
|
16
|
+
// pointer or focus outside its content). The windows opened ON TOP of a takeover
|
|
17
|
+
// (`ResultWindowShell`, the lightbox, a modular overlay) are not Reka layers, so they would open
|
|
18
|
+
// unclickable and lose focus to the takeover under them. The shared stack is what both sides
|
|
19
|
+
// agree on: every open Nuxt UI menu, select, popover and modal holds a ticket on it
|
|
20
|
+
// (`utils/foreignLayers.ts`), so one opened inside a takeover is the top overlay and keeps its own
|
|
21
|
+
// Escape.
|
|
22
|
+
//
|
|
23
|
+
// Every takeover paints at the SAME z-index (`z-50`, as `ResultWindowShell` does) and is teleported
|
|
24
|
+
// to `<body>` when it opens, so DOM order is open order: the overlay stack's top ticket is also the
|
|
25
|
+
// top layer. A per-site z-index let a later-opened `z-50` takeover take focus and Escape while
|
|
26
|
+
// painting under an earlier `z-[60]` one.
|
|
27
|
+
//
|
|
28
|
+
// The takeover is OPEN while this shell is mounted: each site gates it with its own `v-if` inside
|
|
29
|
+
// its own `Teleport` and `Transition`, so a fade owned by a parent mount (`pages/index.vue` for the
|
|
30
|
+
// step reader) still plays in both directions.
|
|
31
|
+
import { useMounted } from '@vueuse/core'
|
|
32
|
+
import { useModalBehavior } from '@modular-vue/core'
|
|
33
|
+
import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
/** The accessible name of the dialog. */
|
|
37
|
+
label: string
|
|
38
|
+
/**
|
|
39
|
+
* What the takeover shows now, for a takeover that swaps its content without closing (the step
|
|
40
|
+
* reader moving to another step). A change moves focus back to the top of the dialog.
|
|
41
|
+
*/
|
|
42
|
+
contentKey?: string | number | null
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{ close: [] }>()
|
|
46
|
+
|
|
47
|
+
// The behaviour activates on MOUNT, not in setup. Most takeovers are async views, and on their first
|
|
48
|
+
// open Nuxt's Suspense defers the mount, so focus moved during setup would find no element yet and
|
|
49
|
+
// stay on the board.
|
|
50
|
+
const mounted = useMounted()
|
|
51
|
+
|
|
52
|
+
// Initial focus goes to the dialog root, not to its first control: a takeover is a whole screen of
|
|
53
|
+
// content, so the reader starts at its top and the first Tab reaches the first control.
|
|
54
|
+
const { dialogRef, isTop } = useModalBehavior({
|
|
55
|
+
active: mounted,
|
|
56
|
+
onClose: () => emit('close'),
|
|
57
|
+
initialFocus: () => dialogRef.value,
|
|
58
|
+
contentKey: () => props.contentKey,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// The trap wraps Tab only when focus is outside the dialog or on its first or last control, and the
|
|
62
|
+
// root is none of those, so the first Shift+Tab from the root would leave for the board behind
|
|
63
|
+
// (the browser moves to the last tabbable element before the root in DOM order). Wrap it here.
|
|
64
|
+
// Hidden controls (`display: none`, such as the reader's outline below `md`) cannot take focus, so
|
|
65
|
+
// they are skipped.
|
|
66
|
+
const FOCUSABLE =
|
|
67
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
68
|
+
|
|
69
|
+
function wrapShiftTabFromRoot(event: KeyboardEvent): void {
|
|
70
|
+
const root = dialogRef.value
|
|
71
|
+
if (event.key !== 'Tab' || !event.shiftKey || !root || event.target !== root) return
|
|
72
|
+
const controls = [...root.querySelectorAll<HTMLElement>(FOCUSABLE)]
|
|
73
|
+
const last = controls.findLast((el) => el.checkVisibility())
|
|
74
|
+
if (!last) return
|
|
75
|
+
event.preventDefault()
|
|
76
|
+
last.focus()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A takeover with its own keys (the lightbox's arrows and zoom) reads this, so its keys stay inert
|
|
80
|
+
// while another overlay is on top of it.
|
|
81
|
+
defineExpose({ isTop })
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div
|
|
86
|
+
ref="dialogRef"
|
|
87
|
+
tabindex="-1"
|
|
88
|
+
role="dialog"
|
|
89
|
+
aria-modal="true"
|
|
90
|
+
:aria-label="label"
|
|
91
|
+
class="z-50 focus:outline-none"
|
|
92
|
+
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
93
|
+
@keydown="wrapShiftTabFromRoot"
|
|
94
|
+
>
|
|
95
|
+
<slot />
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { onBeforeUnmount, onMounted } from 'vue'
|
|
3
|
+
import { useEventListener } from '@vueuse/core'
|
|
4
|
+
import { sharedOverlayStack, type OverlayStackTicket } from '@modular-vue/core'
|
|
3
5
|
import type { Block } from '~/types/domain'
|
|
4
6
|
import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
5
7
|
import { pipelineAllowedForManualStart } from '~/utils/pipeline'
|
|
@@ -79,9 +81,28 @@ function close() {
|
|
|
79
81
|
ui.focus(null)
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// The focus view is one more layer on the shared overlay stack: it holds a ticket while mounted and
|
|
85
|
+
// takes Escape only when that ticket is the top one. A takeover, window, Nuxt UI menu or modal
|
|
86
|
+
// opened from here is pushed after it, so it is the top overlay and keeps its own Escape. The
|
|
87
|
+
// listener is in the CAPTURE phase and registered at setup, so it runs before the listener of any
|
|
88
|
+
// overlay opened later and reads the stack as it was when the key was pressed, before that
|
|
89
|
+
// overlay's close can release its ticket.
|
|
90
|
+
let ticket: OverlayStackTicket | null = null
|
|
91
|
+
onMounted(() => {
|
|
92
|
+
ticket = sharedOverlayStack.push()
|
|
84
93
|
})
|
|
94
|
+
onBeforeUnmount(() => {
|
|
95
|
+
ticket?.release()
|
|
96
|
+
ticket = null
|
|
97
|
+
})
|
|
98
|
+
useEventListener(
|
|
99
|
+
window,
|
|
100
|
+
'keydown',
|
|
101
|
+
(e: KeyboardEvent) => {
|
|
102
|
+
if (e.key === 'Escape' && ticket?.isTop() && ui.focusBlockId) close()
|
|
103
|
+
},
|
|
104
|
+
{ capture: true },
|
|
105
|
+
)
|
|
85
106
|
|
|
86
107
|
function openDecisionFor(decisionId: string) {
|
|
87
108
|
if (instance.value) ui.openDecision(instance.value.id, decisionId)
|
|
@@ -59,10 +59,10 @@ const adding = ref(false)
|
|
|
59
59
|
async function loadRepos() {
|
|
60
60
|
try {
|
|
61
61
|
await github.probe()
|
|
62
|
-
// Load
|
|
62
|
+
// Load the linked-repo projection once per board (to flag repos already on the board). The
|
|
63
63
|
// available-repos list is NOT prefetched — it's searched server-side on demand once
|
|
64
64
|
// the user types (see the repoQueryRaw watcher), so a wide install isn't shipped whole.
|
|
65
|
-
|
|
65
|
+
await github.refresh()
|
|
66
66
|
} catch {
|
|
67
67
|
// Integration off / unreachable → the picker stays empty, GitHubConnect shows.
|
|
68
68
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, watch } from 'vue'
|
|
3
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
4
3
|
import type { TableColumn } from '@nuxt/ui'
|
|
5
4
|
import type { KaizenGrading } from '~/types/domain'
|
|
6
5
|
import { agentKindMeta } from '~/utils/catalog'
|
|
7
6
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
8
|
-
import
|
|
7
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
9
8
|
|
|
10
9
|
// The Kaizen screen: a full-panel overlay listing the workspace's grading history and
|
|
11
10
|
// its verified-combo library. Opened via `ui.openKaizen()` from the sidebar. Read-only —
|
|
@@ -27,9 +26,6 @@ watch(
|
|
|
27
26
|
function close() {
|
|
28
27
|
ui.closeKaizen()
|
|
29
28
|
}
|
|
30
|
-
onKeyStroke('Escape', () => {
|
|
31
|
-
if (open.value) close()
|
|
32
|
-
})
|
|
33
29
|
|
|
34
30
|
function meta(kind: string) {
|
|
35
31
|
return agentKindMeta(kind)
|
|
@@ -66,12 +62,11 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
|
|
|
66
62
|
<template>
|
|
67
63
|
<Teleport to="body">
|
|
68
64
|
<Transition name="kz-fade">
|
|
69
|
-
<
|
|
65
|
+
<TakeoverShell
|
|
70
66
|
v-if="open"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
aria-modal="true"
|
|
67
|
+
:label="t('kaizen.title')"
|
|
68
|
+
class="flex-col bg-app-950/96 backdrop-blur-sm"
|
|
69
|
+
@close="close"
|
|
75
70
|
>
|
|
76
71
|
<header class="flex items-center gap-3 border-b border-default px-6 py-4">
|
|
77
72
|
<div
|
|
@@ -241,7 +236,7 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
|
|
|
241
236
|
</UTable>
|
|
242
237
|
</section>
|
|
243
238
|
</div>
|
|
244
|
-
</
|
|
239
|
+
</TakeoverShell>
|
|
245
240
|
</Transition>
|
|
246
241
|
</Teleport>
|
|
247
242
|
</template>
|
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
// Zoom is pure CSS `transform` (GPU, no canvas), so even large PNGs stay smooth. Keyboard:
|
|
8
8
|
// Esc close · ←/→ prev/next · +/- zoom · 0 reset · double-click toggle fit↔2×.
|
|
9
9
|
import { computed, ref, watch } from 'vue'
|
|
10
|
-
import { useModalBehavior } from '@modular-vue/core'
|
|
11
10
|
import type { ArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
12
11
|
import IconButton from '~/components/common/IconButton.vue'
|
|
13
12
|
import Spinner from '~/components/common/Spinner.vue'
|
|
14
|
-
import
|
|
13
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
15
14
|
|
|
16
15
|
interface LightboxItem {
|
|
17
16
|
artifactId: string
|
|
@@ -39,19 +38,15 @@ const scale = ref(1)
|
|
|
39
38
|
const tx = ref(0)
|
|
40
39
|
const ty = ref(0)
|
|
41
40
|
|
|
42
|
-
// Modal behaviour
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// the reconciliation that replaces the old `active: open && !lightboxOpen` guard the windows
|
|
47
|
-
// used to fight over Tab.
|
|
41
|
+
// Modal behaviour comes from `TakeoverShell` (the shared overlay stack): focus-trap + return,
|
|
42
|
+
// body-scroll lock, and, because it pushes onto the shared stack while open, it becomes the TOP
|
|
43
|
+
// overlay over an owning review window (`ResultWindowShell`), so Escape closes the lightbox first
|
|
44
|
+
// and the owner's trap goes inert until it closes.
|
|
48
45
|
function close() {
|
|
49
46
|
emit('update:open', false)
|
|
50
47
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
onClose: close,
|
|
54
|
-
})
|
|
48
|
+
const shell = ref<InstanceType<typeof TakeoverShell> | null>(null)
|
|
49
|
+
const isTop = computed(() => shell.value?.isTop ?? false)
|
|
55
50
|
|
|
56
51
|
const current = computed(() => props.items[props.index] ?? null)
|
|
57
52
|
const total = computed(() => props.items.length)
|
|
@@ -159,20 +154,17 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
159
154
|
|
|
160
155
|
<template>
|
|
161
156
|
<Teleport to="body">
|
|
162
|
-
<
|
|
157
|
+
<TakeoverShell
|
|
163
158
|
v-if="open"
|
|
164
|
-
ref="
|
|
165
|
-
|
|
166
|
-
class="z-[60] flex-col bg-app-950/95 backdrop-blur-sm focus:outline-none"
|
|
167
|
-
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
168
|
-
role="dialog"
|
|
169
|
-
aria-modal="true"
|
|
159
|
+
ref="shell"
|
|
160
|
+
class="flex-col bg-app-950/95 backdrop-blur-sm"
|
|
170
161
|
data-testid="artifact-lightbox"
|
|
171
|
-
:
|
|
162
|
+
:label="
|
|
172
163
|
current
|
|
173
164
|
? t('media.lightbox.ariaLabel', { label: current.label })
|
|
174
165
|
: t('media.lightbox.ariaLabelFallback')
|
|
175
166
|
"
|
|
167
|
+
@close="close"
|
|
176
168
|
>
|
|
177
169
|
<!-- Toolbar -->
|
|
178
170
|
<div class="flex items-center gap-3 border-b border-default/60 px-4 py-2.5">
|
|
@@ -307,6 +299,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
307
299
|
@click="go(1)"
|
|
308
300
|
/>
|
|
309
301
|
</div>
|
|
310
|
-
</
|
|
302
|
+
</TakeoverShell>
|
|
311
303
|
</Teleport>
|
|
312
304
|
</template>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
3
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
4
3
|
import type { IterationCapChoice } from '~/types/execution'
|
|
5
4
|
import { agentKindMeta } from '~/utils/catalog'
|
|
6
5
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
@@ -32,7 +31,7 @@ import RunDetailLoadState from '~/components/panels/RunDetailLoadState.vue'
|
|
|
32
31
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
33
32
|
import IconButton from '~/components/common/IconButton.vue'
|
|
34
33
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
35
|
-
import
|
|
34
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
36
35
|
|
|
37
36
|
// Detail overlay for a single pipeline step. Opened by clicking an agent in the
|
|
38
37
|
// inspector list (TaskExecution) or the focus-view pipeline (PipelineProgress) via
|
|
@@ -372,10 +371,6 @@ watch(
|
|
|
372
371
|
},
|
|
373
372
|
)
|
|
374
373
|
|
|
375
|
-
onKeyStroke('Escape', () => {
|
|
376
|
-
if (open.value) close()
|
|
377
|
-
})
|
|
378
|
-
|
|
379
374
|
const { copy } = useCopyToClipboard()
|
|
380
375
|
async function copyOutput() {
|
|
381
376
|
if (step.value?.output) await copy(step.value.output)
|
|
@@ -391,14 +386,15 @@ async function copyOutput() {
|
|
|
391
386
|
<!-- `data-agent-kind` names WHICH step this window is scoped to, as the kind rather than as
|
|
392
387
|
the heading beside it: the heading is a translated display label, so it is the wrong
|
|
393
388
|
thing to read for anything that needs to know which agent's step is open. -->
|
|
394
|
-
|
|
389
|
+
<!-- `content-key` moves focus back to the top when the reader swaps to another step. -->
|
|
390
|
+
<TakeoverShell
|
|
395
391
|
v-if="open && step && agent"
|
|
392
|
+
:label="agent.label"
|
|
393
|
+
:content-key="ctx && `${ctx.instanceId}:${ctx.stepIndex}`"
|
|
396
394
|
data-testid="step-detail"
|
|
397
395
|
:data-agent-kind="step?.agentKind"
|
|
398
|
-
class="
|
|
399
|
-
|
|
400
|
-
role="dialog"
|
|
401
|
-
aria-modal="true"
|
|
396
|
+
class="bg-app-950/96 backdrop-blur-sm"
|
|
397
|
+
@close="close"
|
|
402
398
|
>
|
|
403
399
|
<!-- ToC sidebar (only meaningful when there are prose headings) -->
|
|
404
400
|
<aside
|
|
@@ -1085,7 +1081,7 @@ async function copyOutput() {
|
|
|
1085
1081
|
</p>
|
|
1086
1082
|
</div>
|
|
1087
1083
|
</aside>
|
|
1088
|
-
</
|
|
1084
|
+
</TakeoverShell>
|
|
1089
1085
|
</template>
|
|
1090
1086
|
|
|
1091
1087
|
<style scoped>
|
|
@@ -24,10 +24,15 @@ const recurring = useRecurringPipelinesStore()
|
|
|
24
24
|
const requirements = useRequirementsStore()
|
|
25
25
|
const access = useWorkspaceAccess()
|
|
26
26
|
const { t } = useI18n()
|
|
27
|
+
const { present } = usePipelineErrorToast()
|
|
27
28
|
|
|
28
29
|
onMounted(() => {
|
|
29
30
|
fragments.ensureLoaded()
|
|
30
|
-
|
|
31
|
+
// The repo list behind the inspector's repo link and the service config's detect-from-repo
|
|
32
|
+
// lookup. Both read the store, so this one call serves them.
|
|
33
|
+
github.ensureLoaded().catch((error: unknown) => {
|
|
34
|
+
present(error, 'github.connect.errors.loadProjection')
|
|
35
|
+
})
|
|
31
36
|
})
|
|
32
37
|
|
|
33
38
|
const block = computed<Block | undefined>(() =>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
|
3
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
4
3
|
import type { TableColumn } from '@nuxt/ui'
|
|
5
4
|
import { isLlmWarningFinishReason } from '@cat-factory/contracts'
|
|
6
5
|
import type {
|
|
@@ -33,7 +32,7 @@ import ToolCallList from '~/components/observability/ToolCallList.vue'
|
|
|
33
32
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
34
33
|
import IconButton from '~/components/common/IconButton.vue'
|
|
35
34
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
36
|
-
import
|
|
35
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
37
36
|
|
|
38
37
|
/** No run selected: the same empty, NOT-truncated trajectory the store answers with. */
|
|
39
38
|
const EMPTY_TRAJECTORY: RunToolCallTrajectory = Object.freeze({
|
|
@@ -526,9 +525,6 @@ function headroomOf(m: LlmCallMetric): number | null {
|
|
|
526
525
|
function close() {
|
|
527
526
|
ui.closeObservability()
|
|
528
527
|
}
|
|
529
|
-
onKeyStroke('Escape', () => {
|
|
530
|
-
if (open.value) close()
|
|
531
|
-
})
|
|
532
528
|
function exportJson() {
|
|
533
529
|
if (executionId.value) void observability.downloadExport(executionId.value)
|
|
534
530
|
}
|
|
@@ -537,12 +533,12 @@ function exportJson() {
|
|
|
537
533
|
<template>
|
|
538
534
|
<Teleport to="body">
|
|
539
535
|
<Transition name="obs-fade">
|
|
540
|
-
<
|
|
536
|
+
<TakeoverShell
|
|
541
537
|
v-if="open"
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
538
|
+
:label="t('observability.modelActivity')"
|
|
539
|
+
class="flex-col bg-app-950/96 backdrop-blur-sm"
|
|
540
|
+
data-testid="observability-panel"
|
|
541
|
+
@close="close"
|
|
546
542
|
>
|
|
547
543
|
<header class="flex flex-wrap items-center gap-3 border-b border-default px-6 py-4">
|
|
548
544
|
<div
|
|
@@ -1287,7 +1283,7 @@ function exportJson() {
|
|
|
1287
1283
|
</div>
|
|
1288
1284
|
</div>
|
|
1289
1285
|
</div>
|
|
1290
|
-
</
|
|
1286
|
+
</TakeoverShell>
|
|
1291
1287
|
</Transition>
|
|
1292
1288
|
</Teleport>
|
|
1293
1289
|
</template>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, watch } from 'vue'
|
|
3
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
4
3
|
import type { TableColumn } from '@nuxt/ui'
|
|
5
4
|
import type { PlatformObservabilityWindow } from '~/types/execution'
|
|
6
5
|
import { formatMs } from '~/utils/observability'
|
|
@@ -8,7 +7,7 @@ import { FAILURE_KIND_KEYS, isAgentFailureKind } from '~/utils/failureKinds'
|
|
|
8
7
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
9
8
|
import IconButton from '~/components/common/IconButton.vue'
|
|
10
9
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
11
|
-
import
|
|
10
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
12
11
|
|
|
13
12
|
// Deployment-level (platform-operator) observability dashboard: the aggregate health of the
|
|
14
13
|
// active account's runs — outcome totals + success rate, a time-bucketed outcome trend, the
|
|
@@ -105,9 +104,6 @@ function refresh() {
|
|
|
105
104
|
function close() {
|
|
106
105
|
ui.closeOperatorDashboard()
|
|
107
106
|
}
|
|
108
|
-
onKeyStroke('Escape', () => {
|
|
109
|
-
if (open.value) close()
|
|
110
|
-
})
|
|
111
107
|
|
|
112
108
|
// Load (and refresh) whenever the dashboard opens.
|
|
113
109
|
watch(
|
|
@@ -122,13 +118,12 @@ watch(
|
|
|
122
118
|
<template>
|
|
123
119
|
<Teleport to="body">
|
|
124
120
|
<Transition name="obs-fade">
|
|
125
|
-
<
|
|
121
|
+
<TakeoverShell
|
|
126
122
|
v-if="open"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
role="dialog"
|
|
130
|
-
aria-modal="true"
|
|
123
|
+
:label="t('platformObservability.title')"
|
|
124
|
+
class="flex-col bg-app-950/96 backdrop-blur-sm"
|
|
131
125
|
data-testid="operator-dashboard"
|
|
126
|
+
@close="close"
|
|
132
127
|
>
|
|
133
128
|
<header class="flex flex-wrap items-center gap-3 border-b border-default px-6 py-4">
|
|
134
129
|
<div
|
|
@@ -553,7 +548,7 @@ watch(
|
|
|
553
548
|
</div>
|
|
554
549
|
</div>
|
|
555
550
|
</div>
|
|
556
|
-
</
|
|
551
|
+
</TakeoverShell>
|
|
557
552
|
</Transition>
|
|
558
553
|
</Teleport>
|
|
559
554
|
</template>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
4
3
|
import { lastCompleteRollupDay } from '@cat-factory/contracts'
|
|
5
4
|
import type {
|
|
6
5
|
ReportActivityDimension,
|
|
@@ -22,7 +21,7 @@ import ReportsSpendBreakdown from '~/components/panels/ReportsSpendBreakdown.vue
|
|
|
22
21
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
23
22
|
import IconButton from '~/components/common/IconButton.vue'
|
|
24
23
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
25
|
-
import
|
|
24
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
26
25
|
|
|
27
26
|
// Reports: cross-cutting usage analytics for the active account — where the spend and the
|
|
28
27
|
// work actually go. Spend per model and agent kind, spend + run activity per workspace /
|
|
@@ -179,9 +178,6 @@ function refresh() {
|
|
|
179
178
|
function close() {
|
|
180
179
|
ui.closeReports()
|
|
181
180
|
}
|
|
182
|
-
onKeyStroke('Escape', () => {
|
|
183
|
-
if (open.value) close()
|
|
184
|
-
})
|
|
185
181
|
|
|
186
182
|
// Load (and refresh) whenever the panel opens.
|
|
187
183
|
watch(
|
|
@@ -196,13 +192,12 @@ watch(
|
|
|
196
192
|
<template>
|
|
197
193
|
<Teleport to="body">
|
|
198
194
|
<Transition name="reports-fade">
|
|
199
|
-
<
|
|
195
|
+
<TakeoverShell
|
|
200
196
|
v-if="open"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
role="dialog"
|
|
204
|
-
aria-modal="true"
|
|
197
|
+
:label="t('reports.title')"
|
|
198
|
+
class="flex-col bg-app-950/96 backdrop-blur-sm"
|
|
205
199
|
data-testid="reports-panel"
|
|
200
|
+
@close="close"
|
|
206
201
|
>
|
|
207
202
|
<header class="flex flex-wrap items-center gap-3 border-b border-default px-6 py-4">
|
|
208
203
|
<div
|
|
@@ -575,7 +570,7 @@ watch(
|
|
|
575
570
|
</section>
|
|
576
571
|
</div>
|
|
577
572
|
</div>
|
|
578
|
-
</
|
|
573
|
+
</TakeoverShell>
|
|
579
574
|
</Transition>
|
|
580
575
|
</Teleport>
|
|
581
576
|
</template>
|
|
@@ -50,10 +50,10 @@ const ui = useUiStore()
|
|
|
50
50
|
const { t, te } = useI18n()
|
|
51
51
|
|
|
52
52
|
// The custom-manifest-type catalog feeds the `custom` picker. Cheap + shared (coalesced).
|
|
53
|
-
// The repo list
|
|
53
|
+
// The repo list behind the detect-from-repo affordance (owner/name lookup) is loaded by the
|
|
54
|
+
// surface that hosts this section: the inspector panel, or the add-service modal on open.
|
|
54
55
|
onMounted(() => {
|
|
55
56
|
void infra.ensureLoaded()
|
|
56
|
-
void github.ensureLoaded()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
// The service's declared provision type (absent ⇒ treated as `infraless`: no environment
|
|
@@ -252,6 +252,8 @@ const open = computed({
|
|
|
252
252
|
// knows whether to offer the post-release-health gate (it's loaded on demand, not from
|
|
253
253
|
// the snapshot). Best-effort: a failure just leaves the gate hidden.
|
|
254
254
|
onModalOpen(open, () => {
|
|
255
|
+
// Coming back from the model configuration, where none of this changed.
|
|
256
|
+
if (ui.consumeBuilderReturn()) return
|
|
255
257
|
releaseHealth.load().catch(() => {})
|
|
256
258
|
// The prompt-override index badges the steps whose agent no longer runs the shipped prompt.
|
|
257
259
|
// Best-effort: the builder is fully usable without it, and a deployment that wires no
|
|
@@ -404,6 +406,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
|
|
|
404
406
|
:title="t('pipeline.builder.title')"
|
|
405
407
|
side="left"
|
|
406
408
|
:ui="{ content: 'max-w-[90vw] sm:max-w-2xl lg:max-w-5xl xl:max-w-6xl' }"
|
|
409
|
+
@after:leave="ui.builderLeft()"
|
|
407
410
|
>
|
|
408
411
|
<template #body>
|
|
409
412
|
<!-- On compact viewports the three sections stack and the slideover body
|
|
@@ -443,6 +446,7 @@ const { toggleArchive, toggleDefault, edit, removePipeline, clone } = usePipelin
|
|
|
443
446
|
size="xs"
|
|
444
447
|
icon="i-lucide-cpu"
|
|
445
448
|
:title="t('pipeline.builder.configureModelsTooltip')"
|
|
449
|
+
data-testid="pipeline-builder-configure-models"
|
|
446
450
|
@click="ui.openModelConfig()"
|
|
447
451
|
>
|
|
448
452
|
{{ t('pipeline.builder.configureModels') }}
|