@cat-factory/app 0.116.2 → 0.116.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/assets/css/main.css +31 -0
- package/app/components/layout/SideBar.vue +1 -1
- package/app/components/panels/AgentStepDetail.vue +9 -3
- package/app/components/panels/InspectorPanel.vue +3 -1
- package/app/components/providers/PersonalSubscriptionSection.vue +40 -3
- 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/nuxt.config.ts +15 -1
- package/package.json +1 -1
package/app/assets/css/main.css
CHANGED
|
@@ -9,6 +9,37 @@
|
|
|
9
9
|
user-select: text;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/* Keep the bottom-anchored toaster clear of the home indicator / rounded corner
|
|
13
|
+
* on phones. The toaster viewport is bottom-anchored (Nuxt UI positions it at
|
|
14
|
+
* `bottom-4`), so we add the safe-area inset to that OFFSET rather than to the
|
|
15
|
+
* padding: each toast is `position: absolute; bottom: 0` inside the viewport, so
|
|
16
|
+
* padding-bottom does not lift it (an abs child anchored to `bottom-0` tracks the
|
|
17
|
+
* padding-box edge and ignores the padding). `env(safe-area-inset-bottom)` is 0
|
|
18
|
+
* until the viewport meta carries `viewport-fit=cover` (see nuxt.config.ts), so
|
|
19
|
+
* this collapses to the stock `1rem` on desktop. Assumes the default bottom
|
|
20
|
+
* placement (the app does not configure a top-anchored toaster). */
|
|
21
|
+
[data-slot='viewport'] {
|
|
22
|
+
bottom: calc(1rem + env(safe-area-inset-bottom));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Input font floor (P-3): iOS Safari auto-zooms the viewport whenever a focused
|
|
26
|
+
* field renders below 16px, which yanks the layout around on every tap into an
|
|
27
|
+
* input. Floor every editable control at 16px on coarse-pointer (touch) devices.
|
|
28
|
+
* This is the app-wide, fix-once guard: it reaches Nuxt UI's rendered
|
|
29
|
+
* `<input>`/`<textarea>` AND every hand-rolled raw field, and — unlike a
|
|
30
|
+
* component-default override — cannot be defeated by a per-instance `size="sm"`.
|
|
31
|
+
* `max(16px, 1em)` floors at 16px while leaving any deliberately larger field
|
|
32
|
+
* (1em resolves against the surrounding font size) untouched. Fine-pointer
|
|
33
|
+
* (desktop) keeps the denser typography. Checkbox/radio/range/color have no
|
|
34
|
+
* text baseline and never trigger the zoom, so they're excluded. */
|
|
35
|
+
@media (pointer: coarse) {
|
|
36
|
+
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']),
|
|
37
|
+
textarea,
|
|
38
|
+
select {
|
|
39
|
+
font-size: max(16px, 1em);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
/* ---- Agent Architecture Board: shared visual language ---------------------- */
|
|
13
44
|
|
|
14
45
|
:root {
|
|
@@ -139,7 +139,7 @@ watch(
|
|
|
139
139
|
:aria-modal="drawerOpen ? 'true' : undefined"
|
|
140
140
|
:aria-label="isCompact ? t('nav.menu') : undefined"
|
|
141
141
|
:inert="isCompact && !ui.mobileNavOpen"
|
|
142
|
-
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-slate-800 bg-slate-900/95
|
|
142
|
+
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-slate-800 bg-slate-900/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-slate-900/80"
|
|
143
143
|
:class="
|
|
144
144
|
ui.mobileNavOpen ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full lg:translate-x-0'
|
|
145
145
|
"
|
|
@@ -572,7 +572,7 @@ async function copyOutput() {
|
|
|
572
572
|
</p>
|
|
573
573
|
</div>
|
|
574
574
|
|
|
575
|
-
<div class="flex-1 space-y-3 overflow-auto px-4 py-3">
|
|
575
|
+
<div class="flex-1 space-y-3 overflow-auto overscroll-contain px-4 py-3">
|
|
576
576
|
<p
|
|
577
577
|
v-if="editing"
|
|
578
578
|
class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-[12px] leading-relaxed text-amber-200/90"
|
|
@@ -659,7 +659,10 @@ async function copyOutput() {
|
|
|
659
659
|
</div>
|
|
660
660
|
|
|
661
661
|
<!-- edit-then-approve actions -->
|
|
662
|
-
<div
|
|
662
|
+
<div
|
|
663
|
+
v-if="editing"
|
|
664
|
+
class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
665
|
+
>
|
|
663
666
|
<UButton
|
|
664
667
|
color="primary"
|
|
665
668
|
size="sm"
|
|
@@ -682,7 +685,10 @@ async function copyOutput() {
|
|
|
682
685
|
</UButton>
|
|
683
686
|
</div>
|
|
684
687
|
|
|
685
|
-
<div
|
|
688
|
+
<div
|
|
689
|
+
v-else
|
|
690
|
+
class="space-y-2 border-t border-slate-800 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))]"
|
|
691
|
+
>
|
|
686
692
|
<UButton
|
|
687
693
|
color="primary"
|
|
688
694
|
data-testid="step-approve"
|
|
@@ -269,7 +269,9 @@ const showOriginalDescription = ref(false)
|
|
|
269
269
|
(Run / Focus / Delete) stay reachable. The status bar above stays put.
|
|
270
270
|
On compact viewports the panel is a bottom sheet capped to the visible
|
|
271
271
|
height (dvh excludes mobile browser chrome). -->
|
|
272
|
-
<div
|
|
272
|
+
<div
|
|
273
|
+
class="max-h-[80dvh] space-y-4 overflow-y-auto overscroll-contain px-4 pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))] lg:max-h-[calc(100vh-5rem)]"
|
|
274
|
+
>
|
|
273
275
|
<!-- header -->
|
|
274
276
|
<div class="flex items-start justify-between gap-2">
|
|
275
277
|
<div class="flex items-center gap-2">
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// rather than pooled on the workspace. Each token is double-encrypted server-side under a
|
|
5
5
|
// personal PASSWORD (never stored); that password is what you'll enter when you start/retry
|
|
6
6
|
// such a run (cached locally so it's usually transparent). Recurring schedules can't use them.
|
|
7
|
-
import { computed, onMounted, ref } from 'vue'
|
|
7
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
8
8
|
import type { SubscriptionVendor } from '~/types/domain'
|
|
9
9
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
10
10
|
|
|
@@ -87,7 +87,33 @@ const password = ref('')
|
|
|
87
87
|
const expiresOn = ref('') // yyyy-mm-dd (optional)
|
|
88
88
|
const busy = ref(false)
|
|
89
89
|
|
|
90
|
+
// A transient "credentials stored" confirmation shown inline right after a successful save.
|
|
91
|
+
// Emptying the form on success recomputes `disabledReason` back to "enter a token", so without
|
|
92
|
+
// this the user is greeted by a red validation error immediately after they succeeded — which
|
|
93
|
+
// reads as a failure. While this notice is set we suppress `disabledReason` and show it instead,
|
|
94
|
+
// then clear it after a few seconds (or the moment the user starts entering a new credential).
|
|
95
|
+
const savedNotice = ref<string | null>(null)
|
|
96
|
+
let savedTimer: ReturnType<typeof setTimeout> | undefined
|
|
97
|
+
|
|
98
|
+
function clearSavedNotice() {
|
|
99
|
+
savedNotice.value = null
|
|
100
|
+
if (savedTimer) {
|
|
101
|
+
clearTimeout(savedTimer)
|
|
102
|
+
savedTimer = undefined
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Once the user touches the form again the success notice is stale — drop it so `disabledReason`
|
|
107
|
+
// guides the next entry as usual. Guard on non-empty input so the programmatic clear performed by
|
|
108
|
+
// a successful `connect()` (which resets the fields to empty) doesn't immediately wipe the notice
|
|
109
|
+
// we just set; switching vendor always clears it.
|
|
110
|
+
watch([token, password], ([tok, pwd]) => {
|
|
111
|
+
if (savedNotice.value && (tok.trim() || pwd)) clearSavedNotice()
|
|
112
|
+
})
|
|
113
|
+
watch(vendor, () => clearSavedNotice())
|
|
114
|
+
|
|
90
115
|
onMounted(() => void personal.load())
|
|
116
|
+
onBeforeUnmount(() => clearSavedNotice())
|
|
91
117
|
|
|
92
118
|
const selectedMeta = computed(() => vendorMeta(vendor.value) ?? PERSONAL_VENDORS.value[0]!)
|
|
93
119
|
const existing = computed(() => personal.subscriptions.find((s) => s.vendor === vendor.value))
|
|
@@ -118,6 +144,7 @@ const renewals = computed(() =>
|
|
|
118
144
|
|
|
119
145
|
async function connect() {
|
|
120
146
|
if (!token.value.trim() || password.value.length < 6) return
|
|
147
|
+
const vendorName = selectedMeta.value.label
|
|
121
148
|
busy.value = true
|
|
122
149
|
try {
|
|
123
150
|
await personal.store({
|
|
@@ -133,13 +160,19 @@ async function connect() {
|
|
|
133
160
|
password.value = ''
|
|
134
161
|
label.value = ''
|
|
135
162
|
expiresOn.value = ''
|
|
163
|
+
// Confirm success inline (and transiently) so emptying the form doesn't surface the
|
|
164
|
+
// `disabledReason` validation text as if the save had failed. It clears after a few
|
|
165
|
+
// seconds, or as soon as the user starts entering another credential.
|
|
166
|
+
savedNotice.value = t('personalSubscriptions.saved', { vendor: vendorName })
|
|
167
|
+
if (savedTimer) clearTimeout(savedTimer)
|
|
168
|
+
savedTimer = setTimeout(clearSavedNotice, 5000)
|
|
136
169
|
// A connected subscription makes its vendor's models usable, so refresh the catalog:
|
|
137
170
|
// this clears the "No AI model configured" banner and, if the default preset still
|
|
138
171
|
// points at models this subscription doesn't cover, reactively surfaces the
|
|
139
172
|
// preset-mismatch prompt (with its "pick a different preset" link).
|
|
140
173
|
if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
|
|
141
174
|
toast.add({
|
|
142
|
-
title: t('personalSubscriptions.toast.connected', { vendor:
|
|
175
|
+
title: t('personalSubscriptions.toast.connected', { vendor: vendorName }),
|
|
143
176
|
icon: 'i-lucide-check',
|
|
144
177
|
color: 'success',
|
|
145
178
|
})
|
|
@@ -266,7 +299,11 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
266
299
|
</UFormField>
|
|
267
300
|
</div>
|
|
268
301
|
<div class="flex items-center justify-end gap-3">
|
|
269
|
-
<p v-if="
|
|
302
|
+
<p v-if="savedNotice" class="flex items-center gap-1.5 text-sm text-emerald-400">
|
|
303
|
+
<UIcon name="i-lucide-check" class="size-4" />
|
|
304
|
+
{{ savedNotice }}
|
|
305
|
+
</p>
|
|
306
|
+
<p v-else-if="disabledReason" class="text-sm text-rose-400">{{ disabledReason }}</p>
|
|
270
307
|
<UButton
|
|
271
308
|
:loading="busy"
|
|
272
309
|
:disabled="disabledReason !== null"
|
package/i18n/locales/de.json
CHANGED
|
@@ -2569,6 +2569,7 @@
|
|
|
2569
2569
|
"token": "Geben Sie Ihr Token ein, um fortzufahren",
|
|
2570
2570
|
"password": "Geben Sie ein persönliches Passwort mit mindestens 6 Zeichen ein"
|
|
2571
2571
|
},
|
|
2572
|
+
"saved": "{vendor}-Zugangsdaten sicher gespeichert",
|
|
2572
2573
|
"expires": "Läuft ab am {date}",
|
|
2573
2574
|
"noExpiry": "Kein Ablaufdatum gesetzt",
|
|
2574
2575
|
"renewal": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -538,6 +538,7 @@
|
|
|
538
538
|
"token": "Enter your token to continue",
|
|
539
539
|
"password": "Enter a personal password of at least 6 characters"
|
|
540
540
|
},
|
|
541
|
+
"saved": "{vendor} credentials stored securely",
|
|
541
542
|
"expires": "Expires {date}",
|
|
542
543
|
"noExpiry": "No expiry set",
|
|
543
544
|
"renewal": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
"token": "Introduce tu token para continuar",
|
|
488
488
|
"password": "Introduce una contraseña personal de al menos 6 caracteres"
|
|
489
489
|
},
|
|
490
|
+
"saved": "Credenciales de {vendor} guardadas de forma segura",
|
|
490
491
|
"expires": "Caduca el {date}",
|
|
491
492
|
"noExpiry": "Sin fecha de caducidad",
|
|
492
493
|
"renewal": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
"token": "Saisissez votre jeton pour continuer",
|
|
488
488
|
"password": "Saisissez un mot de passe personnel d'au moins 6 caractères"
|
|
489
489
|
},
|
|
490
|
+
"saved": "Identifiants {vendor} enregistrés en toute sécurité",
|
|
490
491
|
"expires": "Expire le {date}",
|
|
491
492
|
"noExpiry": "Aucune date d'expiration",
|
|
492
493
|
"renewal": {
|
package/i18n/locales/he.json
CHANGED
package/i18n/locales/it.json
CHANGED
|
@@ -2569,6 +2569,7 @@
|
|
|
2569
2569
|
"token": "Inserisci il tuo token per continuare",
|
|
2570
2570
|
"password": "Inserisci una password personale di almeno 6 caratteri"
|
|
2571
2571
|
},
|
|
2572
|
+
"saved": "Credenziali {vendor} salvate in modo sicuro",
|
|
2572
2573
|
"expires": "Scade il {date}",
|
|
2573
2574
|
"noExpiry": "Nessuna scadenza impostata",
|
|
2574
2575
|
"renewal": {
|
package/i18n/locales/ja.json
CHANGED
package/i18n/locales/pl.json
CHANGED
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
"token": "Wprowadź token, aby kontynuować",
|
|
488
488
|
"password": "Wprowadź hasło osobiste o długości co najmniej 6 znaków"
|
|
489
489
|
},
|
|
490
|
+
"saved": "Poświadczenia {vendor} zapisane bezpiecznie",
|
|
490
491
|
"expires": "Wygasa {date}",
|
|
491
492
|
"noExpiry": "Brak daty wygaśnięcia",
|
|
492
493
|
"renewal": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
"token": "Devam etmek için token'ınızı girin",
|
|
488
488
|
"password": "En az 6 karakterlik kişisel bir parola girin"
|
|
489
489
|
},
|
|
490
|
+
"saved": "{vendor} kimlik bilgileri güvenli şekilde kaydedildi",
|
|
490
491
|
"expires": "Son kullanma {date}",
|
|
491
492
|
"noExpiry": "Son kullanma tarihi ayarlanmadı",
|
|
492
493
|
"renewal": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -487,6 +487,7 @@
|
|
|
487
487
|
"token": "Введіть свій токен, щоб продовжити",
|
|
488
488
|
"password": "Введіть особистий пароль щонайменше з 6 символів"
|
|
489
489
|
},
|
|
490
|
+
"saved": "Облікові дані {vendor} надійно збережено",
|
|
490
491
|
"expires": "Завершується {date}",
|
|
491
492
|
"noExpiry": "Дата завершення не вказана",
|
|
492
493
|
"renewal": {
|
package/nuxt.config.ts
CHANGED
|
@@ -115,7 +115,21 @@ export default defineNuxtConfig({
|
|
|
115
115
|
app: {
|
|
116
116
|
head: {
|
|
117
117
|
title: 'Agent Architecture Board',
|
|
118
|
-
meta: [
|
|
118
|
+
meta: [
|
|
119
|
+
// `viewport-fit=cover` is what makes `env(safe-area-inset-*)` resolve to
|
|
120
|
+
// non-zero on notched/rounded phones — the safe-area padding on the
|
|
121
|
+
// bottom-anchored surfaces (inspector sheet, drawer, toaster) is inert
|
|
122
|
+
// without it, so the two ship together.
|
|
123
|
+
{
|
|
124
|
+
name: 'viewport',
|
|
125
|
+
content: 'width=device-width, initial-scale=1, viewport-fit=cover',
|
|
126
|
+
},
|
|
127
|
+
// Tint the mobile browser chrome / iOS Safari address bar to the board
|
|
128
|
+
// surface so the app doesn't sit under a mismatched white bar. Matches
|
|
129
|
+
// `--board-bg`. (Home-screen installability — manifest, standalone status
|
|
130
|
+
// bar, touch icons — is deferred to the initiative's E2/A5-icons follow-up.)
|
|
131
|
+
{ name: 'theme-color', content: '#0b1020' },
|
|
132
|
+
],
|
|
119
133
|
},
|
|
120
134
|
},
|
|
121
135
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.116.
|
|
3
|
+
"version": "0.116.4",
|
|
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",
|