@cat-factory/app 0.47.6 → 0.47.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.
@@ -4,6 +4,7 @@
4
4
  // • invalid pipelines (unknown agent kind / bad shape) — DELETE a custom one, RESEED a built-in;
5
5
  // • outdated built-ins (a newer catalog definition is available) — RESEED to adopt it.
6
6
  // Detection is client-side (see usePipelineHealth); the actions hit the pipelines store.
7
+ const { t } = useI18n()
7
8
  const ui = useUiStore()
8
9
  const pipelines = usePipelinesStore()
9
10
  const { invalid, outdated, hasIssues } = usePipelineHealth()
@@ -39,9 +40,10 @@ async function run(id: string, action: () => Promise<unknown>, failTitle: string
39
40
  }
40
41
  }
41
42
 
42
- const reseed = (id: string) => run(id, () => pipelines.reseed(id), 'Could not reseed pipeline')
43
+ const reseed = (id: string) =>
44
+ run(id, () => pipelines.reseed(id), t('pipeline.health.toast.reseedFailed'))
43
45
  const remove = (id: string) =>
44
- run(id, () => pipelines.removePipeline(id), 'Could not delete pipeline')
46
+ run(id, () => pipelines.removePipeline(id), t('pipeline.health.toast.deleteFailed'))
45
47
 
46
48
  /** Reseed every reseedable pipeline (outdated built-ins + invalid built-ins) in one go. */
47
49
  async function reseedAll() {
@@ -61,11 +63,11 @@ const reseedableCount = computed(
61
63
  </script>
62
64
 
63
65
  <template>
64
- <UModal v-model:open="open" title="Pipeline health" :ui="{ content: 'max-w-2xl' }">
66
+ <UModal v-model:open="open" :title="t('pipeline.health.title')" :ui="{ content: 'max-w-2xl' }">
65
67
  <template #body>
66
68
  <div v-if="!hasIssues" class="py-6 text-center text-sm text-slate-400">
67
69
  <UIcon name="i-lucide-check-circle-2" class="mx-auto mb-2 h-8 w-8 text-emerald-400" />
68
- All pipelines are valid and up to date.
70
+ {{ t('pipeline.health.allValid') }}
69
71
  </div>
70
72
 
71
73
  <div v-else class="space-y-5">
@@ -73,11 +75,12 @@ const reseedableCount = computed(
73
75
  <section v-if="invalid.length" class="space-y-2">
74
76
  <div class="flex items-center gap-2">
75
77
  <UIcon name="i-lucide-triangle-alert" class="h-4 w-4 text-rose-400" />
76
- <h3 class="text-sm font-semibold text-slate-200">Invalid pipelines</h3>
78
+ <h3 class="text-sm font-semibold text-slate-200">
79
+ {{ t('pipeline.health.invalidHeading') }}
80
+ </h3>
77
81
  </div>
78
82
  <p class="text-[11px] text-slate-500">
79
- These reference a missing agent or are misconfigured, so they would fail (or misrun) at
80
- start. Delete a custom pipeline, or reseed a built-in to restore its catalog definition.
83
+ {{ t('pipeline.health.invalidDescription') }}
81
84
  </p>
82
85
  <ul class="space-y-2">
83
86
  <li
@@ -92,7 +95,7 @@ const reseedableCount = computed(
92
95
  {{ h.pipeline.name }}
93
96
  </span>
94
97
  <UBadge v-if="h.pipeline.builtin" color="neutral" variant="subtle" size="xs">
95
- built-in
98
+ {{ t('pipeline.health.builtinBadge') }}
96
99
  </UBadge>
97
100
  </div>
98
101
  <ul class="mt-1 space-y-0.5">
@@ -116,7 +119,7 @@ const reseedableCount = computed(
116
119
  :disabled="anyBusy"
117
120
  @click="reseed(h.pipeline.id)"
118
121
  >
119
- Reseed
122
+ {{ t('pipeline.health.reseed') }}
120
123
  </UButton>
121
124
  <UButton
122
125
  v-else
@@ -128,7 +131,7 @@ const reseedableCount = computed(
128
131
  :disabled="anyBusy"
129
132
  @click="remove(h.pipeline.id)"
130
133
  >
131
- Delete
134
+ {{ t('pipeline.health.delete') }}
132
135
  </UButton>
133
136
  </div>
134
137
  </li>
@@ -139,11 +142,12 @@ const reseedableCount = computed(
139
142
  <section v-if="outdated.length" class="space-y-2">
140
143
  <div class="flex items-center gap-2">
141
144
  <UIcon name="i-lucide-arrow-up-circle" class="h-4 w-4 text-amber-400" />
142
- <h3 class="text-sm font-semibold text-slate-200">Updates available</h3>
145
+ <h3 class="text-sm font-semibold text-slate-200">
146
+ {{ t('pipeline.health.updatesHeading') }}
147
+ </h3>
143
148
  </div>
144
149
  <p class="text-[11px] text-slate-500">
145
- A newer version of these built-in pipelines has shipped. Reseed to adopt it (your labels
146
- and archive state are kept).
150
+ {{ t('pipeline.health.updatesDescription') }}
147
151
  </p>
148
152
  <ul class="space-y-2">
149
153
  <li
@@ -166,7 +170,7 @@ const reseedableCount = computed(
166
170
  :disabled="anyBusy"
167
171
  @click="reseed(h.pipeline.id)"
168
172
  >
169
- Reseed
173
+ {{ t('pipeline.health.reseed') }}
170
174
  </UButton>
171
175
  </li>
172
176
  </ul>
@@ -184,7 +188,7 @@ const reseedableCount = computed(
184
188
  :loading="anyBusy"
185
189
  @click="reseedAll"
186
190
  >
187
- Reseed all ({{ reseedableCount }})
191
+ {{ t('pipeline.health.reseedAll', { count: reseedableCount }) }}
188
192
  </UButton>
189
193
  <span v-else />
190
194
  <UButton
@@ -193,7 +197,7 @@ const reseedableCount = computed(
193
197
  :disabled="anyBusy"
194
198
  @click="ui.closePipelineHealth()"
195
199
  >
196
- {{ hasIssues ? 'Dismiss' : 'Done' }}
200
+ {{ hasIssues ? t('pipeline.health.dismiss') : t('pipeline.health.done') }}
197
201
  </UButton>
198
202
  </div>
199
203
  </template>
@@ -22,6 +22,7 @@ const models = useModelsStore()
22
22
  const ui = useUiStore()
23
23
  const execution = useExecutionStore()
24
24
  const reviews = useReviewStage()
25
+ const { t } = useI18n()
25
26
 
26
27
  // While an iterative reviewer gate (requirements-review / clarity-review) folds the
27
28
  // answers / re-reviews in the background it needs NO human, so its parked approval is
@@ -30,11 +31,11 @@ function reviewStageLabel(agentKind: string | undefined): string | null {
30
31
  if (!reviews.isBackground(agentKind, props.instance.blockId)) return null
31
32
  const stage = reviews.stageForBlock(props.instance.blockId)
32
33
  return stage === 'incorporating'
33
- ? 'Incorporating…'
34
+ ? t('pipeline.progress.stage.incorporating')
34
35
  : stage === 'reviewing'
35
- ? 'Re-reviewing'
36
+ ? t('pipeline.progress.stage.reviewing')
36
37
  : stage === 'recommending'
37
- ? 'Recommending…'
38
+ ? t('pipeline.progress.stage.recommending')
38
39
  : null
39
40
  }
40
41
 
@@ -51,9 +52,11 @@ function followUpPending(step: PipelineStep): number {
51
52
  }
52
53
  function followUpLabel(step: PipelineStep): string {
53
54
  const items = step.followUps?.items ?? []
54
- if (items.length === 0) return 'Watching…'
55
+ if (items.length === 0) return t('pipeline.progress.followUp.watching')
55
56
  const pending = followUpPending(step)
56
- return pending > 0 ? `${pending} to decide` : 'All decided'
57
+ return pending > 0
58
+ ? t('pipeline.progress.followUp.toDecide', { count: pending })
59
+ : t('pipeline.progress.followUp.allDecided')
57
60
  }
58
61
 
59
62
  // --- restart from a step -----------------------------------------------------
@@ -79,21 +82,41 @@ async function restartFromHere(i: number) {
79
82
  }
80
83
 
81
84
  /** Visual language for an individual agent's runtime state. */
82
- const STATE_META: Record<AgentState, { label: string; color: string; icon: string }> = {
83
- pending: { label: 'Pending', color: '#64748b', icon: 'i-lucide-circle-dashed' },
84
- working: { label: 'Working', color: '#6366f1', icon: 'i-lucide-loader' },
85
- waiting_decision: { label: 'Needs decision', color: '#f59e0b', icon: 'i-lucide-circle-help' },
86
- done: { label: 'Done', color: '#22c55e', icon: 'i-lucide-circle-check' },
87
- }
85
+ const STATE_META = computed<Record<AgentState, { label: string; color: string; icon: string }>>(
86
+ () => ({
87
+ pending: {
88
+ label: t('pipeline.progress.state.pending'),
89
+ color: '#64748b',
90
+ icon: 'i-lucide-circle-dashed',
91
+ },
92
+ working: {
93
+ label: t('pipeline.progress.state.working'),
94
+ color: '#6366f1',
95
+ icon: 'i-lucide-loader',
96
+ },
97
+ waiting_decision: {
98
+ label: t('pipeline.progress.state.waiting_decision'),
99
+ color: '#f59e0b',
100
+ icon: 'i-lucide-circle-help',
101
+ },
102
+ done: {
103
+ label: t('pipeline.progress.state.done'),
104
+ color: '#22c55e',
105
+ icon: 'i-lucide-circle-check',
106
+ },
107
+ }),
108
+ )
88
109
 
89
110
  /** Visual language for the pipeline instance as a whole. */
90
- const STATUS_META: Record<ExecutionInstance['status'], { label: string; chip: string }> = {
91
- running: { label: 'Running', chip: 'primary' },
92
- blocked: { label: 'Needs you', chip: 'warning' },
93
- paused: { label: 'Paused (budget)', chip: 'neutral' },
94
- done: { label: 'Completed', chip: 'success' },
95
- failed: { label: 'Failed', chip: 'error' },
96
- }
111
+ const STATUS_META = computed<Record<ExecutionInstance['status'], { label: string; chip: string }>>(
112
+ () => ({
113
+ running: { label: t('pipeline.progress.status.running'), chip: 'primary' },
114
+ blocked: { label: t('pipeline.progress.status.blocked'), chip: 'warning' },
115
+ paused: { label: t('pipeline.progress.status.paused'), chip: 'neutral' },
116
+ done: { label: t('pipeline.progress.status.done'), chip: 'success' },
117
+ failed: { label: t('pipeline.progress.status.failed'), chip: 'error' },
118
+ }),
119
+ )
97
120
 
98
121
  const steps = computed(() => props.instance.steps)
99
122
  const total = computed(() => steps.value.length)
@@ -116,7 +139,7 @@ function liveWorking(state: AgentState) {
116
139
  * failed reads as "Failed" with a red cross, not a frozen "Working" loader.
117
140
  */
118
141
  function stepVisual(state: AgentState) {
119
- return isFailedStep(state, runFailed.value) ? FAILED_STEP_META : STATE_META[state]
142
+ return isFailedStep(state, runFailed.value) ? FAILED_STEP_META : STATE_META.value[state]
120
143
  }
121
144
 
122
145
  /** A step counts as fully complete only once its state is `done`. */
@@ -135,7 +158,7 @@ const overallProgress = computed(() => {
135
158
  })
136
159
  const overallPct = computed(() => Math.round(overallProgress.value * 100))
137
160
 
138
- const statusMeta = computed(() => STATUS_META[props.instance.status])
161
+ const statusMeta = computed(() => STATUS_META.value[props.instance.status])
139
162
 
140
163
  /** The agent the pipeline is currently centred on (for the summary line). */
141
164
  const currentAgent = computed(() => {
@@ -171,13 +194,19 @@ const ITEM_ICON: Record<string, string> = {
171
194
  <div class="flex flex-wrap items-center gap-3">
172
195
  <UBadge :color="statusMeta.chip as any" variant="subtle">{{ statusMeta.label }}</UBadge>
173
196
  <span class="text-sm text-slate-300">
174
- <span class="font-semibold text-white">{{ completedCount }}</span>
175
- / {{ total }} agents complete
197
+ <i18n-t keypath="pipeline.progress.agentsComplete" tag="span" scope="global">
198
+ <template #completed>
199
+ <span class="font-semibold text-white">{{ completedCount }}</span>
200
+ </template>
201
+ <template #total>{{ total }}</template>
202
+ </i18n-t>
176
203
  </span>
177
204
  <span v-if="currentAgent && instance.status === 'running'" class="text-xs text-slate-500">
178
- · currently {{ currentAgent }}
205
+ · {{ t('pipeline.progress.currently', { agent: currentAgent }) }}
179
206
  </span>
180
- <span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{ overallPct }}%</span>
207
+ <span class="ml-auto font-mono text-sm tabular-nums text-slate-200">{{
208
+ t('pipeline.progress.percent', { value: overallPct })
209
+ }}</span>
181
210
  </div>
182
211
  <UProgress :model-value="overallPct" class="mt-3" />
183
212
 
@@ -233,7 +262,11 @@ const ITEM_ICON: Record<string, string> = {
233
262
  >
234
263
  <div
235
264
  class="group flex cursor-pointer items-center gap-2"
236
- :title="s.output ? 'View details & read output' : 'View step details'"
265
+ :title="
266
+ s.output
267
+ ? t('pipeline.progress.viewDetailsOutput')
268
+ : t('pipeline.progress.viewDetails')
269
+ "
237
270
  @click="openStep(i)"
238
271
  >
239
272
  <div
@@ -254,13 +287,13 @@ const ITEM_ICON: Record<string, string> = {
254
287
  <span
255
288
  v-if="isCompanionKind(s.agentKind)"
256
289
  class="shrink-0 rounded bg-slate-700/60 px-1 text-[9px] font-medium uppercase tracking-wide text-slate-300"
257
- title="Companion of a producer step"
290
+ :title="t('pipeline.progress.companionTooltip')"
258
291
  >
259
- Companion
292
+ {{ t('pipeline.progress.companion') }}
260
293
  </span>
261
294
  </div>
262
295
  <div class="text-[10px] uppercase tracking-wide text-slate-500">
263
- Step {{ i + 1 }} of {{ total }}
296
+ {{ t('pipeline.progress.stepOf', { current: i + 1, total }) }}
264
297
  </div>
265
298
  </div>
266
299
  <span
@@ -281,7 +314,7 @@ const ITEM_ICON: Record<string, string> = {
281
314
  variant="ghost"
282
315
  size="xs"
283
316
  class="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
284
- title="Restart pipeline from this step"
317
+ :title="t('pipeline.progress.restartTooltip')"
285
318
  @click.stop="restartArmed = i"
286
319
  />
287
320
  <template v-else>
@@ -294,7 +327,7 @@ const ITEM_ICON: Record<string, string> = {
294
327
  class="shrink-0"
295
328
  @click.stop="restartFromHere(i)"
296
329
  >
297
- Restart from here
330
+ {{ t('pipeline.progress.restartFromHere') }}
298
331
  </UButton>
299
332
  <UButton
300
333
  color="neutral"
@@ -304,7 +337,7 @@ const ITEM_ICON: Record<string, string> = {
304
337
  :disabled="restarting === i"
305
338
  @click.stop="restartArmed = null"
306
339
  >
307
- Cancel
340
+ {{ t('common.cancel') }}
308
341
  </UButton>
309
342
  </template>
310
343
  </template>
@@ -330,16 +363,21 @@ const ITEM_ICON: Record<string, string> = {
330
363
  class="mt-2 flex items-center gap-1.5 text-[11px] text-sky-300"
331
364
  >
332
365
  <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 shrink-0 animate-spin" />
333
- <span>Spinning up container…</span>
366
+ <span>{{ t('pipeline.progress.spinningUpContainer') }}</span>
334
367
  </div>
335
368
 
336
369
  <!-- live subtask counts from the agent's todo list -->
337
370
  <div v-if="s.subtasks && s.subtasks.total > 0" class="mt-2">
338
371
  <div class="flex items-center justify-between text-[10px] text-slate-400">
339
372
  <span>
340
- {{ s.subtasks.completed }}/{{ s.subtasks.total }} subtasks
373
+ {{
374
+ t('pipeline.progress.subtasks', {
375
+ completed: s.subtasks.completed,
376
+ total: s.subtasks.total,
377
+ })
378
+ }}
341
379
  <span v-if="s.subtasks.inProgress > 0" class="text-indigo-300">
342
- · {{ s.subtasks.inProgress }} in progress
380
+ {{ t('pipeline.progress.subtasksInProgress', { count: s.subtasks.inProgress }) }}
343
381
  </span>
344
382
  </span>
345
383
  </div>
@@ -399,7 +437,7 @@ const ITEM_ICON: Record<string, string> = {
399
437
  all step metadata) lives in the step-detail overlay opened by click. -->
400
438
  <p v-if="s.output" class="mt-2 flex items-center gap-1 text-[11px] text-slate-500">
401
439
  <UIcon name="i-lucide-book-open-text" class="h-3 w-3 shrink-0" />
402
- Click to read this agent’s output
440
+ {{ t('pipeline.progress.clickToRead') }}
403
441
  </p>
404
442
 
405
443
  <!-- Conditionally-run companion (today the Tester's fixer): a distinct
@@ -423,7 +461,7 @@ const ITEM_ICON: Record<string, string> = {
423
461
  </span>
424
462
  <span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
425
463
  {{ agentKindMeta(companionByStep[i]!.kind).label }}
426
- <span class="text-slate-500">(companion)</span>
464
+ <span class="text-slate-500">{{ t('pipeline.progress.companionSuffix') }}</span>
427
465
  </span>
428
466
  <span
429
467
  class="shrink-0 text-[11px] font-medium"
@@ -454,7 +492,7 @@ const ITEM_ICON: Record<string, string> = {
454
492
  </span>
455
493
  <span class="min-w-0 flex-1 truncate text-[12px] text-slate-300">
456
494
  {{ FOLLOW_UP_COMPANION_META.label }}
457
- <span class="text-slate-500">(companion)</span>
495
+ <span class="text-slate-500">{{ t('pipeline.progress.companionSuffix') }}</span>
458
496
  </span>
459
497
  <span
460
498
  class="shrink-0 text-[11px] font-medium"
@@ -483,7 +521,9 @@ const ITEM_ICON: Record<string, string> = {
483
521
  icon="i-lucide-shield-check"
484
522
  @click="emit('openApproval', s.approval.id)"
485
523
  >
486
- Review &amp; approve {{ agentKindMeta(s.agentKind).label }}'s proposal
524
+ {{
525
+ t('pipeline.progress.reviewApprove', { agent: agentKindMeta(s.agentKind).label })
526
+ }}
487
527
  </UButton>
488
528
  </div>
489
529
 
@@ -496,7 +536,7 @@ const ITEM_ICON: Record<string, string> = {
496
536
  icon="i-lucide-circle-help"
497
537
  @click="emit('openDecision', s.decision.id)"
498
538
  >
499
- Resolve: {{ s.decision.question }}
539
+ {{ t('pipeline.progress.resolve', { question: s.decision.question }) }}
500
540
  </UButton>
501
541
  </div>
502
542
  <p
@@ -31,6 +31,14 @@ export const useAuthStore = defineStore(
31
31
  * setup banner). Null on every other facade.
32
32
  */
33
33
  const localMode = ref<LocalModeConfig | null>(null)
34
+ /**
35
+ * Local mode only: the source-control provider the user last chose to sign in with
36
+ * (its PAT lives server-side in env — this is just the non-secret choice). Persisted, so
37
+ * on a later load we silently re-mint a session from that env PAT without showing the
38
+ * login screen. Set on an explicit sign-in, cleared on logout / 401 (so logout actually
39
+ * signs out — no auto re-login loop).
40
+ */
41
+ const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
34
42
  /** True once the initial auth handshake has settled. */
35
43
  const ready = ref(false)
36
44
 
@@ -83,6 +91,25 @@ export const useAuthStore = defineStore(
83
91
  }
84
92
  if (!user.value) token.value = null
85
93
  }
94
+
95
+ // Local mode: if no live session resolved but the user previously signed in with a
96
+ // configured env PAT, silently re-mint a session from it — so an expired/rotated token
97
+ // never forces the login screen again. The token itself stays server-side; we only
98
+ // remembered the provider choice. Guard on the provider STILL being configured (PAT could
99
+ // have been removed) and clear the choice on failure so we fall back to the login screen
100
+ // instead of looping.
101
+ if (
102
+ localMode.value?.enabled === true &&
103
+ user.value === null &&
104
+ autoLoginProvider.value &&
105
+ localMode.value.patLogin?.configured.includes(autoLoginProvider.value)
106
+ ) {
107
+ try {
108
+ await patLogin({ provider: autoLoginProvider.value })
109
+ } catch {
110
+ autoLoginProvider.value = null
111
+ }
112
+ }
86
113
  // An already-signed-in user who followed an invite link redeems it here (a
87
114
  // brand-new user redeems it server-side during signup/OAuth instead).
88
115
  if (user.value) await maybeAcceptInvite()
@@ -153,6 +180,8 @@ export const useAuthStore = defineStore(
153
180
  */
154
181
  async function patLogin(body: { provider: 'github' | 'gitlab'; token?: string }) {
155
182
  applySession(await api.patLogin(body))
183
+ // Remember the choice so a later load re-mints the session from the env PAT silently.
184
+ autoLoginProvider.value = body.provider
156
185
  }
157
186
 
158
187
  /** Request a password-reset link by email (always resolves; never reveals existence). */
@@ -170,9 +199,18 @@ export const useAuthStore = defineStore(
170
199
  api.logout().catch(() => {})
171
200
  token.value = null
172
201
  user.value = null
202
+ // Forget the remembered provider so logout sticks (otherwise bootstrap would
203
+ // immediately re-mint a session from the env PAT).
204
+ autoLoginProvider.value = null
173
205
  }
174
206
 
175
- /** Called by the API client when a request comes back 401. */
207
+ /**
208
+ * Called by the API client when a request comes back 401. Drops the dead session but KEEPS
209
+ * the remembered provider (unlike logout): a 401 from an expired/rotated token or a
210
+ * transient blip should let the next load silently re-mint from the env PAT, not force the
211
+ * login screen. The guarded re-mint in `bootstrap` clears the choice itself if it genuinely
212
+ * fails (PAT removed/revoked), so there's no re-login loop.
213
+ */
176
214
  function handleUnauthorized() {
177
215
  token.value = null
178
216
  user.value = null
@@ -184,6 +222,7 @@ export const useAuthStore = defineStore(
184
222
  required,
185
223
  providers,
186
224
  localMode,
225
+ autoLoginProvider,
187
226
  ready,
188
227
  isAuthenticated,
189
228
  needsLogin,
@@ -199,5 +238,5 @@ export const useAuthStore = defineStore(
199
238
  handleUnauthorized,
200
239
  }
201
240
  },
202
- { persist: { pick: ['token'] } },
241
+ { persist: { pick: ['token', 'autoLoginProvider'] } },
203
242
  )