@cat-factory/app 0.310.3 → 0.311.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/board/DependencyConnectOverlay.vue +1 -0
- package/app/components/common/AsyncViewError.vue +1 -0
- package/app/components/github/GitHubOnboarding.vue +9 -1
- package/app/components/kaizen/KaizenPanel.vue +3 -1
- package/app/components/layout/AccountTeamSettings.vue +1 -1
- package/app/components/layout/BoardSwitcher.vue +33 -14
- package/app/components/layout/DefaultTestEnvBanner.vue +4 -1
- package/app/components/layout/InfraSetupBanner.vue +26 -2
- package/app/components/layout/InfraSetupNotice.vue +69 -0
- package/app/components/layout/ProviderConfigBanner.vue +8 -1
- package/app/components/layout/SideBar.vue +4 -1
- package/app/components/media/ArtifactLightbox.vue +3 -2
- package/app/components/panels/AgentStepDetail.vue +5 -3
- package/app/components/panels/InspectorPanel.vue +2 -1
- package/app/components/panels/ObservabilityPanel.vue +19 -12
- package/app/components/panels/OperatorDashboardPanel.vue +36 -28
- package/app/components/panels/ReportsPanel.vue +3 -1
- package/app/components/panels/ResultWindowShell.logic.ts +4 -4
- package/app/components/panels/ResultWindowShell.vue +8 -3
- package/app/components/providers/AiProviderOnboardingModal.vue +7 -1
- package/app/components/providers/ApiKeysSection.vue +21 -2
- package/app/components/settings/ModelConfigurationPanel.vue +3 -1
- package/app/components/vcs/VcsConnectSurfaces.vue +30 -14
- package/app/composables/useSetupActor.ts +25 -0
- package/app/composables/useWorkspaceAccess.ts +1 -1
- package/app/modular/nav-gates.ts +1 -2
- package/app/stores/accounts.ts +8 -0
- package/app/stores/workspace/infraSetup.ts +11 -3
- package/app/stores/workspace.ts +3 -1
- package/app/types/domain.ts +2 -0
- package/app/utils/safeArea.spec.ts +33 -0
- package/app/utils/safeArea.ts +41 -0
- package/app/utils/setupAudience.spec.ts +115 -0
- package/app/utils/setupAudience.ts +102 -0
- package/i18n/locales/de.json +15 -1
- package/i18n/locales/en.json +15 -1
- package/i18n/locales/es.json +15 -1
- package/i18n/locales/fr.json +15 -1
- package/i18n/locales/he.json +15 -1
- package/i18n/locales/it.json +15 -1
- package/i18n/locales/ja.json +15 -1
- package/i18n/locales/pl.json +15 -1
- package/i18n/locales/tr.json +15 -1
- package/i18n/locales/uk.json +15 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -321,6 +321,15 @@ That is the whole mechanism, and it is why there is no rule here about WHICH ste
|
|
|
321
321
|
|
|
322
322
|
The default `radius` lives on the `Cat Factory` document in `app/utils/theme/builtins.ts`. It equals `THEME_DEFAULTS.radius` and so writes no CSS, which is the point: changing the number there is the whole edit.
|
|
323
323
|
|
|
324
|
+
### Full-screen overlays bind their root class
|
|
325
|
+
|
|
326
|
+
A phone's notch and home indicator cover whatever sits at the screen edge, and on a screen without them nothing looks wrong. So a full-screen overlay never spells `fixed inset-0` itself. Its root binds one of the two class lists in [`utils/safeArea.ts`](./app/utils/safeArea.ts), which carry the positioning, the `100dvh` cap and the safe-area padding, and the site adds only its stacking, direction and backdrop:
|
|
327
|
+
|
|
328
|
+
- **`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.
|
|
329
|
+
- **`WINDOWED_OVERLAY_CLASS`** for a window on a backdrop (`ResultWindowShell`). Its gutter is 1rem or the inset, whichever is larger.
|
|
330
|
+
|
|
331
|
+
`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.
|
|
332
|
+
|
|
324
333
|
### A control is its Nuxt UI component
|
|
325
334
|
|
|
326
335
|
Every CONTROL in the SPA is the Nuxt UI component for it, never the raw HTML element. A raw `<button>` beside a `UButton` is the same control rendered twice, and only one of the two follows the theme, the focus ring, the disabled state and the size scale; the SPA carried 152 of them, 24 raw `<input>`, five hand-built tables and six hand-rolled `<details>`, each with its own border, background and hover recipe.
|
|
@@ -611,6 +620,14 @@ The seams, and what a new feature should use rather than reading the store ad ho
|
|
|
611
620
|
- **A surface that narrows inline** reads `useUiRoleStore().fullSurface` (the frame header's bug-hunt
|
|
612
621
|
button, the palette's per-connection integration commands). Same rule as the tier: what remains
|
|
613
622
|
must be exactly what the full surface would have shown, only less of it.
|
|
623
|
+
- **A setup prompt** (a "configure this" card over the board) goes through `canActOnSetup`
|
|
624
|
+
(`app/utils/setupAudience.ts`) with the caller from `useSetupActor()`: it combines the role's
|
|
625
|
+
surface with the board and account grants and the owner the server reports for the gap
|
|
626
|
+
(`infraSetupOwners`). A caller who cannot act gets no card. If the gap stops runs they could start,
|
|
627
|
+
it reaches them as one line saying how it gets fixed (`blockingSetupNotice`): the owner to ask, or
|
|
628
|
+
the role switch when they hold the grant and only the designer surface hides the card. A viewer
|
|
629
|
+
gets nothing, and the line honours the card's dismissals. Never a button that opens a screen the
|
|
630
|
+
caller's save is refused on.
|
|
614
631
|
- **A tutorial tour whose step clicks a non-`intake` nav entry declares
|
|
615
632
|
`TUTORIAL_REQUIREMENTS.fullSurface`.** Which tours those are is not a judgement call:
|
|
616
633
|
`tutorial-tours.spec.ts` derives the pairing from `navItemVisible`, so a tour that gains such a
|
package/app/assets/css/main.css
CHANGED
|
@@ -84,6 +84,16 @@ body,
|
|
|
84
84
|
height: 100%;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/* No pull-to-refresh and no scroll chaining out of the app. The SPA is a full-screen canvas whose
|
|
88
|
+
* body never scrolls, so a vertical overscroll that reaches the viewport (from a modal's header,
|
|
89
|
+
* the end of a list, a gutter) has nothing to scroll and on a phone would reload the app instead,
|
|
90
|
+
* dropping whatever was open. Setting it on the root once covers every overlay, and inner scrollers
|
|
91
|
+
* still chain to the scroller around them. */
|
|
92
|
+
html,
|
|
93
|
+
body {
|
|
94
|
+
overscroll-behavior-y: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
body {
|
|
88
98
|
margin: 0;
|
|
89
99
|
overflow: hidden;
|
|
@@ -7,6 +7,7 @@ const { connecting } = useDependencyConnect()
|
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<template>
|
|
10
|
+
<!-- safe-area-ok: a pointer-events-none drawing layer with no controls or text -->
|
|
10
11
|
<svg
|
|
11
12
|
v-if="connecting"
|
|
12
13
|
class="pointer-events-none fixed inset-0 z-50 h-full w-full overflow-visible"
|
|
@@ -9,6 +9,7 @@ function reload() {
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
+
<!-- safe-area-ok: a scrim with one card centred on it, so no content reaches an edge -->
|
|
12
13
|
<div
|
|
13
14
|
data-testid="async-view-error"
|
|
14
15
|
class="fixed inset-0 z-50 grid place-items-center bg-app-950/80 p-6 backdrop-blur-sm"
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
// has no connection yet. cat-factory's whole flow runs on a connected repository host (agents
|
|
4
4
|
// open pull/merge requests on the user's repos), so the board is withheld until the workspace
|
|
5
5
|
// connects one. <VcsConnectSurfaces> renders whichever connect methods the deployment serves; a
|
|
6
|
-
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts.
|
|
6
|
+
// "Sign out" escape hatch avoids trapping a user who needs to switch accounts. The board switcher is
|
|
7
|
+
// the other escape: the gate replaces the whole board, sidebar included, so without it a member
|
|
8
|
+
// who landed on a board they cannot connect had no way to reach a board they can.
|
|
7
9
|
import VcsConnectSurfaces from '~/components/vcs/VcsConnectSurfaces.vue'
|
|
10
|
+
import BoardSwitcher from '~/components/layout/BoardSwitcher.vue'
|
|
8
11
|
import { VCS_PROVIDER_ICONS, VCS_PROVIDER_LABELS } from '~/utils/vcs'
|
|
9
12
|
|
|
10
13
|
const { t } = useI18n()
|
|
@@ -39,6 +42,11 @@ const title = computed(() =>
|
|
|
39
42
|
|
|
40
43
|
<VcsConnectSurfaces :app-intro="t('github.onboarding.appIntro')" />
|
|
41
44
|
|
|
45
|
+
<div class="mt-6 border-t border-default pt-4" data-testid="vcs-onboarding-switch-board">
|
|
46
|
+
<p class="mb-2 text-xs text-muted">{{ t('vcs.onboarding.switchBoard') }}</p>
|
|
47
|
+
<BoardSwitcher standalone />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
42
50
|
<p
|
|
43
51
|
v-if="auth.required && auth.user"
|
|
44
52
|
class="mt-6 border-t border-default pt-4 text-center text-xs text-dimmed"
|
|
@@ -4,6 +4,7 @@ import { onKeyStroke } from '@vueuse/core'
|
|
|
4
4
|
import type { TableColumn } from '@nuxt/ui'
|
|
5
5
|
import type { KaizenGrading } from '~/types/domain'
|
|
6
6
|
import { agentKindMeta } from '~/utils/catalog'
|
|
7
|
+
import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
|
|
7
8
|
|
|
8
9
|
// The Kaizen screen: a full-panel overlay listing the workspace's grading history and
|
|
9
10
|
// its verified-combo library. Opened via `ui.openKaizen()` from the sidebar. Read-only —
|
|
@@ -66,7 +67,8 @@ const historyColumns = computed<TableColumn<KaizenGrading>[]>(() => [
|
|
|
66
67
|
<Transition name="kz-fade">
|
|
67
68
|
<div
|
|
68
69
|
v-if="open"
|
|
69
|
-
class="
|
|
70
|
+
class="z-[60] flex-col bg-app-950/96 backdrop-blur-sm"
|
|
71
|
+
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
70
72
|
role="dialog"
|
|
71
73
|
aria-modal="true"
|
|
72
74
|
>
|
|
@@ -53,7 +53,7 @@ function invitationStatusLabel(status: InvitationStatus): string {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Whether the signed-in caller is an admin of this account (drives edit affordances). */
|
|
56
|
-
const isAdmin = computed(() => accounts.
|
|
56
|
+
const isAdmin = computed(() => accounts.isActiveAccountAdmin)
|
|
57
57
|
/**
|
|
58
58
|
* Members / roles / invitations are org-scoped — the backend rejects membership on a
|
|
59
59
|
* personal account. For a personal account we show a "create an organization" CTA in
|
|
@@ -12,7 +12,16 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
|
12
12
|
// account row folds away — it is a label with a menu duplicated inside the board menu's
|
|
13
13
|
// reach — and the board button keeps only its glyph. Both dropdowns are unchanged, so
|
|
14
14
|
// switching boards never requires expanding the sidebar first.
|
|
15
|
-
|
|
15
|
+
//
|
|
16
|
+
// `standalone` is for a host OUTSIDE the board page (the repository-host gate), where the
|
|
17
|
+
// page-level dialogs are not mounted. The items that open one are left out there: "Account
|
|
18
|
+
// settings" opens `AccountSettingsPanel` and "Delete board" awaits `ConfirmDialog`, so on the gate
|
|
19
|
+
// both would do nothing and then fire once a board page mounted, the delete against the board that
|
|
20
|
+
// was active when it was asked.
|
|
21
|
+
const props = withDefaults(defineProps<{ collapsed?: boolean; standalone?: boolean }>(), {
|
|
22
|
+
collapsed: false,
|
|
23
|
+
standalone: false,
|
|
24
|
+
})
|
|
16
25
|
|
|
17
26
|
const { t } = useI18n()
|
|
18
27
|
|
|
@@ -65,14 +74,19 @@ const accountItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
65
74
|
},
|
|
66
75
|
// Account settings: the unified per-account panel (team + roles + invitations + email
|
|
67
76
|
// sender + account-tier fragment library). The panel itself handles personal accounts
|
|
68
|
-
// (prompting to create an org for the team tab), so this is not gated
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
// (prompting to create an org for the team tab), so this is not gated by role; it is left out
|
|
78
|
+
// only where the panel is not mounted (`standalone`).
|
|
79
|
+
...(props.standalone
|
|
80
|
+
? []
|
|
81
|
+
: [
|
|
82
|
+
{
|
|
83
|
+
label: t('layout.boardSwitcher.account.settings'),
|
|
84
|
+
icon: 'i-lucide-settings',
|
|
85
|
+
onSelect: () => ui.openAccountSettings(),
|
|
86
|
+
},
|
|
87
|
+
]),
|
|
74
88
|
// Admins can set the account-wide default provider new services inherit.
|
|
75
|
-
...(accounts.
|
|
89
|
+
...(accounts.isActiveAccountAdmin
|
|
76
90
|
? [
|
|
77
91
|
{
|
|
78
92
|
label: t('layout.boardSwitcher.account.defaultProvider'),
|
|
@@ -122,12 +136,17 @@ const boardItems = computed<DropdownMenuItem[][]>(() => [
|
|
|
122
136
|
icon: 'i-lucide-pencil',
|
|
123
137
|
onSelect: () => openPrompt('rename'),
|
|
124
138
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
// Delete confirms through the page's `ConfirmDialog`, absent in `standalone`.
|
|
140
|
+
...(props.standalone
|
|
141
|
+
? []
|
|
142
|
+
: [
|
|
143
|
+
{
|
|
144
|
+
label: t('layout.boardSwitcher.board.delete'),
|
|
145
|
+
icon: 'i-lucide-trash-2',
|
|
146
|
+
color: 'error' as const,
|
|
147
|
+
onSelect: () => void removeBoard(),
|
|
148
|
+
},
|
|
149
|
+
]),
|
|
131
150
|
]
|
|
132
151
|
: []),
|
|
133
152
|
],
|
|
@@ -22,14 +22,17 @@ const ui = useUiStore()
|
|
|
22
22
|
const workspace = useWorkspaceStore()
|
|
23
23
|
const settingsStore = useWorkspaceSettingsStore()
|
|
24
24
|
const { canManageSettings } = useWorkspaceAccess()
|
|
25
|
+
const uiRole = useUiRoleStore()
|
|
25
26
|
|
|
26
27
|
// Only prompt someone who can actually answer. A member/viewer cannot write workspace settings
|
|
27
28
|
// (the endpoint is `settings.manage`-gated), so for them this would be an un-actionable nag
|
|
28
|
-
// pointing at a screen that would refuse their save.
|
|
29
|
+
// pointing at a screen that would refuse their save. The designer surface never configures the
|
|
30
|
+
// platform, so it is kept off there too, whatever the grant.
|
|
29
31
|
const show = computed(
|
|
30
32
|
() =>
|
|
31
33
|
workspace.ready &&
|
|
32
34
|
canManageSettings.value &&
|
|
35
|
+
uiRole.fullSurface &&
|
|
33
36
|
needsDefaultProvisioningChoice(settingsStore.settings) &&
|
|
34
37
|
!ui.defaultProvisionDismissed,
|
|
35
38
|
)
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// runtime whose account picked no backend — incl. Cloudflare without an ARTIFACT_BUCKET binding;
|
|
7
7
|
// ephemeral test environments on any runtime that wires the integration).
|
|
8
8
|
//
|
|
9
|
+
// Audience: a card renders only for a caller who can act on its area, per the server-reported
|
|
10
|
+
// owner (`infraSetupOwners`) and `canActOnSetup`. A caller who cannot (a member, a viewer, the
|
|
11
|
+
// designer surface, or anyone when the operator owns the gap) sees one compact line instead, and
|
|
12
|
+
// only for a gap that stops every run (`InfraSetupNotice`).
|
|
13
|
+
//
|
|
9
14
|
// Positioning/stacking against the sibling advisory banners (AI-readiness, provider-config) is
|
|
10
15
|
// owned by the shared, click-through banner column in `pages/index.vue` — so concurrent prompts
|
|
11
16
|
// stack vertically instead of drawing on top of each other. This component only stacks its OWN
|
|
@@ -52,11 +57,14 @@ import {
|
|
|
52
57
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
53
58
|
import type { InfraSetupArea, InfraSetupStatus } from '~/types/domain'
|
|
54
59
|
import { infraSetupDismissalKey, type InfraSetupCardKind } from '~/utils/infraSetup'
|
|
60
|
+
import { blockingSetupNotice, canActOnSetup } from '~/utils/setupAudience'
|
|
61
|
+
import InfraSetupNotice from '~/components/layout/InfraSetupNotice.vue'
|
|
55
62
|
|
|
56
63
|
const { t } = useI18n()
|
|
57
64
|
const ui = useUiStore()
|
|
58
65
|
const auth = useAuthStore()
|
|
59
66
|
const workspace = useWorkspaceStore()
|
|
67
|
+
const { actor } = useSetupActor()
|
|
60
68
|
|
|
61
69
|
// Severity order: no executor blocks EVERY agent, so it leads; a missing test environment blocks
|
|
62
70
|
// only testing agents; missing storage only degrades the UI-tester's screenshots.
|
|
@@ -140,6 +148,9 @@ const visible = computed<AreaCard[]>(() => {
|
|
|
140
148
|
return AREAS.filter((area) => {
|
|
141
149
|
const kind = cardKind(status[area])
|
|
142
150
|
if (!kind) return false
|
|
151
|
+
// A card is for the people who can act on it. Everyone else hears about a gap only when it
|
|
152
|
+
// stops their own work, through the one line below (`blockingSetupNotice`).
|
|
153
|
+
if (!canActOnSetup(workspace.infraSetupOwners?.[area], actor.value)) return false
|
|
143
154
|
// Both dismissals are keyed by the CLAIM, not by the area: silencing "you haven't configured
|
|
144
155
|
// this" must not also silence "you configured it and it is now down". The permanent dismissal
|
|
145
156
|
// only ever covers a setup gap (`dismissPermanently` is offered nowhere else).
|
|
@@ -152,6 +163,18 @@ const visible = computed<AreaCard[]>(() => {
|
|
|
152
163
|
}))
|
|
153
164
|
})
|
|
154
165
|
|
|
166
|
+
// The one line for a caller who cannot act on a gap that stops every run. It honours BOTH
|
|
167
|
+
// dismissals of the card it stands in for, keyed by the same claim: someone who said "don't
|
|
168
|
+
// notify me again" on the card (before their role changed, say) is not nagged again by the line.
|
|
169
|
+
const notice = computed(() => {
|
|
170
|
+
const found = blockingSetupNotice(workspace.infraSetup, workspace.infraSetupOwners, actor.value)
|
|
171
|
+
if (!found) return null
|
|
172
|
+
if (ui.infraSetupSessionDismissed.includes(infraSetupDismissalKey(found.area, found.kind))) {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
return found.kind === 'setup' && dismissedForUser.value.includes(found.area) ? null : found
|
|
176
|
+
})
|
|
177
|
+
|
|
155
178
|
/** Which card an area's status raises, or null when it raises none (`configured`/`not_applicable`). */
|
|
156
179
|
function cardKind(status: InfraSetupStatus): InfraSetupCardKind | null {
|
|
157
180
|
if (isInfraSetupHealthStatus(status)) return 'outage'
|
|
@@ -169,7 +192,7 @@ function titleKey(card: AreaCard): string {
|
|
|
169
192
|
* The dismiss dropdown: the product wants the user asked WHICH kind of dismissal on close. An
|
|
170
193
|
* outage offers the session option ONLY — see the fork note at the top of this file.
|
|
171
194
|
*/
|
|
172
|
-
function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
|
|
195
|
+
function dismissMenu(card: Pick<AreaCard, 'area' | 'kind'>): DropdownMenuItem[][] {
|
|
173
196
|
const session = {
|
|
174
197
|
label: t('layout.infraSetupBanner.dismiss.session'),
|
|
175
198
|
icon: 'i-lucide-clock',
|
|
@@ -195,11 +218,12 @@ function dismissMenu(card: AreaCard): DropdownMenuItem[][] {
|
|
|
195
218
|
than an assertive `role="alert"` per card — an advisory setup nag shouldn't interrupt a
|
|
196
219
|
screen reader, and up to three stacked alerts would spam it. -->
|
|
197
220
|
<div
|
|
198
|
-
v-if="visible.length > 0"
|
|
221
|
+
v-if="visible.length > 0 || notice"
|
|
199
222
|
class="flex w-full flex-col items-center gap-2"
|
|
200
223
|
role="status"
|
|
201
224
|
aria-live="polite"
|
|
202
225
|
>
|
|
226
|
+
<InfraSetupNotice v-if="notice" :notice="notice" :dismiss-menu="dismissMenu(notice)" />
|
|
203
227
|
<!-- An OUTAGE reads red, a setup gap amber: one is something breaking now, the other is
|
|
204
228
|
something never switched on, and a reader has to be able to tell at a glance. -->
|
|
205
229
|
<div
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The one line a caller sees for a setup gap they cannot fix from where they are, when it stops
|
|
3
|
+
// their runs. It replaces the full card (and its "Configure" button that would open a screen their
|
|
4
|
+
// save is refused on) with two facts: what does not work, and how it gets fixed. Compact on
|
|
5
|
+
// purpose: the caller has nothing to do here, so it must not cover the board the way an actionable
|
|
6
|
+
// card may.
|
|
7
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
8
|
+
import type { BlockingInfraArea, SetupNotice, SetupRemedy } from '~/utils/setupAudience'
|
|
9
|
+
|
|
10
|
+
defineProps<{
|
|
11
|
+
notice: SetupNotice
|
|
12
|
+
/** The card's own session / permanent choice, built by the banner so both close the same claim. */
|
|
13
|
+
dismissMenu: DropdownMenuItem[][]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
|
|
18
|
+
// Exhaustive over the blocking areas and the remedies, so a new member of either fails typecheck
|
|
19
|
+
// here rather than rendering a raw key.
|
|
20
|
+
const STATE_KEYS: Record<BlockingInfraArea, Record<SetupNotice['kind'], string>> = {
|
|
21
|
+
agentExecutor: {
|
|
22
|
+
setup: 'layout.infraSetupNotice.agentExecutor.setup',
|
|
23
|
+
outage: 'layout.infraSetupNotice.agentExecutor.outage',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
const REMEDY_KEYS: Record<SetupRemedy, string> = {
|
|
27
|
+
workspace_admin: 'layout.infraSetupNotice.owner.workspace_admin',
|
|
28
|
+
account_admin: 'layout.infraSetupNotice.owner.account_admin',
|
|
29
|
+
operator: 'layout.infraSetupNotice.owner.operator',
|
|
30
|
+
full_surface: 'layout.infraSetupNotice.fullSurface',
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div
|
|
36
|
+
class="pointer-events-auto flex w-full max-w-3xl items-center gap-3 rounded-xl border px-4 py-2 shadow-lg backdrop-blur"
|
|
37
|
+
:class="
|
|
38
|
+
notice.kind === 'outage'
|
|
39
|
+
? 'border-app-error-500/50 bg-app-error-950/90'
|
|
40
|
+
: 'border-app-warning-500/50 bg-app-warning-950/90'
|
|
41
|
+
"
|
|
42
|
+
:data-testid="`infra-setup-notice-${notice.area}`"
|
|
43
|
+
:data-infra-remedy="notice.remedy"
|
|
44
|
+
>
|
|
45
|
+
<UIcon
|
|
46
|
+
:name="notice.kind === 'outage' ? 'i-lucide-plug-zap' : 'i-lucide-server-off'"
|
|
47
|
+
class="h-5 w-5 shrink-0"
|
|
48
|
+
:class="notice.kind === 'outage' ? 'text-app-error-400' : 'text-app-warning-400'"
|
|
49
|
+
/>
|
|
50
|
+
<p
|
|
51
|
+
class="min-w-0 flex-1 text-sm"
|
|
52
|
+
:class="notice.kind === 'outage' ? 'text-app-error-100' : 'text-app-warning-100'"
|
|
53
|
+
>
|
|
54
|
+
{{ t(STATE_KEYS[notice.area][notice.kind]) }}
|
|
55
|
+
<span class="opacity-80">{{ t(REMEDY_KEYS[notice.remedy]) }}</span>
|
|
56
|
+
</p>
|
|
57
|
+
<UDropdownMenu :items="dismissMenu" :content="{ align: 'end' }">
|
|
58
|
+
<UButton
|
|
59
|
+
color="neutral"
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="xs"
|
|
62
|
+
icon="i-lucide-x"
|
|
63
|
+
class="shrink-0"
|
|
64
|
+
:aria-label="t('common.close')"
|
|
65
|
+
:data-testid="`infra-setup-notice-dismiss-${notice.area}`"
|
|
66
|
+
/>
|
|
67
|
+
</UDropdownMenu>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// link into its connect panel. Dismissible per session.
|
|
7
7
|
import { computed, ref, watch } from 'vue'
|
|
8
8
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
9
|
+
import { canActOnSetup } from '~/utils/setupAudience'
|
|
9
10
|
|
|
10
11
|
const { t } = useI18n()
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const workspace = useWorkspaceStore()
|
|
13
14
|
const store = useProviderConnectionsStore()
|
|
15
|
+
const { actor } = useSetupActor()
|
|
14
16
|
|
|
15
17
|
// Exhaustive per-kind title/action keys (leaf names mirror the kind verbatim).
|
|
16
18
|
const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
|
|
@@ -33,7 +35,12 @@ watch(
|
|
|
33
35
|
|
|
34
36
|
const dismissed = ref(false)
|
|
35
37
|
const pending = computed(() => store.needingConfig as ProviderConnectionKind[])
|
|
36
|
-
|
|
38
|
+
// Only for the people whose save would land: the connection fields are a workspace-admin write,
|
|
39
|
+
// and a member or a designer could only open the panel and be refused.
|
|
40
|
+
const show = computed(
|
|
41
|
+
() =>
|
|
42
|
+
pending.value.length > 0 && !dismissed.value && canActOnSetup('workspace_admin', actor.value),
|
|
43
|
+
)
|
|
37
44
|
</script>
|
|
38
45
|
|
|
39
46
|
<template>
|
|
@@ -154,6 +154,7 @@ watch(
|
|
|
154
154
|
enter-from-class="opacity-0"
|
|
155
155
|
leave-to-class="opacity-0"
|
|
156
156
|
>
|
|
157
|
+
<!-- safe-area-ok: a scrim with no content; the drawer below pads its own edges -->
|
|
157
158
|
<div
|
|
158
159
|
v-if="ui.mobileNavOpen"
|
|
159
160
|
class="fixed inset-0 z-30 bg-app-950/60 backdrop-blur-sm lg:hidden"
|
|
@@ -165,6 +166,8 @@ watch(
|
|
|
165
166
|
/>
|
|
166
167
|
</Transition>
|
|
167
168
|
|
|
169
|
+
<!-- safe-area-ok: below lg, an edge drawer that pads the top, bottom and inline-start insets
|
|
170
|
+
itself. The start side is physical left in LTR and right in RTL, hence the `rtl:` override. -->
|
|
168
171
|
<aside
|
|
169
172
|
ref="aside"
|
|
170
173
|
data-testid="sidebar"
|
|
@@ -173,7 +176,7 @@ watch(
|
|
|
173
176
|
:aria-modal="drawerOpen ? 'true' : undefined"
|
|
174
177
|
:aria-label="isCompact ? t('nav.menu') : undefined"
|
|
175
178
|
:inert="isCompact && !ui.mobileNavOpen"
|
|
176
|
-
class="fixed inset-y-0 start-0 z-40 flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-e border-default bg-default/95 px-3 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] backdrop-blur transition-transform duration-200 focus:outline-none lg:static lg:z-auto lg:translate-x-0 lg:bg-default/80"
|
|
179
|
+
class="fixed inset-y-0 start-0 z-40 flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-e border-default bg-default/95 px-3 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] max-lg:ps-[calc(0.75rem+env(safe-area-inset-left))] max-lg:pt-[calc(0.75rem+env(safe-area-inset-top))] max-lg:rtl:ps-[calc(0.75rem+env(safe-area-inset-right))] backdrop-blur transition-transform duration-200 focus:outline-none lg:static lg:z-auto lg:translate-x-0 lg:bg-default/80"
|
|
177
180
|
:class="[
|
|
178
181
|
ui.mobileNavOpen
|
|
179
182
|
? 'translate-x-0'
|
|
@@ -10,6 +10,7 @@ import { computed, ref, watch } from 'vue'
|
|
|
10
10
|
import { useModalBehavior } from '@modular-vue/core'
|
|
11
11
|
import type { ArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
12
12
|
import IconButton from '~/components/common/IconButton.vue'
|
|
13
|
+
import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
|
|
13
14
|
|
|
14
15
|
interface LightboxItem {
|
|
15
16
|
artifactId: string
|
|
@@ -161,7 +162,8 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
161
162
|
v-if="open"
|
|
162
163
|
ref="dialogRef"
|
|
163
164
|
tabindex="-1"
|
|
164
|
-
class="
|
|
165
|
+
class="z-[60] flex-col bg-app-950/95 backdrop-blur-sm focus:outline-none"
|
|
166
|
+
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
165
167
|
role="dialog"
|
|
166
168
|
aria-modal="true"
|
|
167
169
|
data-testid="artifact-lightbox"
|
|
@@ -170,7 +172,6 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
170
172
|
? t('media.lightbox.ariaLabel', { label: current.label })
|
|
171
173
|
: t('media.lightbox.ariaLabelFallback')
|
|
172
174
|
"
|
|
173
|
-
@click.self="close"
|
|
174
175
|
>
|
|
175
176
|
<!-- Toolbar -->
|
|
176
177
|
<div class="flex items-center gap-3 border-b border-default/60 px-4 py-2.5">
|
|
@@ -31,6 +31,7 @@ import InputGateNotice from '~/components/inputGate/InputGateNotice.vue'
|
|
|
31
31
|
import RunDetailLoadState from '~/components/panels/RunDetailLoadState.vue'
|
|
32
32
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
33
33
|
import IconButton from '~/components/common/IconButton.vue'
|
|
34
|
+
import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
|
|
34
35
|
|
|
35
36
|
// Detail overlay for a single pipeline step. Opened by clicking an agent in the
|
|
36
37
|
// inspector list (TaskExecution) or the focus-view pipeline (PipelineProgress) via
|
|
@@ -393,7 +394,8 @@ async function copyOutput() {
|
|
|
393
394
|
v-if="open && step && agent"
|
|
394
395
|
data-testid="step-detail"
|
|
395
396
|
:data-agent-kind="step?.agentKind"
|
|
396
|
-
class="
|
|
397
|
+
class="z-50 bg-app-950/96 backdrop-blur-sm"
|
|
398
|
+
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
397
399
|
role="dialog"
|
|
398
400
|
aria-modal="true"
|
|
399
401
|
>
|
|
@@ -959,7 +961,7 @@ async function copyOutput() {
|
|
|
959
961
|
<!-- edit-then-approve actions -->
|
|
960
962
|
<div
|
|
961
963
|
v-if="editing"
|
|
962
|
-
class="space-y-2 border-t border-default px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
964
|
+
class="space-y-2 border-t border-default px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] lg:pb-3"
|
|
963
965
|
>
|
|
964
966
|
<UButton
|
|
965
967
|
size="sm"
|
|
@@ -984,7 +986,7 @@ async function copyOutput() {
|
|
|
984
986
|
|
|
985
987
|
<div
|
|
986
988
|
v-else
|
|
987
|
-
class="space-y-2 border-t border-default px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
989
|
+
class="space-y-2 border-t border-default px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] lg:pb-3"
|
|
988
990
|
>
|
|
989
991
|
<UButton
|
|
990
992
|
data-testid="step-approve"
|
|
@@ -339,6 +339,7 @@ const showOriginalDescription = ref(false)
|
|
|
339
339
|
NOT `data-block-id`: that attribute is the board's card selector, which the canvas drivers
|
|
340
340
|
measure geometry through (`utils/blockRects.ts`), and a panel answering to it would offer
|
|
341
341
|
the arrows a rect that is not on the canvas at all. -->
|
|
342
|
+
<!-- safe-area-ok: a bottom sheet below lg; its scroller pads the bottom and side insets -->
|
|
342
343
|
<div
|
|
343
344
|
v-if="block && statusMeta && typeMeta"
|
|
344
345
|
data-testid="inspector-panel"
|
|
@@ -352,7 +353,7 @@ const showOriginalDescription = ref(false)
|
|
|
352
353
|
On compact viewports the panel is a bottom sheet capped to the visible
|
|
353
354
|
height (dvh excludes mobile browser chrome). -->
|
|
354
355
|
<div
|
|
355
|
-
class="max-h-[80dvh] space-y-4 overflow-y-auto overscroll-contain
|
|
356
|
+
class="max-h-[80dvh] space-y-4 overflow-y-auto overscroll-contain pt-4 pr-[max(1rem,env(safe-area-inset-right))] pb-[calc(1rem+env(safe-area-inset-bottom))] pl-[max(1rem,env(safe-area-inset-left))] lg:max-h-[calc(100vh-7rem)]"
|
|
356
357
|
>
|
|
357
358
|
<!-- header -->
|
|
358
359
|
<div class="flex items-start justify-between gap-2">
|
|
@@ -32,6 +32,7 @@ import RunFailureSummary from '~/components/observability/RunFailureSummary.vue'
|
|
|
32
32
|
import ToolCallList from '~/components/observability/ToolCallList.vue'
|
|
33
33
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
34
34
|
import IconButton from '~/components/common/IconButton.vue'
|
|
35
|
+
import { FULL_BLEED_TAKEOVER_CLASS } from '~/utils/safeArea'
|
|
35
36
|
|
|
36
37
|
/** No run selected: the same empty, NOT-truncated trajectory the store answers with. */
|
|
37
38
|
const EMPTY_TRAJECTORY: RunToolCallTrajectory = Object.freeze({
|
|
@@ -537,17 +538,18 @@ function exportJson() {
|
|
|
537
538
|
<Transition name="obs-fade">
|
|
538
539
|
<div
|
|
539
540
|
v-if="open"
|
|
540
|
-
class="
|
|
541
|
+
class="z-[60] flex-col bg-app-950/96 backdrop-blur-sm"
|
|
542
|
+
:class="FULL_BLEED_TAKEOVER_CLASS"
|
|
541
543
|
role="dialog"
|
|
542
544
|
aria-modal="true"
|
|
543
545
|
>
|
|
544
|
-
<header class="flex items-center gap-3 border-b border-default px-6 py-4">
|
|
546
|
+
<header class="flex flex-wrap items-center gap-3 border-b border-default px-6 py-4">
|
|
545
547
|
<div
|
|
546
548
|
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-app-info-500/15"
|
|
547
549
|
>
|
|
548
550
|
<UIcon name="i-lucide-activity" class="h-5 w-5 text-app-info-400" />
|
|
549
551
|
</div>
|
|
550
|
-
<div class="min-w-0">
|
|
552
|
+
<div class="min-w-0 flex-1">
|
|
551
553
|
<h1 class="truncate text-base font-semibold text-highlighted">
|
|
552
554
|
{{ t('observability.modelActivity') }}
|
|
553
555
|
</h1>
|
|
@@ -555,7 +557,11 @@ function exportJson() {
|
|
|
555
557
|
{{ block.title }} · {{ runSubtitle }}
|
|
556
558
|
</p>
|
|
557
559
|
</div>
|
|
558
|
-
|
|
560
|
+
<!-- Below lg the controls take their own row, so the close button stays on screen.
|
|
561
|
+
The row scrolls sideways when the tabs are wider than the phone. -->
|
|
562
|
+
<div
|
|
563
|
+
class="order-last flex w-full min-w-0 items-center gap-1.5 overflow-x-auto lg:order-none lg:w-auto"
|
|
564
|
+
>
|
|
559
565
|
<!-- Four VIEWS of the same run, which is what tabs mean. The trajectory is loaded
|
|
560
566
|
lazily the first time the tools view is opened, so the change is watched rather
|
|
561
567
|
than bound straight to `view`. -->
|
|
@@ -580,15 +586,16 @@ function exportJson() {
|
|
|
580
586
|
>
|
|
581
587
|
{{ t('observability.exportJson') }}
|
|
582
588
|
</UButton>
|
|
583
|
-
<IconButton
|
|
584
|
-
icon="i-lucide-x"
|
|
585
|
-
color="neutral"
|
|
586
|
-
variant="ghost"
|
|
587
|
-
size="sm"
|
|
588
|
-
:label="t('observability.closeEsc')"
|
|
589
|
-
@click="close"
|
|
590
|
-
/>
|
|
591
589
|
</div>
|
|
590
|
+
<IconButton
|
|
591
|
+
class="lg:-ms-1.5"
|
|
592
|
+
icon="i-lucide-x"
|
|
593
|
+
color="neutral"
|
|
594
|
+
variant="ghost"
|
|
595
|
+
size="sm"
|
|
596
|
+
:label="t('observability.closeEsc')"
|
|
597
|
+
@click="close"
|
|
598
|
+
/>
|
|
592
599
|
</header>
|
|
593
600
|
|
|
594
601
|
<div class="flex-1 overflow-auto px-6 py-6">
|