@cat-factory/app 0.315.6 → 0.315.8
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/common/ConfirmDialog.vue +5 -28
- package/app/components/common/TakeoverShell.vue +97 -0
- package/app/components/focus/BlockFocusView.vue +24 -3
- 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/ObservabilityPanel.vue +7 -11
- package/app/components/panels/OperatorDashboardPanel.vue +6 -11
- package/app/components/panels/ReportsPanel.vue +6 -11
- package/app/components/settings/ModelConfigurationPanel.vue +12 -12
- package/app/plugins/foreign-layers.client.ts +8 -0
- package/app/utils/foreignLayers.spec.ts +100 -0
- package/app/utils/foreignLayers.ts +71 -0
- package/package.json +2 -2
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)
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
// preference. The list view shows each preset; the editor view creates/edits one with a
|
|
12
12
|
// base-model picker plus a filterable per-agent override list.
|
|
13
13
|
import { computed, ref, watch } from 'vue'
|
|
14
|
-
import { onKeyStroke } from '@vueuse/core'
|
|
15
14
|
import type { ModelFlavor } from '@cat-factory/contracts'
|
|
16
15
|
import type { AgentKind } from '~/types/domain'
|
|
17
16
|
import type { ModelPreset } from '~/types/model-presets'
|
|
@@ -25,7 +24,7 @@ import { showOverrideField } from '~/utils/uiMode'
|
|
|
25
24
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
26
25
|
import IconButton from '~/components/common/IconButton.vue'
|
|
27
26
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
28
|
-
import
|
|
27
|
+
import TakeoverShell from '~/components/common/TakeoverShell.vue'
|
|
29
28
|
|
|
30
29
|
const { t } = useI18n()
|
|
31
30
|
const ui = useUiStore()
|
|
@@ -119,12 +118,11 @@ watch(
|
|
|
119
118
|
{ immediate: true },
|
|
120
119
|
)
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Esc backs out of the editor first, then closes the panel.
|
|
121
|
+
// Esc backs out of the editor first, then closes the panel.
|
|
122
|
+
function backOut() {
|
|
125
123
|
if (editor.value) editor.value = null
|
|
126
124
|
else open.value = false
|
|
127
|
-
}
|
|
125
|
+
}
|
|
128
126
|
|
|
129
127
|
/** The selectable catalog models, as `{ id, label, suffix }` for a dropdown. */
|
|
130
128
|
const selectableModels = computed(() => {
|
|
@@ -309,13 +307,13 @@ async function save() {
|
|
|
309
307
|
<template>
|
|
310
308
|
<Teleport to="body">
|
|
311
309
|
<Transition name="reader-fade">
|
|
312
|
-
<
|
|
310
|
+
<TakeoverShell
|
|
313
311
|
v-if="open"
|
|
312
|
+
:label="t('settings.modelConfiguration.title')"
|
|
313
|
+
:content-key="editor ? 'editor' : 'list'"
|
|
314
|
+
class="flex-col bg-app-950/96 backdrop-blur-sm"
|
|
314
315
|
data-testid="model-configuration-panel"
|
|
315
|
-
|
|
316
|
-
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
317
|
-
role="dialog"
|
|
318
|
-
aria-modal="true"
|
|
316
|
+
@close="backOut"
|
|
319
317
|
>
|
|
320
318
|
<header class="flex items-center gap-3 border-b border-default px-6 py-4">
|
|
321
319
|
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/15">
|
|
@@ -444,6 +442,7 @@ async function save() {
|
|
|
444
442
|
variant="ghost"
|
|
445
443
|
color="error"
|
|
446
444
|
icon="i-lucide-trash-2"
|
|
445
|
+
data-testid="model-preset-delete"
|
|
447
446
|
:disabled="p.isDefault"
|
|
448
447
|
:loading="busy"
|
|
449
448
|
:label="
|
|
@@ -518,6 +517,7 @@ async function save() {
|
|
|
518
517
|
variant="subtle"
|
|
519
518
|
trailing-icon="i-lucide-chevron-down"
|
|
520
519
|
class="w-full justify-between"
|
|
520
|
+
data-testid="model-preset-base-model"
|
|
521
521
|
>
|
|
522
522
|
<span class="truncate">{{ modelLabel(editor.baseModelId) }}</span>
|
|
523
523
|
</UButton>
|
|
@@ -618,7 +618,7 @@ async function save() {
|
|
|
618
618
|
</template>
|
|
619
619
|
</div>
|
|
620
620
|
</div>
|
|
621
|
-
</
|
|
621
|
+
</TakeoverShell>
|
|
622
622
|
</Transition>
|
|
623
623
|
</Teleport>
|
|
624
624
|
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sharedOverlayStack } from '@modular-vue/core'
|
|
2
|
+
import { trackForeignLayers } from '~/utils/foreignLayers'
|
|
3
|
+
|
|
4
|
+
// Put every open Nuxt UI menu, select, popover and modal on the shared overlay stack, so the
|
|
5
|
+
// takeover or window under it leaves Escape and Tab to it. Why: `utils/foreignLayers.ts`.
|
|
6
|
+
export default defineNuxtPlugin(() => {
|
|
7
|
+
trackForeignLayers(document.body, sharedOverlayStack)
|
|
8
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createOverlayStack, type OverlayStack } from '@modular-vue/core'
|
|
3
|
+
import { trackForeignLayers } from '~/utils/foreignLayers'
|
|
4
|
+
|
|
5
|
+
/** A Reka-shaped layer as Nuxt UI portals it: a wrapper on `<body>` around the layer element. */
|
|
6
|
+
function openLayer(root: HTMLElement, role: string | null): HTMLElement {
|
|
7
|
+
const wrapper = document.createElement('div')
|
|
8
|
+
const layer = document.createElement('div')
|
|
9
|
+
layer.setAttribute('data-dismissable-layer', '')
|
|
10
|
+
layer.dataset.state = 'open'
|
|
11
|
+
if (role) layer.setAttribute('role', role)
|
|
12
|
+
wrapper.append(layer)
|
|
13
|
+
root.append(wrapper)
|
|
14
|
+
return layer
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** MutationObserver callbacks run as a microtask after the DOM change. */
|
|
18
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
19
|
+
|
|
20
|
+
describe('trackForeignLayers', () => {
|
|
21
|
+
let root: HTMLElement
|
|
22
|
+
let stack: OverlayStack
|
|
23
|
+
let stop: () => void
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
root = document.createElement('div')
|
|
27
|
+
document.body.append(root)
|
|
28
|
+
stack = createOverlayStack()
|
|
29
|
+
stop = trackForeignLayers(root, stack)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
stop()
|
|
34
|
+
root.remove()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('puts an open menu above the overlay under it, and gives the top back when it closes', async () => {
|
|
38
|
+
const takeover = stack.push()
|
|
39
|
+
const menu = openLayer(root, 'menu')
|
|
40
|
+
await settle()
|
|
41
|
+
expect(takeover.isTop()).toBe(false)
|
|
42
|
+
|
|
43
|
+
menu.parentElement!.remove()
|
|
44
|
+
await settle()
|
|
45
|
+
expect(takeover.isTop()).toBe(true)
|
|
46
|
+
expect(stack.size).toBe(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('gives the top back when a layer starts its exit animation, before it leaves the DOM', async () => {
|
|
50
|
+
const takeover = stack.push()
|
|
51
|
+
const menu = openLayer(root, 'menu')
|
|
52
|
+
await settle()
|
|
53
|
+
|
|
54
|
+
menu.dataset.state = 'closed'
|
|
55
|
+
await settle()
|
|
56
|
+
expect(takeover.isTop()).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('takes a ticket for each focus-taking role, and none for a tooltip', async () => {
|
|
60
|
+
for (const role of ['menu', 'listbox', 'dialog', 'alertdialog']) openLayer(root, role)
|
|
61
|
+
openLayer(root, null)
|
|
62
|
+
await settle()
|
|
63
|
+
expect(stack.size).toBe(4)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('keeps open order, so a menu opened over a modal is the top layer', async () => {
|
|
67
|
+
const takeover = stack.push()
|
|
68
|
+
const modal = openLayer(root, 'dialog')
|
|
69
|
+
await settle()
|
|
70
|
+
const window = stack.push()
|
|
71
|
+
expect(window.isTop()).toBe(true)
|
|
72
|
+
|
|
73
|
+
// A menu opened inside the window goes above it, and closing the modal under both changes
|
|
74
|
+
// nothing for the window.
|
|
75
|
+
const menu = openLayer(root, 'menu')
|
|
76
|
+
await settle()
|
|
77
|
+
expect(window.isTop()).toBe(false)
|
|
78
|
+
modal.parentElement!.remove()
|
|
79
|
+
menu.parentElement!.remove()
|
|
80
|
+
await settle()
|
|
81
|
+
expect(window.isTop()).toBe(true)
|
|
82
|
+
window.release()
|
|
83
|
+
expect(takeover.isTop()).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('counts a layer that was already open when tracking started', () => {
|
|
87
|
+
stop()
|
|
88
|
+
openLayer(root, 'dialog')
|
|
89
|
+
stop = trackForeignLayers(root, stack)
|
|
90
|
+
expect(stack.size).toBe(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('releases every ticket when tracking stops', async () => {
|
|
94
|
+
openLayer(root, 'menu')
|
|
95
|
+
openLayer(root, 'dialog')
|
|
96
|
+
await settle()
|
|
97
|
+
stop()
|
|
98
|
+
expect(stack.size).toBe(0)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Nuxt UI layers on the shared overlay stack.
|
|
2
|
+
//
|
|
3
|
+
// Two layer stacks run in this app. `useModalBehavior` (`TakeoverShell`, `ResultWindowShell`, the
|
|
4
|
+
// modular overlays) keeps `sharedOverlayStack`; Reka, under every Nuxt UI menu, select, popover
|
|
5
|
+
// and modal, keeps its own. Each one only knows its own layers, so a dropdown opened inside a
|
|
6
|
+
// takeover lost Escape: the shell handles the key first, in the capture phase, and calls
|
|
7
|
+
// `preventDefault()`, and Reka then leaves a prevented Escape alone. The takeover closed and the
|
|
8
|
+
// dropdown stayed.
|
|
9
|
+
//
|
|
10
|
+
// The fix is ONE stack. While a Reka layer is open it holds a ticket, pushed in open order, so
|
|
11
|
+
// it is the top overlay and `useModalBehavior` stands down: no Escape, no Tab trap. Reka then
|
|
12
|
+
// handles its own keys. Only the stack's own API is used here (`push`, `release`); what is
|
|
13
|
+
// Reka-specific is the selector.
|
|
14
|
+
|
|
15
|
+
import type { OverlayStack, OverlayStackTicket } from '@modular-vue/core'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An open Reka layer that takes focus and owns its keys. Reka marks every layer with
|
|
19
|
+
* `data-dismissable-layer`, and the role is on the same element: `menu` (dropdown menu), `listbox`
|
|
20
|
+
* (select), `dialog` (popover, modal), `alertdialog`. A tooltip is a layer too, but it has no
|
|
21
|
+
* role, so it never takes a ticket: focus stays on its trigger, and a ticket would switch off the
|
|
22
|
+
* Tab trap under it. `data-state="closed"` is a layer playing its exit animation.
|
|
23
|
+
*/
|
|
24
|
+
export const FOREIGN_LAYER_SELECTOR =
|
|
25
|
+
'[data-dismissable-layer]:is([role="menu"],[role="listbox"],[role="dialog"],[role="alertdialog"]):not([data-state="closed"])'
|
|
26
|
+
|
|
27
|
+
const ANY_LAYER = '[data-dismissable-layer]'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hold one `stack` ticket for each open layer under `root`, for as long as it is open. Returns the
|
|
31
|
+
* function that stops tracking and releases every ticket.
|
|
32
|
+
*
|
|
33
|
+
* What is observed is kept narrow, because `root` is `<body>` and the board re-renders under it
|
|
34
|
+
* constantly. Reka teleports each layer to `<body>`, so an opening or a closing is a change to
|
|
35
|
+
* `root`'s DIRECT children, which a non-subtree `childList` observer sees without ever being
|
|
36
|
+
* told about a board patch. A layer's own exit animation is its `data-state` flipping to
|
|
37
|
+
* `closed`, which the attribute observer sees; it is filtered to that one attribute and then to
|
|
38
|
+
* records whose target is a layer. Only those records trigger the one `sync` query.
|
|
39
|
+
*/
|
|
40
|
+
export function trackForeignLayers(root: HTMLElement, stack: OverlayStack): () => void {
|
|
41
|
+
const tickets = new Map<Element, OverlayStackTicket>()
|
|
42
|
+
|
|
43
|
+
function sync(): void {
|
|
44
|
+
const open = new Set(root.querySelectorAll(FOREIGN_LAYER_SELECTOR))
|
|
45
|
+
for (const [layer, ticket] of tickets) {
|
|
46
|
+
if (open.has(layer)) continue
|
|
47
|
+
ticket.release()
|
|
48
|
+
tickets.delete(layer)
|
|
49
|
+
}
|
|
50
|
+
// Document order is open order: Reka appends each layer's portal to the end of `<body>`.
|
|
51
|
+
for (const layer of open) {
|
|
52
|
+
if (!tickets.has(layer)) tickets.set(layer, stack.push())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const portals = new MutationObserver(sync)
|
|
57
|
+
portals.observe(root, { childList: true })
|
|
58
|
+
|
|
59
|
+
const layerState = new MutationObserver((records) => {
|
|
60
|
+
if (records.some((r) => r.target instanceof Element && r.target.matches(ANY_LAYER))) sync()
|
|
61
|
+
})
|
|
62
|
+
layerState.observe(root, { subtree: true, attributes: true, attributeFilter: ['data-state'] })
|
|
63
|
+
sync()
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
portals.disconnect()
|
|
67
|
+
layerState.disconnect()
|
|
68
|
+
for (const ticket of tickets.values()) ticket.release()
|
|
69
|
+
tickets.clear()
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.315.
|
|
3
|
+
"version": "0.315.8",
|
|
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,7 +18,7 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@cat-factory/contracts": "0.
|
|
21
|
+
"@cat-factory/contracts": "0.369.0",
|
|
22
22
|
"@modular-frontend/core": "0.6.0",
|
|
23
23
|
"@modular-vue/core": "^1.5.0",
|
|
24
24
|
"@modular-vue/journeys": "^1.4.0",
|