@cat-factory/app 0.46.11 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/auth/AuthGate.vue +3 -2
- package/app/components/auth/LoginScreen.vue +172 -32
- package/app/components/auth/ResetPasswordScreen.vue +16 -13
- package/app/components/auth/UserMenu.vue +3 -2
- package/app/components/layout/AccountDeploymentSettings.vue +307 -37
- package/app/components/layout/AccountFragmentSettings.vue +2 -3
- package/app/components/layout/AccountTeamSettings.vue +75 -45
- package/app/components/layout/AiProvidersBanner.vue +11 -11
- package/app/components/layout/BoardSwitcher.vue +63 -29
- package/app/components/layout/CommandBar.vue +68 -54
- package/app/components/layout/GitHubPatBanner.vue +13 -6
- package/app/components/layout/IntegrationBackTitle.vue +4 -1
- package/app/components/layout/IntegrationsHub.vue +67 -49
- package/app/components/layout/NotificationsInbox.vue +48 -19
- package/app/components/layout/PersonalSetupModal.vue +26 -16
- package/app/components/layout/ProviderConfigBanner.vue +14 -9
- package/app/components/layout/SideBar.vue +20 -18
- package/app/components/layout/SpendWarningBanner.vue +16 -16
- package/app/components/providers/ApiKeysSection.vue +27 -3
- package/app/components/providers/PersonalSubscriptionSection.vue +18 -4
- package/app/components/providers/SignInRequiredNotice.vue +19 -0
- package/app/components/settings/AccountSettingsPanel.vue +9 -6
- package/app/components/settings/IssueTrackerPanel.vue +114 -53
- package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
- package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
- package/app/components/settings/MergeThresholdsPanel.vue +93 -45
- package/app/components/settings/ModelConfigurationPanel.vue +62 -36
- package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
- package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
- package/app/components/settings/ProviderConnectionPanel.vue +115 -61
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
- package/app/components/settings/UserSecretsSection.vue +37 -18
- package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
- package/app/composables/api/auth.ts +7 -0
- package/app/stores/auth.ts +25 -1
- package/app/types/accountSettings.ts +4 -0
- package/i18n/locales/en.json +908 -0
- package/i18n/locales/es.json +899 -0
- package/i18n/locales/fr.json +899 -0
- package/i18n/locales/pl.json +899 -0
- package/i18n/locales/uk.json +899 -0
- package/package.json +2 -2
|
@@ -7,44 +7,72 @@ import type { Notification } from '~/types/domain'
|
|
|
7
7
|
// (merge / confirm / retry) or dismissed. Hydrated from the snapshot and patched
|
|
8
8
|
// live via the `notification` WorkspaceEvent.
|
|
9
9
|
|
|
10
|
+
const { t, te } = useI18n()
|
|
11
|
+
|
|
10
12
|
const notifications = useNotificationsStore()
|
|
11
13
|
const ui = useUiStore()
|
|
12
14
|
const execution = useExecutionStore()
|
|
13
15
|
|
|
14
16
|
const busy = ref<string | null>(null)
|
|
15
17
|
|
|
16
|
-
/** Per-type display metadata (icon, colour
|
|
18
|
+
/** Per-type display metadata (icon, colour). The primary-action label is resolved
|
|
19
|
+
* separately through the i18n catalog (`ACTION_KEYS`). */
|
|
17
20
|
type Accent = 'warning' | 'primary' | 'error'
|
|
18
|
-
const META: Record<Notification['type'], { icon: string; color: Accent
|
|
19
|
-
merge_review: { icon: 'i-lucide-git-pull-request-arrow', color: 'warning'
|
|
20
|
-
pipeline_complete: { icon: 'i-lucide-circle-check', color: 'primary'
|
|
21
|
-
ci_failed: { icon: 'i-lucide-triangle-alert', color: 'error'
|
|
22
|
-
test_failed: { icon: 'i-lucide-flask-conical', color: 'error'
|
|
21
|
+
const META: Record<Notification['type'], { icon: string; color: Accent }> = {
|
|
22
|
+
merge_review: { icon: 'i-lucide-git-pull-request-arrow', color: 'warning' },
|
|
23
|
+
pipeline_complete: { icon: 'i-lucide-circle-check', color: 'primary' },
|
|
24
|
+
ci_failed: { icon: 'i-lucide-triangle-alert', color: 'error' },
|
|
25
|
+
test_failed: { icon: 'i-lucide-flask-conical', color: 'error' },
|
|
23
26
|
// Clicking the title opens the review window for the task (see `reveal`); "act" just marks
|
|
24
27
|
// it read (the server performs no side-effect for this type).
|
|
25
|
-
requirement_review: { icon: 'i-lucide-clipboard-list', color: 'primary'
|
|
28
|
+
requirement_review: { icon: 'i-lucide-clipboard-list', color: 'primary' },
|
|
26
29
|
// Clicking the title opens the clarity review window for the task (see `reveal`); "act"
|
|
27
30
|
// just marks it read (the server performs no side-effect for this type).
|
|
28
|
-
clarity_review: { icon: 'i-lucide-bug', color: 'primary'
|
|
31
|
+
clarity_review: { icon: 'i-lucide-bug', color: 'primary' },
|
|
29
32
|
// A post-release Datadog regression the on-call agent investigated. The human decides
|
|
30
33
|
// whether to revert (in GitHub via the PR link) or acknowledge; "act" marks it handled.
|
|
31
|
-
release_regression: { icon: 'i-lucide-activity', color: 'error'
|
|
34
|
+
release_regression: { icon: 'i-lucide-activity', color: 'error' },
|
|
32
35
|
// Clicking the title opens the parked step's decision surface (companion → step detail
|
|
33
36
|
// with the iteration-cap prompt; requirements → the review window); "act" just marks it
|
|
34
37
|
// read (the decision itself is resolved in that surface, not here).
|
|
35
|
-
decision_required: { icon: 'i-lucide-circle-help', color: 'warning'
|
|
38
|
+
decision_required: { icon: 'i-lucide-circle-help', color: 'warning' },
|
|
36
39
|
// Clicking the title opens the human-testing window for the task (see `reveal`); "act" just
|
|
37
40
|
// marks it read (the gate is resolved in that window — confirm / request a fix — not here).
|
|
38
|
-
human_test_ready: { icon: 'i-lucide-user-check', color: 'primary'
|
|
41
|
+
human_test_ready: { icon: 'i-lucide-user-check', color: 'primary' },
|
|
39
42
|
// Clicking the title opens the visual-confirmation window for the task (see `reveal`); "act"
|
|
40
43
|
// just marks it read (the gate is resolved in that window — approve / request a fix — not here).
|
|
41
|
-
visual_confirmation_ready: { icon: 'i-lucide-camera', color: 'primary'
|
|
44
|
+
visual_confirmation_ready: { icon: 'i-lucide-camera', color: 'primary' },
|
|
42
45
|
// Clicking the title opens the task's gate window (where the human can request a freeform
|
|
43
46
|
// fix); "act" just marks it read (approval happens on GitHub, not here).
|
|
44
|
-
human_review: { icon: 'i-lucide-users', color: 'primary'
|
|
47
|
+
human_review: { icon: 'i-lucide-users', color: 'primary' },
|
|
45
48
|
// Clicking the title opens the Follow-up companion window for the run (see `reveal`); "act"
|
|
46
49
|
// just marks it read (items are decided in that window — file / send back / answer — not here).
|
|
47
|
-
followup_pending: { icon: 'i-lucide-compass', color: 'warning'
|
|
50
|
+
followup_pending: { icon: 'i-lucide-compass', color: 'warning' },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Per-type primary-action label. An exhaustive Record keyed off the notification
|
|
54
|
+
// type union (a missing member fails the typecheck); each value is a LITERAL catalog
|
|
55
|
+
// key so the typed-message-keys check sees it. Leaf keys mirror the enum value verbatim.
|
|
56
|
+
const ACTION_KEYS: Record<Notification['type'], string> = {
|
|
57
|
+
merge_review: 'layout.notifications.action.merge_review',
|
|
58
|
+
pipeline_complete: 'layout.notifications.action.pipeline_complete',
|
|
59
|
+
ci_failed: 'layout.notifications.action.ci_failed',
|
|
60
|
+
test_failed: 'layout.notifications.action.test_failed',
|
|
61
|
+
requirement_review: 'layout.notifications.action.requirement_review',
|
|
62
|
+
clarity_review: 'layout.notifications.action.clarity_review',
|
|
63
|
+
release_regression: 'layout.notifications.action.release_regression',
|
|
64
|
+
decision_required: 'layout.notifications.action.decision_required',
|
|
65
|
+
human_test_ready: 'layout.notifications.action.human_test_ready',
|
|
66
|
+
visual_confirmation_ready: 'layout.notifications.action.visual_confirmation_ready',
|
|
67
|
+
human_review: 'layout.notifications.action.human_review',
|
|
68
|
+
followup_pending: 'layout.notifications.action.followup_pending',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The localized primary-action label for a notification (te()-guarded against a
|
|
72
|
+
* locale that omits the key, falling back to the generic "mark read" verb). */
|
|
73
|
+
function actionLabel(n: Notification): string {
|
|
74
|
+
const key = ACTION_KEYS[n.type]
|
|
75
|
+
return te(key) ? t(key) : t('layout.notifications.action.markRead')
|
|
48
76
|
}
|
|
49
77
|
|
|
50
78
|
/** A notification the escalation sweep has flagged as overdue (waited past the threshold). */
|
|
@@ -176,7 +204,7 @@ function revealDecision(n: Notification) {
|
|
|
176
204
|
<template #content>
|
|
177
205
|
<div class="max-h-[28rem] w-[min(24rem,92vw)] overflow-y-auto p-2">
|
|
178
206
|
<div class="px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
179
|
-
|
|
207
|
+
{{ t('layout.notifications.heading') }}
|
|
180
208
|
</div>
|
|
181
209
|
<div
|
|
182
210
|
v-for="n in notifications.open"
|
|
@@ -208,7 +236,7 @@ function revealDecision(n: Notification) {
|
|
|
208
236
|
v-if="isUrgent(n)"
|
|
209
237
|
class="shrink-0 rounded bg-error-500/20 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-error-400"
|
|
210
238
|
>
|
|
211
|
-
|
|
239
|
+
{{ t('layout.notifications.overdue') }}
|
|
212
240
|
</span>
|
|
213
241
|
</div>
|
|
214
242
|
<p class="mt-0.5 text-[11px] leading-snug text-slate-400">{{ n.body }}</p>
|
|
@@ -219,7 +247,8 @@ function revealDecision(n: Notification) {
|
|
|
219
247
|
rel="noopener"
|
|
220
248
|
class="mt-1 inline-flex items-center gap-1 text-[11px] text-sky-400 hover:underline"
|
|
221
249
|
>
|
|
222
|
-
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
250
|
+
<UIcon name="i-lucide-external-link" class="h-3 w-3" />
|
|
251
|
+
{{ t('layout.notifications.openPr') }}
|
|
223
252
|
</a>
|
|
224
253
|
<div class="mt-2 flex items-center gap-1.5">
|
|
225
254
|
<UButton
|
|
@@ -230,7 +259,7 @@ function revealDecision(n: Notification) {
|
|
|
230
259
|
:loading="busy === n.id"
|
|
231
260
|
@click="act(n)"
|
|
232
261
|
>
|
|
233
|
-
{{
|
|
262
|
+
{{ actionLabel(n) }}
|
|
234
263
|
</UButton>
|
|
235
264
|
<UButton
|
|
236
265
|
data-testid="notification-dismiss"
|
|
@@ -240,7 +269,7 @@ function revealDecision(n: Notification) {
|
|
|
240
269
|
:disabled="busy === n.id"
|
|
241
270
|
@click="dismiss(n)"
|
|
242
271
|
>
|
|
243
|
-
|
|
272
|
+
{{ t('layout.notifications.dismiss') }}
|
|
244
273
|
</UButton>
|
|
245
274
|
</div>
|
|
246
275
|
</div>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// previously sat, confusingly, among the workspace-wide integrations. Each row reuses the
|
|
6
6
|
// existing per-panel handlers via `ui.openFromPersonal(...)`, so opening one closes this
|
|
7
7
|
// hub, reveals that panel, and gives it a "Back to My setup" control (IntegrationBackTitle).
|
|
8
|
+
const { t } = useI18n()
|
|
8
9
|
const ui = useUiStore()
|
|
9
10
|
const userSecrets = useUserSecretsStore()
|
|
10
11
|
const localModels = useLocalModelsStore()
|
|
@@ -54,14 +55,14 @@ const groups = computed<PersonalGroup[]>(() => {
|
|
|
54
55
|
// --- Source control --------------------------------------------------------
|
|
55
56
|
const pat = !!userSecrets.statusFor('github_pat')
|
|
56
57
|
out.push({
|
|
57
|
-
title: '
|
|
58
|
+
title: t('layout.personalSetup.sourceControl.title'),
|
|
58
59
|
items: [
|
|
59
60
|
{
|
|
60
61
|
key: 'github-pat',
|
|
61
62
|
icon: 'i-lucide-key-round',
|
|
62
|
-
label: '
|
|
63
|
-
description: '
|
|
64
|
-
status: pat ? '
|
|
63
|
+
label: t('layout.personalSetup.githubToken.label'),
|
|
64
|
+
description: t('layout.personalSetup.githubToken.description'),
|
|
65
|
+
status: pat ? t('layout.personalSetup.connected') : undefined,
|
|
65
66
|
connected: pat,
|
|
66
67
|
onClick: () => go(ui.openUserSecrets),
|
|
67
68
|
},
|
|
@@ -72,24 +73,27 @@ const groups = computed<PersonalGroup[]>(() => {
|
|
|
72
73
|
const runnerCount = localModels.endpoints.length
|
|
73
74
|
const subCount = personalSubs.subscriptions.length
|
|
74
75
|
out.push({
|
|
75
|
-
title: '
|
|
76
|
+
title: t('layout.personalSetup.models.title'),
|
|
76
77
|
items: [
|
|
77
78
|
{
|
|
78
79
|
key: 'local-runners',
|
|
79
80
|
icon: 'i-lucide-server',
|
|
80
|
-
label: '
|
|
81
|
-
description: '
|
|
82
|
-
status: runnerCount
|
|
81
|
+
label: t('layout.personalSetup.localRunners.label'),
|
|
82
|
+
description: t('layout.personalSetup.localRunners.description'),
|
|
83
|
+
status: runnerCount
|
|
84
|
+
? t('layout.personalSetup.connectedCount', { count: runnerCount }, runnerCount)
|
|
85
|
+
: undefined,
|
|
83
86
|
connected: runnerCount > 0,
|
|
84
87
|
onClick: () => go(ui.openLocalModels),
|
|
85
88
|
},
|
|
86
89
|
{
|
|
87
90
|
key: 'personal-subs',
|
|
88
91
|
icon: 'i-lucide-user',
|
|
89
|
-
label: '
|
|
90
|
-
description:
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
label: t('layout.personalSetup.subscriptions.label'),
|
|
93
|
+
description: t('layout.personalSetup.subscriptions.description'),
|
|
94
|
+
status: subCount
|
|
95
|
+
? t('layout.personalSetup.connectedCount', { count: subCount }, subCount)
|
|
96
|
+
: undefined,
|
|
93
97
|
connected: subCount > 0,
|
|
94
98
|
onClick: () => go(() => ui.openVendorCredentials('personal')),
|
|
95
99
|
},
|
|
@@ -101,11 +105,15 @@ const groups = computed<PersonalGroup[]>(() => {
|
|
|
101
105
|
</script>
|
|
102
106
|
|
|
103
107
|
<template>
|
|
104
|
-
<UModal
|
|
108
|
+
<UModal
|
|
109
|
+
v-model:open="open"
|
|
110
|
+
:title="t('layout.personalSetup.title')"
|
|
111
|
+
:ui="{ content: 'max-w-xl' }"
|
|
112
|
+
>
|
|
105
113
|
<template #body>
|
|
106
114
|
<div class="space-y-5">
|
|
107
115
|
<p class="text-xs text-slate-400">
|
|
108
|
-
|
|
116
|
+
{{ t('layout.personalSetup.intro') }}
|
|
109
117
|
</p>
|
|
110
118
|
|
|
111
119
|
<section v-for="group in groups" :key="group.title">
|
|
@@ -125,9 +133,11 @@ const groups = computed<PersonalGroup[]>(() => {
|
|
|
125
133
|
<div class="flex items-center gap-2">
|
|
126
134
|
<span class="truncate text-sm font-medium text-slate-100">{{ item.label }}</span>
|
|
127
135
|
<UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
|
|
128
|
-
{{ item.status || '
|
|
136
|
+
{{ item.status || t('layout.personalSetup.connected') }}
|
|
129
137
|
</UBadge>
|
|
130
|
-
<span v-else class="text-[11px] text-slate-500">
|
|
138
|
+
<span v-else class="text-[11px] text-slate-500">
|
|
139
|
+
{{ t('layout.personalSetup.notConnected') }}
|
|
140
|
+
</span>
|
|
131
141
|
</div>
|
|
132
142
|
<p class="truncate text-xs text-slate-400">{{ item.description }}</p>
|
|
133
143
|
</div>
|
|
@@ -7,13 +7,19 @@
|
|
|
7
7
|
import { computed, ref, watch } from 'vue'
|
|
8
8
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
9
9
|
|
|
10
|
+
const { t } = useI18n()
|
|
10
11
|
const ui = useUiStore()
|
|
11
12
|
const workspace = useWorkspaceStore()
|
|
12
13
|
const store = useProviderConnectionsStore()
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Exhaustive per-kind title/action keys (leaf names mirror the kind verbatim).
|
|
16
|
+
const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
|
|
17
|
+
environment: 'layout.providerConfigBanner.titleOne.environment',
|
|
18
|
+
'runner-pool': 'layout.providerConfigBanner.titleOne.runner-pool',
|
|
19
|
+
}
|
|
20
|
+
const ACTION_KEYS: Record<ProviderConnectionKind, string> = {
|
|
21
|
+
environment: 'layout.providerConfigBanner.action.environment',
|
|
22
|
+
'runner-pool': 'layout.providerConfigBanner.action.runner-pool',
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
// Probe both providers once the workspace is ready (the descriptor view is secret-less).
|
|
@@ -44,8 +50,8 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
|
|
|
44
50
|
<h2 class="text-lg font-semibold text-amber-100">
|
|
45
51
|
{{
|
|
46
52
|
pending.length > 1
|
|
47
|
-
? '
|
|
48
|
-
:
|
|
53
|
+
? t('layout.providerConfigBanner.titleMany')
|
|
54
|
+
: t(TITLE_KEYS[pending[0]!])
|
|
49
55
|
}}
|
|
50
56
|
</h2>
|
|
51
57
|
<UButton
|
|
@@ -53,13 +59,12 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
|
|
|
53
59
|
variant="ghost"
|
|
54
60
|
size="xs"
|
|
55
61
|
icon="i-lucide-x"
|
|
56
|
-
aria-label="
|
|
62
|
+
:aria-label="t('common.close')"
|
|
57
63
|
@click="dismissed = true"
|
|
58
64
|
/>
|
|
59
65
|
</div>
|
|
60
66
|
<p class="mt-1 text-sm text-amber-200/90">
|
|
61
|
-
|
|
62
|
-
run yet. Add the mandatory fields to finish connecting it.
|
|
67
|
+
{{ t('layout.providerConfigBanner.body') }}
|
|
63
68
|
</p>
|
|
64
69
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
65
70
|
<UButton
|
|
@@ -70,7 +75,7 @@ const show = computed(() => pending.value.length > 0 && !dismissed.value)
|
|
|
70
75
|
icon="i-lucide-settings"
|
|
71
76
|
@click="ui.openProviderConnection(k)"
|
|
72
77
|
>
|
|
73
|
-
|
|
78
|
+
{{ t(ACTION_KEYS[k]) }}
|
|
74
79
|
</UButton>
|
|
75
80
|
</div>
|
|
76
81
|
</div>
|
|
@@ -11,6 +11,8 @@ import LanguageSwitcher from '~/components/layout/LanguageSwitcher.vue'
|
|
|
11
11
|
import UserMenu from '~/components/auth/UserMenu.vue'
|
|
12
12
|
import { useViewport } from '~/composables/useViewport'
|
|
13
13
|
|
|
14
|
+
const { t } = useI18n()
|
|
15
|
+
|
|
14
16
|
const documents = useDocumentsStore()
|
|
15
17
|
const tasks = useTasksStore()
|
|
16
18
|
const github = useGitHubStore()
|
|
@@ -107,7 +109,7 @@ watch(
|
|
|
107
109
|
data-testid="sidebar-backdrop"
|
|
108
110
|
role="button"
|
|
109
111
|
tabindex="-1"
|
|
110
|
-
:aria-label="
|
|
112
|
+
:aria-label="t('common.close')"
|
|
111
113
|
@click="ui.closeMobileNav()"
|
|
112
114
|
/>
|
|
113
115
|
</Transition>
|
|
@@ -118,7 +120,7 @@ watch(
|
|
|
118
120
|
tabindex="-1"
|
|
119
121
|
:role="drawerOpen ? 'dialog' : undefined"
|
|
120
122
|
:aria-modal="drawerOpen ? 'true' : undefined"
|
|
121
|
-
:aria-label="isCompact ?
|
|
123
|
+
:aria-label="isCompact ? t('nav.menu') : undefined"
|
|
122
124
|
:inert="isCompact && !ui.mobileNavOpen"
|
|
123
125
|
class="fixed inset-y-0 left-0 z-40 flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto border-r border-slate-800 bg-slate-900/95 p-3 backdrop-blur transition-transform duration-200 focus:outline-none lg:static lg:z-auto lg:translate-x-0 lg:bg-slate-900/80"
|
|
124
126
|
:class="ui.mobileNavOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
|
|
@@ -134,13 +136,13 @@ watch(
|
|
|
134
136
|
@click="ui.openCommandBar()"
|
|
135
137
|
>
|
|
136
138
|
<UIcon name="i-lucide-search" class="h-4 w-4 shrink-0" />
|
|
137
|
-
<span class="flex-1 truncate">{{
|
|
139
|
+
<span class="flex-1 truncate">{{ t('nav.commandBar') }}</span>
|
|
138
140
|
<UKbd value="⌘K" />
|
|
139
141
|
</button>
|
|
140
142
|
|
|
141
143
|
<section>
|
|
142
144
|
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
143
|
-
{{
|
|
145
|
+
{{ t('nav.create') }}
|
|
144
146
|
</h2>
|
|
145
147
|
<div class="space-y-1.5">
|
|
146
148
|
<UButton
|
|
@@ -152,7 +154,7 @@ watch(
|
|
|
152
154
|
class="justify-start"
|
|
153
155
|
@click="ui.openBuilder()"
|
|
154
156
|
>
|
|
155
|
-
{{
|
|
157
|
+
{{ t('nav.buildPipeline') }}
|
|
156
158
|
</UButton>
|
|
157
159
|
</div>
|
|
158
160
|
</section>
|
|
@@ -161,7 +163,7 @@ watch(
|
|
|
161
163
|
|
|
162
164
|
<section>
|
|
163
165
|
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
164
|
-
{{
|
|
166
|
+
{{ t('nav.repositories') }}
|
|
165
167
|
</h2>
|
|
166
168
|
<div class="space-y-1.5">
|
|
167
169
|
<UButton
|
|
@@ -174,7 +176,7 @@ watch(
|
|
|
174
176
|
class="justify-start"
|
|
175
177
|
@click="ui.openAddService()"
|
|
176
178
|
>
|
|
177
|
-
{{
|
|
179
|
+
{{ t('nav.addFromRepo') }}
|
|
178
180
|
</UButton>
|
|
179
181
|
<UButton
|
|
180
182
|
block
|
|
@@ -185,7 +187,7 @@ watch(
|
|
|
185
187
|
class="justify-start"
|
|
186
188
|
@click="ui.openBootstrap()"
|
|
187
189
|
>
|
|
188
|
-
{{
|
|
190
|
+
{{ t('nav.bootstrapRepo') }}
|
|
189
191
|
</UButton>
|
|
190
192
|
</div>
|
|
191
193
|
</section>
|
|
@@ -194,7 +196,7 @@ watch(
|
|
|
194
196
|
|
|
195
197
|
<section>
|
|
196
198
|
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
197
|
-
{{
|
|
199
|
+
{{ t('nav.integrations') }}
|
|
198
200
|
</h2>
|
|
199
201
|
<div class="space-y-1.5">
|
|
200
202
|
<!-- Every external system the workspace can enable/link now lives behind
|
|
@@ -209,7 +211,7 @@ watch(
|
|
|
209
211
|
class="justify-start"
|
|
210
212
|
@click="ui.openIntegrations()"
|
|
211
213
|
>
|
|
212
|
-
{{
|
|
214
|
+
{{ t('nav.integrations') }}
|
|
213
215
|
</UButton>
|
|
214
216
|
<!-- The Sandbox: try prompt versions/models against graded fixtures, off to the
|
|
215
217
|
side of the board. Opens the on-demand testing window. -->
|
|
@@ -222,7 +224,7 @@ watch(
|
|
|
222
224
|
class="justify-start"
|
|
223
225
|
@click="ui.openSandbox()"
|
|
224
226
|
>
|
|
225
|
-
{{
|
|
227
|
+
{{ t('nav.sandbox') }}
|
|
226
228
|
</UButton>
|
|
227
229
|
<!-- The Kaizen screen: grading history + verified prompt/agent/model combos. -->
|
|
228
230
|
<UButton
|
|
@@ -234,7 +236,7 @@ watch(
|
|
|
234
236
|
class="justify-start"
|
|
235
237
|
@click="ui.openKaizen()"
|
|
236
238
|
>
|
|
237
|
-
{{
|
|
239
|
+
{{ t('nav.kaizen') }}
|
|
238
240
|
</UButton>
|
|
239
241
|
</div>
|
|
240
242
|
</section>
|
|
@@ -243,7 +245,7 @@ watch(
|
|
|
243
245
|
<USeparator />
|
|
244
246
|
<section>
|
|
245
247
|
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
246
|
-
{{
|
|
248
|
+
{{ t('nav.workspaceContext') }}
|
|
247
249
|
</h2>
|
|
248
250
|
<UButton
|
|
249
251
|
block
|
|
@@ -254,7 +256,7 @@ watch(
|
|
|
254
256
|
class="justify-start"
|
|
255
257
|
@click="ui.openFragmentLibrary()"
|
|
256
258
|
>
|
|
257
|
-
{{
|
|
259
|
+
{{ t('nav.contextFragments') }}
|
|
258
260
|
</UButton>
|
|
259
261
|
</section>
|
|
260
262
|
</template>
|
|
@@ -262,7 +264,7 @@ watch(
|
|
|
262
264
|
<USeparator />
|
|
263
265
|
<section>
|
|
264
266
|
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
265
|
-
{{
|
|
267
|
+
{{ t('nav.configuration') }}
|
|
266
268
|
</h2>
|
|
267
269
|
<div class="space-y-1.5">
|
|
268
270
|
<!-- Merge thresholds, issue writeback and default service best practices are
|
|
@@ -276,7 +278,7 @@ watch(
|
|
|
276
278
|
class="justify-start"
|
|
277
279
|
@click="ui.openWorkspaceSettings()"
|
|
278
280
|
>
|
|
279
|
-
{{
|
|
281
|
+
{{ t('nav.workspaceSettings') }}
|
|
280
282
|
</UButton>
|
|
281
283
|
<UButton
|
|
282
284
|
block
|
|
@@ -287,7 +289,7 @@ watch(
|
|
|
287
289
|
class="justify-start"
|
|
288
290
|
@click="ui.openModelConfig()"
|
|
289
291
|
>
|
|
290
|
-
{{
|
|
292
|
+
{{ t('nav.modelConfiguration') }}
|
|
291
293
|
</UButton>
|
|
292
294
|
<!-- Account & team: members + roles, invitations, email sender, account API keys.
|
|
293
295
|
Shown once accounts (auth) are enabled. -->
|
|
@@ -301,7 +303,7 @@ watch(
|
|
|
301
303
|
class="justify-start"
|
|
302
304
|
@click="ui.openAccountSettings()"
|
|
303
305
|
>
|
|
304
|
-
{{
|
|
306
|
+
{{ t('nav.accountSettings') }}
|
|
305
307
|
</UButton>
|
|
306
308
|
</div>
|
|
307
309
|
</section>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
|
|
4
|
+
const { t, n } = useI18n()
|
|
4
5
|
const workspace = useWorkspaceStore()
|
|
5
6
|
|
|
6
7
|
const spend = computed(() => workspace.spend)
|
|
@@ -8,18 +9,13 @@ const spend = computed(() => workspace.spend)
|
|
|
8
9
|
const exceeded = computed(() => spend.value?.exceeded ?? false)
|
|
9
10
|
|
|
10
11
|
function money(amount: number, currency: string) {
|
|
11
|
-
|
|
12
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
|
|
13
|
-
} catch {
|
|
14
|
-
// Fall back if the currency code isn't recognised by the runtime.
|
|
15
|
-
return `${amount.toFixed(2)} ${currency}`
|
|
16
|
-
}
|
|
12
|
+
return n(amount, { key: 'currency', currency })
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
const tokens = computed(() => {
|
|
20
16
|
const s = spend.value
|
|
21
17
|
if (!s) return ''
|
|
22
|
-
return
|
|
18
|
+
return n(s.inputTokens + s.outputTokens, 'decimal')
|
|
23
19
|
})
|
|
24
20
|
|
|
25
21
|
const resuming = ref(false)
|
|
@@ -47,29 +43,33 @@ async function resume() {
|
|
|
47
43
|
<UIcon name="i-lucide-octagon-alert" class="mt-0.5 h-10 w-10 shrink-0 text-red-400" />
|
|
48
44
|
<div class="min-w-0 flex-1">
|
|
49
45
|
<h2 class="text-lg font-semibold text-red-100">
|
|
50
|
-
|
|
46
|
+
{{ t('layout.spendWarningBanner.title') }}
|
|
51
47
|
</h2>
|
|
52
48
|
<p class="mt-1 text-sm text-red-200/90">
|
|
53
|
-
|
|
54
|
-
paused to avoid further cost. Execution resumes automatically when the budget rolls
|
|
55
|
-
over next month, or once the limit is raised.
|
|
49
|
+
{{ t('layout.spendWarningBanner.body') }}
|
|
56
50
|
</p>
|
|
57
51
|
|
|
58
52
|
<dl class="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
59
53
|
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
60
|
-
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
54
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
55
|
+
{{ t('layout.spendWarningBanner.spent') }}
|
|
56
|
+
</dt>
|
|
61
57
|
<dd class="text-base font-semibold tabular-nums text-red-50">
|
|
62
58
|
{{ money(spend.costSpent, spend.currency) }}
|
|
63
59
|
</dd>
|
|
64
60
|
</div>
|
|
65
61
|
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
66
|
-
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
62
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
63
|
+
{{ t('layout.spendWarningBanner.budget') }}
|
|
64
|
+
</dt>
|
|
67
65
|
<dd class="text-base font-semibold tabular-nums text-red-50">
|
|
68
66
|
{{ money(spend.costLimit, spend.currency) }}
|
|
69
67
|
</dd>
|
|
70
68
|
</div>
|
|
71
69
|
<div class="rounded-lg bg-red-900/50 px-3 py-2">
|
|
72
|
-
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
70
|
+
<dt class="text-[11px] uppercase tracking-wide text-red-300/80">
|
|
71
|
+
{{ t('layout.spendWarningBanner.tokens') }}
|
|
72
|
+
</dt>
|
|
73
73
|
<dd class="text-base font-semibold tabular-nums text-red-50">{{ tokens }}</dd>
|
|
74
74
|
</div>
|
|
75
75
|
</dl>
|
|
@@ -82,10 +82,10 @@ async function resume() {
|
|
|
82
82
|
:loading="resuming"
|
|
83
83
|
@click="resume"
|
|
84
84
|
>
|
|
85
|
-
|
|
85
|
+
{{ t('layout.spendWarningBanner.resume') }}
|
|
86
86
|
</UButton>
|
|
87
87
|
<span class="text-xs text-red-300/70">
|
|
88
|
-
|
|
88
|
+
{{ t('layout.spendWarningBanner.resumeHint') }}
|
|
89
89
|
</span>
|
|
90
90
|
</div>
|
|
91
91
|
</div>
|
|
@@ -24,11 +24,18 @@ const props = withDefaults(defineProps<{ accountId?: string; category?: 'direct'
|
|
|
24
24
|
const workspace = useWorkspaceStore()
|
|
25
25
|
const keys = useApiKeysStore()
|
|
26
26
|
const models = useModelsStore()
|
|
27
|
+
const auth = useAuthStore()
|
|
27
28
|
const toast = useToast()
|
|
29
|
+
const { t } = useI18n()
|
|
28
30
|
|
|
29
31
|
/** Account-wide mode (single account scope) vs the default workspace/user toggle. */
|
|
30
32
|
const isAccount = computed(() => !!props.accountId)
|
|
31
33
|
|
|
34
|
+
// "My keys" (user scope) are stored per-user, so they need a signed-in user. Block just
|
|
35
|
+
// that scope when there's none (a deployment without sign-in); workspace/account keys are
|
|
36
|
+
// unaffected. The scope toggle stays enabled so the user can switch back to a shared scope.
|
|
37
|
+
const needsSignIn = computed(() => !isAccount.value && scope.value === 'user' && !auth.user)
|
|
38
|
+
|
|
32
39
|
interface ProviderMeta {
|
|
33
40
|
value: ApiKeyProvider
|
|
34
41
|
label: string
|
|
@@ -257,11 +264,17 @@ async function remove(k: ApiKey) {
|
|
|
257
264
|
<USelect
|
|
258
265
|
v-model="provider"
|
|
259
266
|
:items="PROVIDERS.map((p) => ({ label: p.label, value: p.value }))"
|
|
267
|
+
:disabled="needsSignIn"
|
|
260
268
|
class="w-64"
|
|
261
269
|
/>
|
|
262
270
|
</UFormField>
|
|
263
271
|
</div>
|
|
264
272
|
|
|
273
|
+
<ProvidersSignInRequiredNotice
|
|
274
|
+
v-if="needsSignIn"
|
|
275
|
+
:message="t('auth.signInRequired.userApiKeys')"
|
|
276
|
+
/>
|
|
277
|
+
|
|
265
278
|
<!-- where to get the key -->
|
|
266
279
|
<ol
|
|
267
280
|
class="list-decimal space-y-1.5 rounded-lg border border-slate-700 bg-slate-900/60 p-4 pl-8 text-sm text-slate-300"
|
|
@@ -290,13 +303,24 @@ async function remove(k: ApiKey) {
|
|
|
290
303
|
<!-- add form -->
|
|
291
304
|
<div class="space-y-2">
|
|
292
305
|
<UFormField label="Label (optional)">
|
|
293
|
-
<UInput v-model="label" placeholder="e.g. team key" />
|
|
306
|
+
<UInput v-model="label" :disabled="needsSignIn" placeholder="e.g. team key" />
|
|
294
307
|
</UFormField>
|
|
295
308
|
<UFormField label="API key">
|
|
296
|
-
<UTextarea
|
|
309
|
+
<UTextarea
|
|
310
|
+
v-model="key"
|
|
311
|
+
:rows="2"
|
|
312
|
+
:disabled="needsSignIn"
|
|
313
|
+
placeholder="paste the API key"
|
|
314
|
+
class="font-mono"
|
|
315
|
+
/>
|
|
297
316
|
</UFormField>
|
|
298
317
|
<div class="flex justify-end">
|
|
299
|
-
<UButton
|
|
318
|
+
<UButton
|
|
319
|
+
:loading="busy"
|
|
320
|
+
:disabled="needsSignIn || !key.trim()"
|
|
321
|
+
icon="i-lucide-plus"
|
|
322
|
+
@click="add()"
|
|
323
|
+
>
|
|
300
324
|
Connect
|
|
301
325
|
</UButton>
|
|
302
326
|
</div>
|