@cat-factory/app 0.46.10 → 0.46.11

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.
Files changed (29) hide show
  1. package/app/components/observability/StepMetricsBar.vue +31 -11
  2. package/app/components/observability/StepModelActivity.vue +3 -2
  3. package/app/components/panels/AgentStepDetail.vue +52 -39
  4. package/app/components/panels/DecisionModal.vue +12 -5
  5. package/app/components/panels/GenericStructuredResultView.vue +16 -6
  6. package/app/components/panels/InspectorPanel.vue +35 -28
  7. package/app/components/panels/ObservabilityPanel.vue +92 -49
  8. package/app/components/panels/StepMetadataCard.vue +87 -30
  9. package/app/components/panels/StepRestartControl.vue +4 -3
  10. package/app/components/panels/StepRunMeta.vue +23 -10
  11. package/app/components/panels/StepTestReport.vue +14 -11
  12. package/app/components/panels/inspector/ContainerSummary.vue +25 -14
  13. package/app/components/panels/inspector/EpicChildren.vue +7 -4
  14. package/app/components/panels/inspector/RecurringScheduleSettings.vue +60 -19
  15. package/app/components/panels/inspector/ServiceFragments.vue +3 -2
  16. package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +18 -13
  17. package/app/components/panels/inspector/ServiceTestConfig.vue +50 -39
  18. package/app/components/panels/inspector/TaskAgentConfig.vue +6 -5
  19. package/app/components/panels/inspector/TaskDependencies.vue +9 -4
  20. package/app/components/panels/inspector/TaskEstimateBadge.vue +14 -13
  21. package/app/components/panels/inspector/TaskExecution.vue +65 -31
  22. package/app/components/panels/inspector/TaskRunSettings.vue +93 -56
  23. package/app/components/panels/inspector/TaskStructure.vue +5 -4
  24. package/i18n/locales/en.json +402 -0
  25. package/i18n/locales/es.json +402 -0
  26. package/i18n/locales/fr.json +402 -0
  27. package/i18n/locales/pl.json +402 -0
  28. package/i18n/locales/uk.json +402 -0
  29. package/package.json +1 -1
@@ -28,6 +28,7 @@ const agentRuns = useAgentRunsStore()
28
28
  const github = useGitHubStore()
29
29
  const recurring = useRecurringPipelinesStore()
30
30
  const requirements = useRequirementsStore()
31
+ const { t } = useI18n()
31
32
 
32
33
  // When the selected task block backs a recurring pipeline, the inspector shows the
33
34
  // schedule controls + history, and "Delete" removes the schedule (block + history).
@@ -58,20 +59,20 @@ const instance = computed(() => execution.getInstance(block.value?.executionId))
58
59
  const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
59
60
 
60
61
  // Containers show a derived activity status (never "done"); tasks use their own.
61
- const FRAME_LABEL: Record<BlockStatus, string> = {
62
- planned: 'No tasks',
63
- ready: 'Live',
64
- in_progress: 'Active',
65
- blocked: 'Needs attention',
66
- pr_ready: 'Active',
67
- done: 'Live',
62
+ const FRAME_LABEL_KEYS: Record<BlockStatus, string> = {
63
+ planned: 'panels.inspector.frameStatus.planned',
64
+ ready: 'panels.inspector.frameStatus.ready',
65
+ in_progress: 'panels.inspector.frameStatus.in_progress',
66
+ blocked: 'panels.inspector.frameStatus.blocked',
67
+ pr_ready: 'panels.inspector.frameStatus.pr_ready',
68
+ done: 'panels.inspector.frameStatus.done',
68
69
  }
69
70
  const effectiveStatus = computed<BlockStatus>(() =>
70
71
  isContainer.value ? board.frameStatus(block.value!.id) : block.value!.status,
71
72
  )
72
73
  const statusMeta = computed(() => (block.value ? STATUS_META[effectiveStatus.value] : null))
73
74
  const statusLabel = computed(() =>
74
- isContainer.value ? FRAME_LABEL[effectiveStatus.value] : statusMeta.value!.label,
75
+ isContainer.value ? t(FRAME_LABEL_KEYS[effectiveStatus.value]) : statusMeta.value!.label,
75
76
  )
76
77
 
77
78
  const runnable = computed(() => (block.value ? board.isRunnable(block.value.id) : false))
@@ -80,12 +81,12 @@ const runnable = computed(() => (block.value ? board.isRunnable(block.value.id)
80
81
  // reads as "Delete task" rather than ambiguously removing the whole service.
81
82
  const deleteLabel = computed(() =>
82
83
  schedule.value
83
- ? 'Delete recurring pipeline'
84
+ ? t('panels.inspector.deleteRecurringPipeline')
84
85
  : isTask.value
85
- ? 'Delete task'
86
+ ? t('panels.inspector.deleteTask')
86
87
  : level.value === 'module'
87
- ? 'Delete module'
88
- : 'Delete service',
88
+ ? t('panels.inspector.deleteModule')
89
+ : t('panels.inspector.deleteService'),
89
90
  )
90
91
 
91
92
  // A task is "started" once a pipeline has been launched on it (it has an
@@ -252,7 +253,7 @@ const showOriginalDescription = ref(false)
252
253
  v-model="block.title"
253
254
  size="sm"
254
255
  class="w-full"
255
- placeholder="Title…"
256
+ :placeholder="t('panels.inspector.titlePlaceholder')"
256
257
  @change="saveTitle"
257
258
  @blur="saveTitle"
258
259
  />
@@ -265,13 +266,15 @@ const showOriginalDescription = ref(false)
265
266
  class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
266
267
  >
267
268
  <UIcon name="i-lucide-file-check-2" class="h-3.5 w-3.5" />
268
- Reworked requirements
269
+ {{ t('panels.inspector.reworkedRequirements') }}
269
270
  </div>
270
271
  <p class="line-clamp-5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
271
272
  {{ reqReworkedText }}
272
273
  </p>
273
274
  <div class="mt-2 flex items-center justify-between gap-2">
274
- <p class="text-[11px] text-slate-500">Agent steps use this document.</p>
275
+ <p class="text-[11px] text-slate-500">
276
+ {{ t('panels.inspector.agentStepsUseDocument') }}
277
+ </p>
275
278
  <UButton
276
279
  color="neutral"
277
280
  variant="link"
@@ -279,7 +282,7 @@ const showOriginalDescription = ref(false)
279
282
  icon="i-lucide-maximize-2"
280
283
  @click="ui.openRequirementReview(block.id)"
281
284
  >
282
- Open
285
+ {{ t('panels.inspector.open') }}
283
286
  </UButton>
284
287
  </div>
285
288
  </div>
@@ -290,7 +293,7 @@ const showOriginalDescription = ref(false)
290
293
  :icon="showOriginalDescription ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
291
294
  @click="showOriginalDescription = !showOriginalDescription"
292
295
  >
293
- Original description (frozen)
296
+ {{ t('panels.inspector.originalDescriptionFrozen') }}
294
297
  </UButton>
295
298
  <UTextarea
296
299
  v-if="showOriginalDescription"
@@ -300,7 +303,7 @@ const showOriginalDescription = ref(false)
300
303
  size="sm"
301
304
  class="w-full"
302
305
  disabled
303
- placeholder="No description"
306
+ :placeholder="t('panels.inspector.noDescription')"
304
307
  />
305
308
  </template>
306
309
 
@@ -313,13 +316,13 @@ const showOriginalDescription = ref(false)
313
316
  size="sm"
314
317
  class="w-full"
315
318
  :disabled="!editable"
316
- placeholder="Describe this block…"
319
+ :placeholder="t('panels.inspector.describeBlock')"
317
320
  @change="saveDescription"
318
321
  @blur="saveDescription"
319
322
  />
320
323
  <p v-if="isTask && !editable" class="flex items-center gap-1 text-[11px] text-slate-500">
321
324
  <UIcon name="i-lucide-lock" class="h-3 w-3" />
322
- This task has started — its details are locked.
325
+ {{ t('panels.inspector.taskStartedLocked') }}
323
326
  </p>
324
327
 
325
328
  <!-- prior incorporated requirements kept as a base after a review-driven reset -->
@@ -328,13 +331,13 @@ const showOriginalDescription = ref(false)
328
331
  class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
329
332
  >
330
333
  <UIcon name="i-lucide-history" class="h-3.5 w-3.5" />
331
- Last incorporated requirements
334
+ {{ t('panels.inspector.lastIncorporatedRequirements') }}
332
335
  </div>
333
336
  <p class="line-clamp-5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
334
337
  {{ reqReworkedText }}
335
338
  </p>
336
339
  <p class="mt-2 text-[11px] text-slate-500">
337
- From the previous review. Use it as a base — edit the description above and resubmit.
340
+ {{ t('panels.inspector.priorDocHint') }}
338
341
  </p>
339
342
  </div>
340
343
  </template>
@@ -350,7 +353,7 @@ const showOriginalDescription = ref(false)
350
353
  >
351
354
  <span class="flex items-center gap-1.5 text-xs text-amber-300">
352
355
  <UIcon name="i-lucide-loader-circle" class="h-3.5 w-3.5 animate-spin" />
353
- Bootstrapping…
356
+ {{ t('panels.inspector.bootstrapping') }}
354
357
  </span>
355
358
  <AgentStopButton :run-id="runningRun.runId" :kind="runningRun.kind" size="xs" />
356
359
  </div>
@@ -391,7 +394,11 @@ const showOriginalDescription = ref(false)
391
394
  icon="i-lucide-ticket"
392
395
  @click="ui.openTaskImport()"
393
396
  >
394
- {{ tasks.anyOffered ? 'Import issue' : 'Connect a tracker' }}
397
+ {{
398
+ tasks.anyOffered
399
+ ? t('panels.inspector.importIssue')
400
+ : t('panels.inspector.connectTracker')
401
+ }}
395
402
  </UButton>
396
403
  <UButton
397
404
  v-if="isContainer && documents.available && documents.anyConnected"
@@ -401,7 +408,7 @@ const showOriginalDescription = ref(false)
401
408
  icon="i-lucide-wand-sparkles"
402
409
  @click="spawnFromDocument"
403
410
  >
404
- Spawn from document
411
+ {{ t('panels.inspector.spawnFromDocument') }}
405
412
  </UButton>
406
413
  </div>
407
414
 
@@ -422,7 +429,7 @@ const showOriginalDescription = ref(false)
422
429
  icon="i-lucide-scroll-text"
423
430
  @click="ui.openServiceSpec(block.id)"
424
431
  >
425
- View Requirements
432
+ {{ t('panels.inspector.viewRequirements') }}
426
433
  </UButton>
427
434
 
428
435
  <!-- service / module: tasks summary -->
@@ -461,7 +468,7 @@ const showOriginalDescription = ref(false)
461
468
  trailing-icon="i-lucide-chevron-down"
462
469
  :disabled="!runnable"
463
470
  >
464
- {{ instance ? 'Re-run' : 'Run' }}
471
+ {{ instance ? t('panels.inspector.reRun') : t('panels.inspector.run') }}
465
472
  </UButton>
466
473
  </UDropdownMenu>
467
474
  <UButton
@@ -472,7 +479,7 @@ const showOriginalDescription = ref(false)
472
479
  icon="i-lucide-maximize-2"
473
480
  @click="ui.focus(block.id)"
474
481
  >
475
- Focus
482
+ {{ t('panels.inspector.focus') }}
476
483
  </UButton>
477
484
  <UButton
478
485
  color="error"
@@ -15,6 +15,7 @@ const ui = useUiStore()
15
15
  const execution = useExecutionStore()
16
16
  const board = useBoardStore()
17
17
  const observability = useObservabilityStore()
18
+ const { t, d } = useI18n()
18
19
 
19
20
  const executionId = computed(() => ui.observabilityInstanceId)
20
21
  const open = computed(() => !!executionId.value)
@@ -110,7 +111,7 @@ function agentMeta(kind: string) {
110
111
  return agentKindMeta(kind)
111
112
  }
112
113
  function clock(ms: number): string {
113
- return new Date(ms).toLocaleTimeString()
114
+ return d(new Date(ms), 'long')
114
115
  }
115
116
  /** Pretty-print the prompt JSON; fall back to the raw string if it isn't JSON. */
116
117
  function prettyPrompt(raw: string): string {
@@ -150,7 +151,9 @@ function exportJson() {
150
151
  <UIcon name="i-lucide-activity" class="h-5 w-5 text-sky-400" />
151
152
  </div>
152
153
  <div class="min-w-0">
153
- <h1 class="truncate text-base font-semibold text-white">Model activity</h1>
154
+ <h1 class="truncate text-base font-semibold text-white">
155
+ {{ t('observability.modelActivity') }}
156
+ </h1>
154
157
  <p v-if="block" class="truncate text-xs text-slate-500">
155
158
  {{ block.title }} · {{ instance?.pipelineName }}
156
159
  </p>
@@ -166,7 +169,7 @@ function exportJson() {
166
169
  "
167
170
  @click="view = 'calls'"
168
171
  >
169
- Model activity
172
+ {{ t('observability.modelActivity') }}
170
173
  </button>
171
174
  <button
172
175
  class="rounded-md px-2.5 py-1 transition"
@@ -177,7 +180,7 @@ function exportJson() {
177
180
  "
178
181
  @click="view = 'context'"
179
182
  >
180
- Provided context
183
+ {{ t('observability.providedContext') }}
181
184
  </button>
182
185
  </div>
183
186
  <UButton
@@ -188,17 +191,17 @@ function exportJson() {
188
191
  size="sm"
189
192
  :loading="exporting"
190
193
  :disabled="!calls.length"
191
- title="Download an LLM-friendly JSON export of this run"
194
+ :title="t('observability.exportHint')"
192
195
  @click="exportJson"
193
196
  >
194
- Export JSON
197
+ {{ t('observability.exportJson') }}
195
198
  </UButton>
196
199
  <UButton
197
200
  icon="i-lucide-x"
198
201
  color="neutral"
199
202
  variant="ghost"
200
203
  size="sm"
201
- title="Close (Esc)"
204
+ :title="t('observability.closeEsc')"
202
205
  @click="close"
203
206
  />
204
207
  </div>
@@ -210,12 +213,14 @@ function exportJson() {
210
213
  <section class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
211
214
  <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-4">
212
215
  <div>
213
- <dt class="text-[11px] uppercase tracking-wide text-slate-500">Calls</dt>
216
+ <dt class="text-[11px] uppercase tracking-wide text-slate-500">
217
+ {{ t('observability.summary.calls') }}
218
+ </dt>
214
219
  <dd class="mt-0.5 tabular-nums text-slate-200">{{ totals.calls }}</dd>
215
220
  </div>
216
221
  <div>
217
222
  <dt class="text-[11px] uppercase tracking-wide text-slate-500">
218
- Tokens (in / out)
223
+ {{ t('observability.summary.tokensInOut') }}
219
224
  </dt>
220
225
  <dd class="mt-0.5 tabular-nums text-slate-200">
221
226
  {{ formatTokens(totals.promptTokens) }} /
@@ -224,7 +229,7 @@ function exportJson() {
224
229
  </div>
225
230
  <div>
226
231
  <dt class="text-[11px] uppercase tracking-wide text-slate-500">
227
- Transport overhead
232
+ {{ t('observability.summary.transportOverhead') }}
228
233
  </dt>
229
234
  <dd class="mt-0.5 tabular-nums text-slate-200">
230
235
  <span v-if="totals.transportPct !== null">
@@ -235,7 +240,7 @@ function exportJson() {
235
240
  </div>
236
241
  <div>
237
242
  <dt class="text-[11px] uppercase tracking-wide text-slate-500">
238
- Model execution
243
+ {{ t('observability.summary.modelExecution') }}
239
244
  </dt>
240
245
  <dd class="mt-0.5 tabular-nums text-slate-200">
241
246
  {{ formatMs(totals.upstreamMs) }}
@@ -244,13 +249,27 @@ function exportJson() {
244
249
  </dl>
245
250
  <div class="mt-3 flex flex-wrap gap-1.5">
246
251
  <UBadge v-if="totals.errors" color="error" variant="subtle" size="sm">
247
- {{ totals.errors }} error{{ totals.errors === 1 ? '' : 's' }}
252
+ {{
253
+ t('observability.metricsBar.errors', { count: totals.errors }, totals.errors)
254
+ }}
248
255
  </UBadge>
249
256
  <UBadge v-if="totals.warnings" color="warning" variant="subtle" size="sm">
250
- {{ totals.warnings }} warning{{ totals.warnings === 1 ? '' : 's' }}
257
+ {{
258
+ t(
259
+ 'observability.metricsBar.warnings',
260
+ { count: totals.warnings },
261
+ totals.warnings,
262
+ )
263
+ }}
251
264
  </UBadge>
252
265
  <UBadge v-if="totals.truncated" color="error" variant="subtle" size="sm">
253
- {{ totals.truncated }} truncated
266
+ {{
267
+ t(
268
+ 'observability.summary.truncated',
269
+ { count: totals.truncated },
270
+ totals.truncated,
271
+ )
272
+ }}
254
273
  </UBadge>
255
274
  </div>
256
275
  </section>
@@ -260,8 +279,8 @@ function exportJson() {
260
279
  v-if="loading && !calls.length"
261
280
  class="flex items-center gap-2 py-8 text-center text-sm text-slate-500 justify-center"
262
281
  >
263
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" /> Loading model
264
- activity…
282
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
283
+ {{ t('observability.loadingActivity') }}
265
284
  </p>
266
285
  <p
267
286
  v-else-if="error"
@@ -273,7 +292,7 @@ function exportJson() {
273
292
  v-else-if="!calls.length"
274
293
  class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
275
294
  >
276
- No model calls recorded for this run.
295
+ {{ t('observability.noCalls') }}
277
296
  </p>
278
297
 
279
298
  <!-- per-call list -->
@@ -309,18 +328,26 @@ function exportJson() {
309
328
  class="ml-auto flex items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
310
329
  >
311
330
  <span
312
- :title="`${c.promptTokens} prompt / ${c.completionTokens} completion tokens`"
331
+ :title="
332
+ t('observability.call.tokensTitle', {
333
+ prompt: c.promptTokens,
334
+ completion: c.completionTokens,
335
+ })
336
+ "
313
337
  >
314
338
  {{ formatTokens(c.promptTokens) }}↑ {{ formatTokens(c.completionTokens) }}↓
315
339
  </span>
316
- <span v-if="headroomOf(c) !== null" :title="'Output used vs limit'">
340
+ <span
341
+ v-if="headroomOf(c) !== null"
342
+ :title="t('observability.call.outputUsedVsLimit')"
343
+ >
317
344
  {{ headroomOf(c) }}%
318
345
  </span>
319
- <span title="Transport overhead / model execution">
346
+ <span :title="t('observability.call.transportVsExecution')">
320
347
  {{ formatMs(c.overheadMs) }} / {{ formatMs(c.upstreamMs) }}
321
348
  </span>
322
349
  <UBadge v-if="!c.ok" color="error" variant="subtle" size="sm">
323
- {{ c.httpStatus ?? 'error' }}
350
+ {{ c.httpStatus ?? t('observability.call.error') }}
324
351
  </UBadge>
325
352
  <UBadge
326
353
  v-else-if="isWarning(c.finishReason)"
@@ -330,7 +357,9 @@ function exportJson() {
330
357
  >
331
358
  {{ c.finishReason }}
332
359
  </UBadge>
333
- <span v-else class="text-slate-600">{{ c.finishReason ?? 'ok' }}</span>
360
+ <span v-else class="text-slate-600">{{
361
+ c.finishReason ?? t('observability.call.ok')
362
+ }}</span>
334
363
  <span class="hidden text-slate-600 md:inline">{{ clock(c.createdAt) }}</span>
335
364
  </div>
336
365
  </button>
@@ -340,27 +369,40 @@ function exportJson() {
340
369
  {{ c.errorMessage }}
341
370
  </p>
342
371
  <div class="flex flex-wrap gap-x-5 gap-y-1 text-[11px] text-slate-500">
343
- <span>{{ c.messageCount }} messages</span>
344
- <span>{{ c.toolCount }} tools</span>
345
- <span>{{ c.streaming ? 'streamed' : 'buffered' }}</span>
346
- <span v-if="c.requestMaxTokens != null"
347
- >max_tokens {{ c.requestMaxTokens }}</span
348
- >
349
- <span v-if="c.cachedPromptTokens > 0" class="text-emerald-400"
350
- >{{ c.cachedPromptTokens }}/{{ c.promptTokens }} prompt cached</span
351
- >
352
- <span>total {{ formatMs(c.totalMs) }}</span>
372
+ <span>{{ t('observability.call.messages', { count: c.messageCount }) }}</span>
373
+ <span>{{ t('observability.call.tools', { count: c.toolCount }) }}</span>
374
+ <span>{{
375
+ c.streaming
376
+ ? t('observability.call.streamed')
377
+ : t('observability.call.buffered')
378
+ }}</span>
379
+ <span v-if="c.requestMaxTokens != null">{{
380
+ t('observability.call.maxTokens', { value: c.requestMaxTokens })
381
+ }}</span>
382
+ <span v-if="c.cachedPromptTokens > 0" class="text-emerald-400">{{
383
+ t('observability.call.promptCached', {
384
+ cached: c.cachedPromptTokens,
385
+ prompt: c.promptTokens,
386
+ })
387
+ }}</span>
388
+ <span>{{
389
+ t('observability.call.total', { duration: formatMs(c.totalMs) })
390
+ }}</span>
353
391
  </div>
354
392
  <div>
355
393
  <div
356
394
  class="mb-1 flex items-center gap-2 text-[11px] uppercase tracking-wide text-slate-500"
357
395
  >
358
- <span>Prompt</span>
396
+ <span>{{ t('observability.call.prompt') }}</span>
359
397
  <span
360
398
  v-if="c.promptPrefixCount > 0"
361
399
  class="normal-case tracking-normal text-slate-600"
362
400
  >
363
- (new messages only — {{ c.promptPrefixCount }} earlier omitted)
401
+ {{
402
+ t('observability.call.promptPrefixOmitted', {
403
+ count: c.promptPrefixCount,
404
+ })
405
+ }}
364
406
  </span>
365
407
  </div>
366
408
  <pre
@@ -370,7 +412,7 @@ function exportJson() {
370
412
  </div>
371
413
  <div>
372
414
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
373
- Response
415
+ {{ t('observability.call.response') }}
374
416
  </div>
375
417
  <pre
376
418
  class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
@@ -379,7 +421,7 @@ function exportJson() {
379
421
  </div>
380
422
  <div v-if="c.reasoningText">
381
423
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
382
- Reasoning
424
+ {{ t('observability.call.reasoning') }}
383
425
  </div>
384
426
  <pre
385
427
  class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"
@@ -397,15 +439,14 @@ function exportJson() {
397
439
  v-if="contextLoading && !contextSnapshots.length"
398
440
  class="flex items-center justify-center gap-2 py-8 text-center text-sm text-slate-500"
399
441
  >
400
- <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" /> Loading provided
401
- context…
442
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
443
+ {{ t('observability.loadingContext') }}
402
444
  </p>
403
445
  <p
404
446
  v-else-if="!contextSnapshots.length"
405
447
  class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
406
448
  >
407
- No agent context stored for this run. It is captured per dispatch when the workspace
408
- has "Store full agent context" enabled.
449
+ {{ t('observability.noContext') }}
409
450
  </p>
410
451
 
411
452
  <ul v-else class="space-y-2">
@@ -435,10 +476,12 @@ function exportJson() {
435
476
  <div
436
477
  class="ml-auto flex items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
437
478
  >
438
- <span :title="'Injected context files'">{{ s.contextFiles.length }} files</span>
439
- <span :title="'Best-practice fragments'"
440
- >{{ s.fragments.length }} fragments</span
441
- >
479
+ <span :title="t('observability.context.injectedFiles')">{{
480
+ t('observability.context.filesCount', { count: s.contextFiles.length })
481
+ }}</span>
482
+ <span :title="t('observability.context.bestPracticeFragments')">{{
483
+ t('observability.context.fragmentsCount', { count: s.fragments.length })
484
+ }}</span>
442
485
  <span class="hidden text-slate-600 md:inline">{{ clock(s.createdAt) }}</span>
443
486
  </div>
444
487
  </button>
@@ -446,7 +489,7 @@ function exportJson() {
446
489
  <div v-if="expandedCtx[s.id]" class="border-t border-slate-800 px-4 py-3 space-y-3">
447
490
  <div>
448
491
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
449
- System prompt
492
+ {{ t('observability.context.systemPrompt') }}
450
493
  </div>
451
494
  <pre
452
495
  class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
@@ -455,7 +498,7 @@ function exportJson() {
455
498
  </div>
456
499
  <div>
457
500
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
458
- User prompt
501
+ {{ t('observability.context.userPrompt') }}
459
502
  </div>
460
503
  <pre
461
504
  class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
@@ -464,7 +507,7 @@ function exportJson() {
464
507
  </div>
465
508
  <div v-if="s.fragments.length">
466
509
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
467
- Best-practice fragments
510
+ {{ t('observability.context.bestPracticeFragments') }}
468
511
  </div>
469
512
  <div
470
513
  v-for="f in s.fragments"
@@ -480,7 +523,7 @@ function exportJson() {
480
523
  </div>
481
524
  <div v-if="s.contextFiles.length">
482
525
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
483
- Injected context files
526
+ {{ t('observability.context.injectedFiles') }}
484
527
  </div>
485
528
  <div
486
529
  v-for="file in s.contextFiles"
@@ -499,7 +542,7 @@ function exportJson() {
499
542
  </div>
500
543
  <div>
501
544
  <div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
502
- Details
545
+ {{ t('observability.context.details') }}
503
546
  </div>
504
547
  <pre
505
548
  class="max-h-48 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"