@cat-factory/app 0.95.0 → 0.96.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.
@@ -5,14 +5,11 @@
5
5
  // banner disappears, but this collapsed history stays available so every previous error
6
6
  // remains viewable. Renders nothing when there is no trail.
7
7
  import type { AgentFailure } from '~/types/domain'
8
- import FailureDetail from '~/components/board/FailureDetail.vue'
8
+ import FailureHistoryList from '~/components/board/FailureHistoryList.vue'
9
9
 
10
10
  const props = defineProps<{ failures: AgentFailure[] }>()
11
11
 
12
- const { t, d } = useI18n()
13
-
14
- // Newest attempt first — the most recent failure is the most relevant to look at.
15
- const ordered = computed(() => [...props.failures].reverse())
12
+ const { t } = useI18n()
16
13
  </script>
17
14
 
18
15
  <template>
@@ -28,33 +25,6 @@ const ordered = computed(() => [...props.failures].reverse())
28
25
  {{ t('board.failure.history.previousErrors', { count: failures.length }, failures.length) }}
29
26
  </summary>
30
27
 
31
- <ol class="mt-2 space-y-2">
32
- <li
33
- v-for="failure in ordered"
34
- :key="failure.occurredAt"
35
- class="rounded-md border border-slate-800/80 bg-slate-950/50 px-2.5 py-2"
36
- data-testid="agent-failure-history-entry"
37
- >
38
- <div class="flex items-center gap-1.5 text-[10px] text-slate-500">
39
- <UIcon name="i-lucide-alert-triangle" class="h-3 w-3 shrink-0 text-rose-400/70" />
40
- <time>{{ d(new Date(failure.occurredAt), 'long') }}</time>
41
- </div>
42
-
43
- <p class="mt-1 text-[11px] leading-snug text-slate-300" :title="failure.message">
44
- {{ failure.message }}
45
- </p>
46
-
47
- <p v-if="failure.hint" class="mt-1 text-[10px] leading-snug text-slate-500">
48
- {{ failure.hint }}
49
- </p>
50
-
51
- <FailureDetail
52
- :detail="failure.detail"
53
- :message="failure.message"
54
- summary-class="text-[10px] text-slate-500 hover:text-slate-300"
55
- pre-class="bg-slate-950/80 text-[10px] text-slate-400"
56
- />
57
- </li>
58
- </ol>
28
+ <FailureHistoryList :failures="props.failures" class="mt-2" />
59
29
  </details>
60
30
  </template>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ // The newest-first list of failed-attempt entries (timestamp + message + hint + collapsible
3
+ // detail), shared by the task-inspector's "previous errors" disclosure (AgentFailureHistory)
4
+ // and the step-detail overlay's per-step "execution history". Presentational only — the caller
5
+ // decides which trail to pass (the whole run's, or one step's) and how to reveal it.
6
+ import type { AgentFailure } from '~/types/domain'
7
+ import FailureDetail from '~/components/board/FailureDetail.vue'
8
+
9
+ const props = defineProps<{ failures: AgentFailure[] }>()
10
+
11
+ const { d } = useI18n()
12
+
13
+ // Newest attempt first — the most recent failure is the most relevant to look at.
14
+ const ordered = computed(() => [...props.failures].reverse())
15
+ </script>
16
+
17
+ <template>
18
+ <ol class="space-y-2">
19
+ <li
20
+ v-for="failure in ordered"
21
+ :key="failure.occurredAt"
22
+ class="rounded-md border border-slate-800/80 bg-slate-950/50 px-2.5 py-2"
23
+ data-testid="agent-failure-history-entry"
24
+ >
25
+ <div class="flex items-center gap-1.5 text-[10px] text-slate-500">
26
+ <UIcon name="i-lucide-alert-triangle" class="h-3 w-3 shrink-0 text-rose-400/70" />
27
+ <time>{{ d(new Date(failure.occurredAt), 'long') }}</time>
28
+ </div>
29
+
30
+ <p class="mt-1 text-[11px] leading-snug text-slate-300" :title="failure.message">
31
+ {{ failure.message }}
32
+ </p>
33
+
34
+ <p v-if="failure.hint" class="mt-1 text-[10px] leading-snug text-slate-500">
35
+ {{ failure.hint }}
36
+ </p>
37
+
38
+ <FailureDetail
39
+ :detail="failure.detail"
40
+ :message="failure.message"
41
+ summary-class="text-[10px] text-slate-500 hover:text-slate-300"
42
+ pre-class="bg-slate-950/80 text-[10px] text-slate-400"
43
+ />
44
+ </li>
45
+ </ol>
46
+ </template>
@@ -27,6 +27,10 @@ const { t } = useI18n()
27
27
 
28
28
  // Draft replies, keyed by item id, so editing one item doesn't disturb others.
29
29
  const drafts = ref<Record<string, string>>({})
30
+ // The server-side reply each draft was last seeded/synced to, so the seeding watch can refresh
31
+ // a draft when the recorded reply changes server-side WITHOUT clobbering one the human is
32
+ // actively editing (mirrors the requirements window).
33
+ const seededReply = ref<Record<string, string>>({})
30
34
  // Freeform "do it differently" comment when redoing a merge the human was unhappy with.
31
35
  const redoComment = ref('')
32
36
  const showRedo = ref(false)
@@ -39,10 +43,14 @@ const showRedo = ref(false)
39
43
  const { open, blockId, close } = useResultView('clarity-review', {
40
44
  onOpen: (id) => {
41
45
  drafts.value = {}
46
+ seededReply.value = {}
42
47
  redoComment.value = ''
43
48
  showRedo.value = false
44
49
  void clarity.load(id)
45
50
  },
51
+ // Flush any typed-but-unblurred answer before the view tears down (X, backdrop, Escape) so
52
+ // closing the window never silently drops it (UX-33).
53
+ onClose: () => void flushDrafts(),
46
54
  })
47
55
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
48
56
  const review = computed<ClarityReview | null>(() =>
@@ -144,18 +152,70 @@ function notifyError(title: string, e: unknown) {
144
152
  })
145
153
  }
146
154
 
147
- async function submitReply(item: ClarityReviewItem) {
148
- if (!review.value) return
155
+ // Answers auto-save on blur — no explicit "save" button (matching the requirements window, so
156
+ // muscle memory carries across the two, UX-34). The textarea is pre-seeded with the recorded
157
+ // reply (see the watch below); persist only when the trimmed draft actually differs from what's
158
+ // already recorded, so blurring an untouched field is a no-op.
159
+ async function persistDraft(item: ClarityReviewItem, r: ClarityReview | null = review.value) {
160
+ if (!r || frozen.value) return
149
161
  const text = (drafts.value[item.id] ?? '').trim()
150
- if (!text) return
162
+ if (!text || text === (item.reply ?? '').trim()) return
151
163
  try {
152
- await clarity.reply(review.value, item.id, text)
153
- drafts.value = { ...drafts.value, [item.id]: '' }
164
+ await clarity.reply(r, item.id, text)
154
165
  } catch (e) {
155
166
  notifyError(t('clarity.error.saveAnswer'), e)
156
167
  }
157
168
  }
158
169
 
170
+ // Persist every dirty draft before an action that consumes the answers (or on window close).
171
+ // Snapshots the review up front and threads it through, so the persist completes even if the
172
+ // window closes mid-flush (the reactive `review` goes null the moment the view tears down).
173
+ async function flushDrafts() {
174
+ const r = review.value
175
+ if (!r) return
176
+ for (const item of r.items) {
177
+ if (item.status === 'open' || item.status === 'answered') await persistDraft(item, r)
178
+ }
179
+ }
180
+
181
+ // Seed a draft for each finding from its recorded reply so the textarea shows the current
182
+ // answer (editing in place). New findings from a re-review get seeded; a draft the user hasn't
183
+ // diverged from is refreshed when the recorded reply changes server-side; drafts the user is
184
+ // actively editing are left untouched.
185
+ watch(
186
+ review,
187
+ (r) => {
188
+ if (!r) return
189
+ const nextDrafts = { ...drafts.value }
190
+ const nextSeeded = { ...seededReply.value }
191
+ let changed = false
192
+ for (const item of r.items) {
193
+ const reply = item.reply ?? ''
194
+ if (!(item.id in nextDrafts)) {
195
+ nextDrafts[item.id] = reply
196
+ nextSeeded[item.id] = reply
197
+ changed = true
198
+ continue
199
+ }
200
+ const draft = nextDrafts[item.id] ?? ''
201
+ const seeded = nextSeeded[item.id] ?? ''
202
+ if (draft === seeded && draft !== reply) {
203
+ nextDrafts[item.id] = reply
204
+ nextSeeded[item.id] = reply
205
+ changed = true
206
+ } else if (draft === reply && seeded !== reply) {
207
+ nextSeeded[item.id] = reply
208
+ changed = true
209
+ }
210
+ }
211
+ if (changed) {
212
+ drafts.value = nextDrafts
213
+ seededReply.value = nextSeeded
214
+ }
215
+ },
216
+ { immediate: true },
217
+ )
218
+
159
219
  async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus) {
160
220
  if (!review.value) return
161
221
  try {
@@ -168,6 +228,7 @@ async function setStatus(item: ClarityReviewItem, itemStatus: ClarityItemStatus)
168
228
  async function incorporate(feedback?: string) {
169
229
  if (!review.value || !blockId.value) return
170
230
  try {
231
+ await flushDrafts()
171
232
  await clarity.incorporate(review.value, feedback)
172
233
  } catch (e) {
173
234
  notifyError(t('clarity.error.incorporate'), e)
@@ -208,6 +269,7 @@ async function proceed() {
208
269
  if (!blockId.value) return
209
270
  acting.value = true
210
271
  try {
272
+ await flushDrafts()
211
273
  await clarity.proceed(blockId.value)
212
274
  toast.add({ title: t('clarity.toast.proceeding'), icon: 'i-lucide-arrow-right' })
213
275
  } catch (e) {
@@ -269,7 +331,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
269
331
  </div>
270
332
  </header>
271
333
 
272
- <div class="flex min-h-0 flex-1">
334
+ <div class="flex min-h-0 flex-1 flex-col lg:flex-row">
273
335
  <!-- main column -->
274
336
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
275
337
  <p class="mb-4 text-sm text-slate-400">
@@ -372,9 +434,10 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
372
434
  {{ item.detail }}
373
435
  </p>
374
436
 
375
- <!-- recorded answer -->
437
+ <!-- recorded answer (only for non-editable findings — for editable ones
438
+ the answer lives in the textarea below, seeded from the reply) -->
376
439
  <div
377
- v-if="item.reply"
440
+ v-if="item.reply && item.status !== 'open' && item.status !== 'answered'"
378
441
  class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
379
442
  >
380
443
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
@@ -383,7 +446,8 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
383
446
  <p class="whitespace-pre-line">{{ item.reply }}</p>
384
447
  </div>
385
448
 
386
- <!-- react: answer (relevant) or dismiss (irrelevant). Disabled once the
449
+ <!-- react: answer (relevant) or dismiss (irrelevant). The answer
450
+ auto-saves on blur — no explicit save button. Disabled once the
387
451
  bug report is clarified / awaiting a higher-level decision. -->
388
452
  <template v-if="item.status === 'open' || item.status === 'answered'">
389
453
  <UTextarea
@@ -392,24 +456,11 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
392
456
  autoresize
393
457
  size="sm"
394
458
  class="mt-2 w-full"
395
- :placeholder="
396
- item.reply
397
- ? t('clarity.refineAnswerPlaceholder')
398
- : t('clarity.answerPlaceholder')
399
- "
459
+ :placeholder="t('clarity.answerPlaceholder')"
400
460
  :disabled="frozen"
461
+ @blur="persistDraft(item)"
401
462
  />
402
463
  <div class="mt-2 flex flex-wrap items-center gap-2">
403
- <UButton
404
- color="primary"
405
- variant="soft"
406
- size="xs"
407
- icon="i-lucide-corner-down-left"
408
- :disabled="!(drafts[item.id] ?? '').trim() || frozen"
409
- @click="submitReply(item)"
410
- >
411
- {{ t('clarity.saveAnswer') }}
412
- </UButton>
413
464
  <UButton
414
465
  color="neutral"
415
466
  variant="ghost"
@@ -476,10 +527,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
476
527
  </template>
477
528
  </div>
478
529
 
479
- <!-- right action rail -->
480
- <aside class="hidden w-72 shrink-0 flex-col border-s border-slate-800 lg:flex">
530
+ <!-- action rail: a right-hand column on wide screens, a bottom action bar below `lg`
531
+ (never hidden the gate is otherwise unadvanceable on a laptop split-screen /
532
+ tablet, UX-32). The informational stats collapse away below `lg` to keep the
533
+ bottom bar compact; the actions themselves always show. -->
534
+ <aside
535
+ class="flex w-full shrink-0 flex-col border-t border-slate-800 lg:w-72 lg:border-s lg:border-t-0"
536
+ >
481
537
  <div class="flex flex-col gap-4 px-4 py-5">
482
- <div v-if="review" class="space-y-2 text-xs text-slate-400">
538
+ <div v-if="review" class="hidden space-y-2 text-xs text-slate-400 lg:block">
483
539
  <div class="flex items-center justify-between">
484
540
  <span>{{ t('clarity.rail.findings') }}</span>
485
541
  <span class="text-slate-300">{{ review.items.length }}</span>
@@ -11,6 +11,7 @@ import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBind
11
11
  import { UI_TESTER_AGENT_KIND } from '@cat-factory/contracts'
12
12
  import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
13
13
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
14
+ import FailureHistoryList from '~/components/board/FailureHistoryList.vue'
14
15
  import { useStepTimer } from '~/composables/useStepTimer'
15
16
  import { useStepProse } from '~/composables/useStepProse'
16
17
  import { useStepApproval } from '~/composables/useStepApproval'
@@ -84,6 +85,19 @@ const runNotes = computed(() => (isFrontendFrame.value ? (instance.value?.notes
84
85
  const showProvisioning = ref(false)
85
86
  const executionId = computed(() => instance.value?.id ?? null)
86
87
 
88
+ // This step's own "execution history": the run-level failure trail narrowed to the failures
89
+ // recorded for THIS step (each carries the `stepIndex` it failed at). Includes the current
90
+ // failure when the run is presently failed at this step (it moves into `failureHistory` only on
91
+ // the next retry). Revealed behind a toggle, mirroring the infra-attempts drawer above.
92
+ const stepFailures = computed(() => {
93
+ const idx = ctx.value?.stepIndex
94
+ if (idx == null) return []
95
+ const trail = [...(instance.value?.failureHistory ?? [])]
96
+ if (instance.value?.failure) trail.push(instance.value.failure)
97
+ return trail.filter((f) => f.stepIndex === idx)
98
+ })
99
+ const showHistory = ref(false)
100
+
87
101
  // A failed run is no longer executing: a step left mid-flight (state still
88
102
  // `working`, no `finishedAt`) must stop looking live — no ticking clock, no
89
103
  // "spinning up" phase, no spinner.
@@ -188,6 +202,8 @@ watch(
188
202
  () => {
189
203
  prose.reset()
190
204
  approval.resetForStep()
205
+ // Collapse the per-step execution history so reopening a different step starts clean.
206
+ showHistory.value = false
191
207
  },
192
208
  )
193
209
 
@@ -423,6 +439,35 @@ async function copyOutput() {
423
439
  />
424
440
  </div>
425
441
 
442
+ <!-- this step's failure trail (the run-level history narrowed to this step),
443
+ behind a toggle — mirrors the "previous errors" history on the task inspector
444
+ but scoped to the step the user is looking at -->
445
+ <div v-if="stepFailures.length">
446
+ <UButton
447
+ :icon="showHistory ? 'i-lucide-chevron-up' : 'i-lucide-history'"
448
+ variant="ghost"
449
+ size="xs"
450
+ data-testid="step-execution-history-toggle"
451
+ @click="
452
+ () => {
453
+ showHistory = !showHistory
454
+ }
455
+ "
456
+ >
457
+ {{
458
+ showHistory
459
+ ? t('panels.stepDetail.hideExecutionHistory')
460
+ : t('panels.stepDetail.executionHistory')
461
+ }}
462
+ </UButton>
463
+ <FailureHistoryList
464
+ v-if="showHistory"
465
+ class="mt-2"
466
+ :failures="stepFailures"
467
+ data-testid="step-execution-history"
468
+ />
469
+ </div>
470
+
426
471
  <!-- tester report: what was tested, the per-area outcomes, the concerns
427
472
  it raised and the greenlight verdict; plus the fixer-loop phase -->
428
473
  <StepTestReport v-if="testReport" :report="testReport" :phase="testPhase" />
@@ -1,7 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, reactive, ref, watch } from 'vue'
3
3
  import { onKeyStroke } from '@vueuse/core'
4
- import type { AgentContextSnapshot, LlmCallMetric } from '~/types/execution'
4
+ import type {
5
+ AgentContextSnapshot,
6
+ AgentSearchQuery,
7
+ LlmCallMetric,
8
+ WebSearchProvider,
9
+ } from '~/types/execution'
5
10
  import { agentKindMeta } from '~/utils/catalog'
6
11
  import { formatMs, formatTokens, pct } from '~/utils/observability'
7
12
 
@@ -33,8 +38,9 @@ const error = computed(() =>
33
38
  executionId.value ? (observability.errors[executionId.value] ?? null) : null,
34
39
  )
35
40
 
36
- // Which view is shown: the per-call model activity, or the complete provided context.
37
- const view = ref<'calls' | 'context'>('calls')
41
+ // Which view is shown: per-call model activity, the complete provided context, or the
42
+ // performed web searches.
43
+ const view = ref<'calls' | 'context' | 'search'>('calls')
38
44
 
39
45
  const contextSnapshots = computed<AgentContextSnapshot[]>(() =>
40
46
  executionId.value ? observability.contextFor(executionId.value) : [],
@@ -43,6 +49,39 @@ const contextLoading = computed(
43
49
  () => !!executionId.value && observability.isContextLoading(executionId.value),
44
50
  )
45
51
 
52
+ const searchQueries = computed<AgentSearchQuery[]>(() =>
53
+ executionId.value ? observability.searchQueriesFor(executionId.value) : [],
54
+ )
55
+ const searchLoading = computed(
56
+ () => !!executionId.value && observability.isSearchQueriesLoading(executionId.value),
57
+ )
58
+
59
+ // Brand names, kept verbatim across locales (not translatable prose).
60
+ const PROVIDER_LABEL: Record<WebSearchProvider, string> = { brave: 'Brave', searxng: 'SearXNG' }
61
+ function providerLabel(provider: WebSearchProvider | null): string {
62
+ return provider ? PROVIDER_LABEL[provider] : ''
63
+ }
64
+
65
+ // Whether web search was available to this run's container agents, and which provider(s)
66
+ // served it — a static per-run fact set on each container step at dispatch (not gated by
67
+ // prompt-recording telemetry, unlike the performed queries below).
68
+ const searchAvailability = computed<{ available: boolean; providers: WebSearchProvider[] } | null>(
69
+ () => {
70
+ const steps = (instance.value?.steps ?? []).filter((s) => s.search)
71
+ if (!steps.length) return null
72
+ const available = steps.some((s) => s.search?.available)
73
+ const providers = [
74
+ ...new Set(
75
+ steps
76
+ .map((s) => s.search)
77
+ .filter((x): x is NonNullable<typeof x> => !!x?.available && !!x.provider)
78
+ .map((x) => x.provider as WebSearchProvider),
79
+ ),
80
+ ]
81
+ return { available, providers }
82
+ },
83
+ )
84
+
46
85
  // Load (and refresh) whenever a different run's panel opens. Reset to the calls view
47
86
  // and load both the calls and the provided-context snapshots.
48
87
  watch(
@@ -52,6 +91,7 @@ watch(
52
91
  view.value = 'calls'
53
92
  void observability.load(id)
54
93
  void observability.loadContext(id)
94
+ void observability.loadSearchQueries(id)
55
95
  }
56
96
  },
57
97
  // Lazy v-if mount: the panel mounts with executionId already set, so load immediately.
@@ -182,6 +222,17 @@ function exportJson() {
182
222
  >
183
223
  {{ t('observability.providedContext') }}
184
224
  </button>
225
+ <button
226
+ class="rounded-md px-2.5 py-1 transition"
227
+ :class="
228
+ view === 'search'
229
+ ? 'bg-slate-800 text-slate-100'
230
+ : 'text-slate-400 hover:text-slate-200'
231
+ "
232
+ @click="view = 'search'"
233
+ >
234
+ {{ t('observability.webSearch') }}
235
+ </button>
185
236
  </div>
186
237
  <UButton
187
238
  v-if="view === 'calls'"
@@ -434,7 +485,7 @@ function exportJson() {
434
485
  </div>
435
486
 
436
487
  <!-- Provided context: the complete context each container agent was given. -->
437
- <div v-else class="mx-auto max-w-4xl space-y-5">
488
+ <div v-else-if="view === 'context'" class="mx-auto max-w-4xl space-y-5">
438
489
  <p
439
490
  v-if="contextLoading && !contextSnapshots.length"
440
491
  class="flex items-center justify-center gap-2 py-8 text-center text-sm text-slate-500"
@@ -553,6 +604,88 @@ function exportJson() {
553
604
  </li>
554
605
  </ul>
555
606
  </div>
607
+
608
+ <div v-else class="mx-auto max-w-4xl space-y-5">
609
+ <!-- Availability header: a static per-run fact (not telemetry-gated). -->
610
+ <section
611
+ v-if="searchAvailability"
612
+ class="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-xl border border-slate-800 bg-slate-900/50 px-4 py-3 text-[13px]"
613
+ >
614
+ <span class="text-[11px] uppercase tracking-wide text-slate-500">
615
+ {{ t('observability.webSearch') }}
616
+ </span>
617
+ <span
618
+ class="inline-flex items-center gap-1.5"
619
+ :class="searchAvailability.available ? 'text-emerald-300' : 'text-slate-400'"
620
+ >
621
+ <UIcon
622
+ :name="searchAvailability.available ? 'i-lucide-globe' : 'i-lucide-globe-lock'"
623
+ class="h-4 w-4"
624
+ />
625
+ {{
626
+ searchAvailability.available
627
+ ? t('observability.search.available')
628
+ : t('observability.search.unavailable')
629
+ }}
630
+ </span>
631
+ <span v-if="searchAvailability.providers.length" class="text-slate-400 tabular-nums">
632
+ {{ t('observability.search.provider') }}:
633
+ {{ searchAvailability.providers.map(providerLabel).join(', ') }}
634
+ </span>
635
+ </section>
636
+
637
+ <p
638
+ v-if="searchLoading && !searchQueries.length"
639
+ class="flex items-center justify-center gap-2 py-8 text-center text-sm text-slate-500"
640
+ >
641
+ <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
642
+ {{ t('observability.loadingSearch') }}
643
+ </p>
644
+ <p
645
+ v-else-if="!searchQueries.length"
646
+ class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
647
+ >
648
+ {{ t('observability.noSearch') }}
649
+ </p>
650
+
651
+ <div v-else>
652
+ <div class="mb-2 text-[11px] uppercase tracking-wide text-slate-500">
653
+ {{ t('observability.search.queriesTitle') }}
654
+ </div>
655
+ <ul class="space-y-2">
656
+ <li
657
+ v-for="q in searchQueries"
658
+ :key="q.id"
659
+ class="flex items-center gap-3 rounded-xl border border-slate-800 bg-slate-900/40 px-4 py-2.5"
660
+ >
661
+ <UIcon
662
+ :name="agentMeta(q.agentKind).icon"
663
+ class="h-4 w-4 shrink-0"
664
+ :style="{ color: agentMeta(q.agentKind).color }"
665
+ :title="agentMeta(q.agentKind).label"
666
+ />
667
+ <span class="min-w-0 flex-1 truncate text-[13px] text-slate-200" :title="q.query">
668
+ {{ q.query }}
669
+ </span>
670
+ <div
671
+ class="flex shrink-0 items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
672
+ >
673
+ <span v-if="q.provider" class="hidden sm:inline">{{
674
+ providerLabel(q.provider)
675
+ }}</span>
676
+ <span>{{
677
+ t(
678
+ 'observability.search.resultsCount',
679
+ { count: q.resultCount },
680
+ q.resultCount,
681
+ )
682
+ }}</span>
683
+ <span class="hidden text-slate-600 md:inline">{{ clock(q.createdAt) }}</span>
684
+ </div>
685
+ </li>
686
+ </ul>
687
+ </div>
688
+ </div>
556
689
  </div>
557
690
  </div>
558
691
  </Transition>
@@ -59,6 +59,10 @@ const { open, blockId, instanceId, stepIndex, close } = useResultView('requireme
59
59
  docCollapsedOverride.value = null
60
60
  void requirements.load(id)
61
61
  },
62
+ // Closing the window (X, backdrop, Escape) must not silently drop an answer the user typed
63
+ // but never blurred out of. Flush before the view tears down; flushDrafts captures the
64
+ // review up front so the persist survives blockId going null on close (UX-33).
65
+ onClose: () => void flushDrafts(),
62
66
  })
63
67
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
64
68
  const review = computed<RequirementReview | null>(() =>
@@ -167,23 +171,29 @@ function notifyError(title: string, e: unknown) {
167
171
  // the recorded reply (see the watch below); editing and blurring persists it. Persist only
168
172
  // when the trimmed draft actually differs from what's already recorded, so blurring an
169
173
  // untouched field is a no-op.
170
- async function persistDraft(item: RequirementReviewItem) {
171
- if (!review.value || frozen.value) return
174
+ async function persistDraft(
175
+ item: RequirementReviewItem,
176
+ r: RequirementReview | null = review.value,
177
+ ) {
178
+ if (!r || frozen.value) return
172
179
  const text = (drafts.value[item.id] ?? '').trim()
173
180
  if (!text || text === (item.reply ?? '').trim()) return
174
181
  try {
175
- await requirements.reply(review.value, item.id, text)
182
+ await requirements.reply(r, item.id, text)
176
183
  } catch (e) {
177
184
  notifyError(t('requirements.errors.saveAnswer'), e)
178
185
  }
179
186
  }
180
187
 
181
- // Persist every dirty draft before an action that consumes the answers, so a value the
182
- // user typed but never blurred out of isn't lost.
188
+ // Persist every dirty draft before an action that consumes the answers (or on window close),
189
+ // so a value the user typed but never blurred out of isn't lost. Snapshots the review up front
190
+ // and threads it through, so the persist completes even if the window closes mid-flush (the
191
+ // reactive `review` goes null the moment the view tears down).
183
192
  async function flushDrafts() {
184
- if (!review.value) return
185
- for (const item of review.value.items) {
186
- if (item.status === 'open' || item.status === 'answered') await persistDraft(item)
193
+ const r = review.value
194
+ if (!r) return
195
+ for (const item of r.items) {
196
+ if (item.status === 'open' || item.status === 'answered') await persistDraft(item, r)
187
197
  }
188
198
  }
189
199
 
@@ -483,7 +493,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
483
493
  </div>
484
494
  </header>
485
495
 
486
- <div class="flex min-h-0 flex-1">
496
+ <div class="flex min-h-0 flex-1 flex-col lg:flex-row">
487
497
  <!-- main column -->
488
498
  <div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
489
499
  <i18n-t
@@ -842,10 +852,15 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
842
852
  </template>
843
853
  </div>
844
854
 
845
- <!-- right action rail -->
846
- <aside class="hidden w-72 shrink-0 flex-col border-s border-slate-800 lg:flex">
855
+ <!-- action rail: a right-hand column on wide screens, a bottom action bar below `lg`
856
+ (never hidden the gate is otherwise unadvanceable on a laptop split-screen /
857
+ tablet, UX-32). The informational stats collapse away below `lg` to keep the
858
+ bottom bar compact; the actions themselves always show. -->
859
+ <aside
860
+ class="flex w-full shrink-0 flex-col border-t border-slate-800 lg:w-72 lg:border-s lg:border-t-0"
861
+ >
847
862
  <div class="flex flex-col gap-4 px-4 py-5">
848
- <div v-if="review" class="space-y-2 text-xs text-slate-400">
863
+ <div v-if="review" class="hidden space-y-2 text-xs text-slate-400 lg:block">
849
864
  <div class="flex items-center justify-between">
850
865
  <span>{{ t('requirements.stats.findings') }}</span>
851
866
  <span class="text-slate-300">{{ review.items.length }}</span>
@@ -4,6 +4,7 @@ import {
4
4
  exportExecutionLlmMetricsContract,
5
5
  getExecutionAgentContextContract,
6
6
  getExecutionLlmMetricsContract,
7
+ getExecutionSearchQueriesContract,
7
8
  mergeBlockContract,
8
9
  rejectStepContract,
9
10
  requestStepChangesContract,
@@ -145,6 +146,14 @@ export function executionApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
145
146
  pathParams: { executionId },
146
147
  }),
147
148
 
149
+ // The web searches each container agent performed in a run (query, provider,
150
+ // result count). Empty when not wired / storing is off.
151
+ getSearchQueries: (workspaceId: string, executionId: string) =>
152
+ send(getExecutionSearchQueriesContract, {
153
+ pathPrefix: ws(workspaceId),
154
+ pathParams: { executionId },
155
+ }),
156
+
148
157
  // ---- spend safeguard --------------------------------------------------
149
158
  resumeSpend: (workspaceId: string) =>
150
159
  send(resumeSpendContract, { pathPrefix: ws(workspaceId) }),
@@ -56,14 +56,18 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
56
56
  // Branch on the kind so `send` sees a single concrete contract (a union contract can't
57
57
  // type-check the optional `queryParams`).
58
58
  describeProvider: (workspaceId: string, kind: ProviderConnectionKind, backendKind?: string) =>
59
+ // Only send the `kind` query when a concrete backend is requested. Passing
60
+ // `{ kind: undefined }` serializes to `?kind=` (an empty string), which the backend
61
+ // reads as a real — unknown — backend kind and rejects with 422; omitting the param
62
+ // lets it fall back to the workspace's stored/default kind as intended.
59
63
  kind === 'environment'
60
64
  ? send(CONTRACTS.environment.describe, {
61
65
  pathPrefix: ws(workspaceId),
62
- queryParams: { kind: backendKind },
66
+ queryParams: backendKind ? { kind: backendKind } : {},
63
67
  })
64
68
  : send(CONTRACTS['runner-pool'].describe, {
65
69
  pathPrefix: ws(workspaceId),
66
- queryParams: { kind: backendKind },
70
+ queryParams: backendKind ? { kind: backendKind } : {},
67
71
  }),
68
72
 
69
73
  getProviderConnection: (workspaceId: string, kind: ProviderConnectionKind) =>
@@ -12,8 +12,17 @@
12
12
  *
13
13
  * A synchronous window (one that reads its data straight off the execution step, like the
14
14
  * test report) simply omits `onOpen`.
15
+ *
16
+ * `onClose` runs on EVERY close path — the X button, backdrop click, and the Escape key
17
+ * handled here — BEFORE the view is torn down, so a window with unsaved draft input (the
18
+ * review windows) can flush it in one place instead of every caller having to remember to.
19
+ * It runs synchronously; if it kicks off async work it must capture whatever it needs first,
20
+ * because `blockId`/the derived state go null the moment the view closes.
15
21
  */
16
- export function useResultView(viewId: string, opts?: { onOpen?: (blockId: string) => void }) {
22
+ export function useResultView(
23
+ viewId: string,
24
+ opts?: { onOpen?: (blockId: string) => void; onClose?: () => void },
25
+ ) {
17
26
  const ui = useUiStore()
18
27
 
19
28
  const open = computed(() => ui.resultView?.view === viewId)
@@ -26,6 +35,7 @@ export function useResultView(viewId: string, opts?: { onOpen?: (blockId: string
26
35
  const stage = computed(() => (open.value ? (ui.resultView!.stage ?? null) : null))
27
36
 
28
37
  function close() {
38
+ if (open.value) opts?.onClose?.()
29
39
  ui.closeResultView()
30
40
  }
31
41
 
@@ -17,6 +17,7 @@ import DecisionModal from '~/components/panels/DecisionModal.vue'
17
17
  import AgentStepDetail from '~/components/panels/AgentStepDetail.vue'
18
18
  import StepResultViewHost from '~/components/panels/StepResultViewHost.vue'
19
19
  import AddTaskModal from '~/components/board/AddTaskModal.vue'
20
+ import CreateInitiativeModal from '~/components/board/CreateInitiativeModal.vue'
20
21
  import GitHubOnboarding from '~/components/github/GitHubOnboarding.vue'
21
22
  import CommandBar from '~/components/layout/CommandBar.vue'
22
23
  import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
@@ -76,6 +76,41 @@ describe('execution store snapshot/event reconcile', () => {
76
76
  expect(store.getInstance('e1')?.rev).toBe(2)
77
77
  })
78
78
 
79
+ it('drops a superseded failed run when a retry replaces it under a new id (same block)', () => {
80
+ // A failed run for a block is cached...
81
+ store.hydrate(
82
+ [{ id: 'e_old', blockId: 'b1', steps: [], status: 'failed', rev: 1 } as never],
83
+ 'ws1',
84
+ )
85
+ // ...then a retry mints a FRESH run (new id) for the SAME block and deletes the old one
86
+ // server-side. The post-retry snapshot carries only the new running run.
87
+ store.hydrate(
88
+ [{ id: 'e_new', blockId: 'b1', steps: [], status: 'running', rev: 1 } as never],
89
+ 'ws1',
90
+ )
91
+ // The dead predecessor must not linger and shadow the running run in the by-block projection.
92
+ expect(store.getInstance('e_old')).toBeUndefined()
93
+ expect(store.getInstance('e_new')?.status).toBe('running')
94
+ expect(store.getByBlock('b1')?.id).toBe('e_new')
95
+ })
96
+
97
+ it('keeps a live-added running run when a stale snapshot still lists its block predecessor', () => {
98
+ // A retry already minted e_new (running) for b1 — a live event added it to the cache...
99
+ store.hydrate(
100
+ [{ id: 'e_new', blockId: 'b1', steps: [], status: 'running', rev: 1 } as never],
101
+ 'ws1',
102
+ )
103
+ // ...but a reconnect resync fetched BEFORE the retry resolves late (under load) and still
104
+ // carries the now-deleted predecessor e_old (failed) for the same block.
105
+ store.hydrate(
106
+ [{ id: 'e_old', blockId: 'b1', steps: [], status: 'failed', rev: 1 } as never],
107
+ 'ws1',
108
+ )
109
+ // The live running run must survive — only a TERMINAL cached run is a superseded predecessor.
110
+ expect(store.getInstance('e_new')?.status).toBe('running')
111
+ expect(store.getByBlock('b1')?.id).toBe('e_new')
112
+ })
113
+
79
114
  it('a workspace switch replaces the cache outright (no cross-board leak)', () => {
80
115
  store.hydrate([run('e1', 1, 'running')], 'ws1')
81
116
  store.upsert(run('e2', 1, 'running'))
@@ -35,6 +35,11 @@ export const useExecutionStore = defineStore('execution', () => {
35
35
  return e.rev ?? 0
36
36
  }
37
37
 
38
+ /** A finished run — nothing further will execute or emit. Matches `runLive`/`runFailed`. */
39
+ function isTerminal(status: ExecutionInstance['status']): boolean {
40
+ return status === 'done' || status === 'failed'
41
+ }
42
+
38
43
  /**
39
44
  * Reconcile the cached executions with a server snapshot for `workspaceId`. A snapshot
40
45
  * is authoritative EXCEPT where a live `execution` event already advanced (or ADDED) a
@@ -45,7 +50,24 @@ export const useExecutionStore = defineStore('execution', () => {
45
50
  * can't revert a just-terminal run to `running`. A terminal run emits nothing
46
51
  * further, so a regression here would strand the UI until an unrelated refresh.
47
52
  * - DROP: a run a live event just ADDED that the (older) snapshot never saw — keep it
48
- * rather than silently dropping it.
53
+ * rather than silently dropping it, but ONLY when it is not the terminal predecessor a
54
+ * retry replaced (see below).
55
+ *
56
+ * The DROP caveat matters because a retry/restart REPLACES a block's run with a fresh one
57
+ * under a NEW id (the old run is deleted server-side), so the two attempts can't be
58
+ * reconciled by id or `rev`. Since there is exactly one run per block, a cached-only run
59
+ * whose block the snapshot already covers is that superseded predecessor — drop it.
60
+ * Preserving it would leave the dead `failed` run shadowing the running one in the by-block
61
+ * projection (`agentRuns.byBlock`, last-write-wins), keeping the failure banner up and its
62
+ * empty trail hiding the retry's carried-forward failure history.
63
+ *
64
+ * The drop is gated on the cached run being TERMINAL (`done`/`failed`): only a finished
65
+ * predecessor is ever superseded. A cached run still `running`/`blocked`/`paused` is a
66
+ * genuinely live-added run, so it must survive even when a stale reconnect snapshot (fetched
67
+ * before a retry, resolving late under load — see `useWorkspaceStream`) still lists its
68
+ * block's now-deleted predecessor. Dropping a live run there would strand the UI showing the
69
+ * dead attempt — the inverse of the bug this guard fixes — and `rev` can't catch it (the
70
+ * ids differ).
49
71
  */
50
72
  function hydrate(next: ExecutionInstance[], workspaceId: string) {
51
73
  const sameWorkspace = hydratedWorkspaceId === workspaceId
@@ -55,12 +77,19 @@ export const useExecutionStore = defineStore('execution', () => {
55
77
  return
56
78
  }
57
79
  const incomingIds = new Set(next.map((e) => e.id))
80
+ const incomingBlocks = new Set(next.map((e) => e.blockId))
58
81
  const held = new Map(instances.value.map((e) => [e.id, e]))
59
82
  const reconciled = next.map((incoming) => {
60
83
  const current = held.get(incoming.id)
61
84
  return current && revOf(current) > revOf(incoming) ? current : incoming
62
85
  })
63
- const preserved = [...held.values()].filter((e) => !incomingIds.has(e.id))
86
+ // Preserve a cached-only run UNLESS it is the terminal predecessor a retry replaced: a
87
+ // finished (`done`/`failed`) run whose block the snapshot now covers under a fresh id.
88
+ // Gating on the CACHED run being terminal keeps a live `running`/`blocked`/`paused` run
89
+ // that a stale snapshot happens to omit.
90
+ const preserved = [...held.values()].filter(
91
+ (e) => !incomingIds.has(e.id) && !(isTerminal(e.status) && incomingBlocks.has(e.blockId)),
92
+ )
64
93
  instances.value = [...reconciled, ...preserved]
65
94
  }
66
95
 
@@ -87,7 +116,13 @@ export const useExecutionStore = defineStore('execution', () => {
87
116
  }
88
117
 
89
118
  function getByBlock(blockId: string) {
90
- return instances.value.find((e) => e.blockId === blockId)
119
+ const runs = instances.value.filter((e) => e.blockId === blockId)
120
+ if (runs.length <= 1) return runs[0]
121
+ // A block only holds several runs transiently: a stale reconnect snapshot re-listing a
122
+ // retry's now-deleted terminal predecessor alongside the live successor. Prefer the live
123
+ // one so this projection agrees with `agentRuns.byBlock` (whose last-write-wins already
124
+ // resolves to it) — the failed predecessor is dead and about to fall out on the next read.
125
+ return runs.find((e) => !isTerminal(e.status)) ?? runs.at(-1)
91
126
  }
92
127
 
93
128
  /** How many decisions anywhere are awaiting a human. */
@@ -1,6 +1,11 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
- import type { AgentContextSnapshot, LlmCallActivity, LlmCallMetric } from '~/types/execution'
3
+ import type {
4
+ AgentContextSnapshot,
5
+ AgentSearchQuery,
6
+ LlmCallActivity,
7
+ LlmCallMetric,
8
+ } from '~/types/execution'
4
9
  import { useWorkspaceStore } from '~/stores/workspace'
5
10
 
6
11
  /**
@@ -23,6 +28,10 @@ export const useObservabilityStore = defineStore('observability', () => {
23
28
  const contextByExecution = ref<Record<string, AgentContextSnapshot[]>>({})
24
29
  /** Execution ids whose context is currently loading. */
25
30
  const contextLoading = ref<Set<string>>(new Set())
31
+ /** Per-execution-id performed-search-query list (newest first). */
32
+ const searchQueriesByExecution = ref<Record<string, AgentSearchQuery[]>>({})
33
+ /** Execution ids whose search queries are currently loading. */
34
+ const searchQueriesLoading = ref<Set<string>>(new Set())
26
35
  /** Execution ids currently loading. */
27
36
  const loading = ref<Set<string>>(new Set())
28
37
  /** Execution ids currently exporting. */
@@ -134,6 +143,30 @@ export const useObservabilityStore = defineStore('observability', () => {
134
143
  }
135
144
  }
136
145
 
146
+ function searchQueriesFor(executionId: string): AgentSearchQuery[] {
147
+ return searchQueriesByExecution.value[executionId] ?? []
148
+ }
149
+ function isSearchQueriesLoading(executionId: string): boolean {
150
+ return searchQueriesLoading.value.has(executionId)
151
+ }
152
+
153
+ /** Load (or refresh) the performed web-search queries for a run. */
154
+ async function loadSearchQueries(executionId: string) {
155
+ if (!workspace.workspaceId) return
156
+ withFlag(searchQueriesLoading, executionId, true)
157
+ try {
158
+ const { searchQueries } = await api.getSearchQueries(workspace.requireId(), executionId)
159
+ searchQueriesByExecution.value = {
160
+ ...searchQueriesByExecution.value,
161
+ [executionId]: searchQueries,
162
+ }
163
+ } catch {
164
+ // Best-effort: the panel shows an empty state; nothing is persisted client-side.
165
+ } finally {
166
+ withFlag(searchQueriesLoading, executionId, false)
167
+ }
168
+ }
169
+
137
170
  /**
138
171
  * Fetch the LLM-friendly export bundle and trigger a client-side download. The
139
172
  * events socket auths via a Bearer header (a plain `<a download>` can't), so we
@@ -169,5 +202,9 @@ export const useObservabilityStore = defineStore('observability', () => {
169
202
  contextFor,
170
203
  isContextLoading,
171
204
  loadContext,
205
+ searchQueriesByExecution,
206
+ searchQueriesFor,
207
+ isSearchQueriesLoading,
208
+ loadSearchQueries,
172
209
  }
173
210
  })
@@ -24,6 +24,9 @@ export type {
24
24
  LlmCallActivity,
25
25
  LlmExportInsight,
26
26
  LlmMetricsExport,
27
+ AgentSearchQuery,
28
+ WebSearchAvailability,
29
+ WebSearchProvider,
27
30
  PipelineStep,
28
31
  FollowUpItemKind,
29
32
  FollowUpItemStatus,
@@ -1006,6 +1006,8 @@
1006
1006
  "companionCapDetail": "Do one more automatic rework round, proceed to the next step accepting the current output, or stop and reset the task so you can edit the inputs and resubmit.",
1007
1007
  "infraAttempts": "Infrastructure attempts",
1008
1008
  "hideInfraAttempts": "Hide infrastructure attempts",
1009
+ "executionHistory": "Execution history",
1010
+ "hideExecutionHistory": "Hide execution history",
1009
1011
  "editingConclusions": "Editing the conclusions",
1010
1012
  "editConclusionsPlaceholder": "Edit the agent's conclusions; your edits are saved when you approve…",
1011
1013
  "noProseOutput": "This agent produced no prose output.",
@@ -1139,6 +1141,16 @@
1139
1141
  "systemPrompt": "System prompt",
1140
1142
  "userPrompt": "User prompt",
1141
1143
  "details": "Details"
1144
+ },
1145
+ "webSearch": "Web search",
1146
+ "loadingSearch": "Loading web searches…",
1147
+ "noSearch": "No web searches recorded for this run. Queries are captured when web search runs and the workspace has 'Store full agent context' enabled.",
1148
+ "search": {
1149
+ "available": "Available",
1150
+ "unavailable": "Unavailable",
1151
+ "provider": "Provider",
1152
+ "resultsCount": "{count} result | {count} results",
1153
+ "queriesTitle": "Performed searches"
1142
1154
  }
1143
1155
  },
1144
1156
  "auth": {
@@ -3325,8 +3337,6 @@
3325
3337
  "description": "Noun label heading the recorded reply to a finding (the answer that was given), not the verb."
3326
3338
  },
3327
3339
  "answerPlaceholder": "Answer this finding…",
3328
- "refineAnswerPlaceholder": "Refine your answer…",
3329
- "saveAnswer": "Save answer",
3330
3340
  "dismissIrrelevant": "Dismiss as irrelevant",
3331
3341
  "reopen": "Reopen",
3332
3342
  "docHeading": "Clarified bug report",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "Haz una ronda más de reelaboración automática, avanza al siguiente paso aceptando la salida actual, o detén y restablece la tarea para que puedas editar las entradas y reenviarla.",
964
964
  "infraAttempts": "Intentos de infraestructura",
965
965
  "hideInfraAttempts": "Ocultar intentos de infraestructura",
966
+ "executionHistory": "Historial de ejecución",
967
+ "hideExecutionHistory": "Ocultar historial de ejecución",
966
968
  "editingConclusions": "Editando las conclusiones",
967
969
  "editConclusionsPlaceholder": "Edita las conclusiones del agente; tus cambios se guardan cuando apruebas…",
968
970
  "noProseOutput": "Este agente no produjo salida en prosa.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "Prompt del sistema",
1097
1099
  "userPrompt": "Prompt del usuario",
1098
1100
  "details": "Detalles"
1101
+ },
1102
+ "webSearch": "Búsqueda web",
1103
+ "loadingSearch": "Cargando búsquedas web…",
1104
+ "noSearch": "No se registraron búsquedas web para esta ejecución. Las consultas se capturan cuando se ejecuta la búsqueda web y el espacio de trabajo tiene activado 'Almacenar contexto completo del agente'.",
1105
+ "search": {
1106
+ "available": "Disponible",
1107
+ "unavailable": "No disponible",
1108
+ "provider": "Proveedor",
1109
+ "resultsCount": "{count} resultado | {count} resultados",
1110
+ "queriesTitle": "Búsquedas realizadas"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3218,8 +3230,6 @@
3218
3230
  "reReviewingStage": "Volviendo a revisar el informe de error actualizado… Puedes cerrar esto, te avisaremos solo si se necesita más información.",
3219
3231
  "answerLabel": "Respuesta",
3220
3232
  "answerPlaceholder": "Responde a este hallazgo…",
3221
- "refineAnswerPlaceholder": "Refina tu respuesta…",
3222
- "saveAnswer": "Guardar respuesta",
3223
3233
  "dismissIrrelevant": "Descartar por irrelevante",
3224
3234
  "reopen": "Reabrir",
3225
3235
  "docHeading": "Informe de error aclarado",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "Effectuer un tour de retravail automatique supplémentaire, passer à l'étape suivante en acceptant la sortie actuelle, ou arrêter et réinitialiser la tâche pour modifier les entrées et la resoumettre.",
964
964
  "infraAttempts": "Tentatives d'infrastructure",
965
965
  "hideInfraAttempts": "Masquer les tentatives d'infrastructure",
966
+ "executionHistory": "Historique d'exécution",
967
+ "hideExecutionHistory": "Masquer l'historique d'exécution",
966
968
  "editingConclusions": "Modification des conclusions",
967
969
  "editConclusionsPlaceholder": "Modifiez les conclusions de l'agent ; vos modifications sont enregistrées lorsque vous approuvez…",
968
970
  "noProseOutput": "Cet agent n'a produit aucune sortie en texte libre.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "Prompt système",
1097
1099
  "userPrompt": "Prompt utilisateur",
1098
1100
  "details": "Détails"
1101
+ },
1102
+ "webSearch": "Recherche web",
1103
+ "loadingSearch": "Chargement des recherches web…",
1104
+ "noSearch": "Aucune recherche web enregistrée pour cette exécution. Les requêtes sont capturées lorsque la recherche web s'exécute et que l'espace de travail a activé « Stocker le contexte complet de l'agent ».",
1105
+ "search": {
1106
+ "available": "Disponible",
1107
+ "unavailable": "Indisponible",
1108
+ "provider": "Fournisseur",
1109
+ "resultsCount": "{count} résultat | {count} résultats",
1110
+ "queriesTitle": "Recherches effectuées"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3218,8 +3230,6 @@
3218
3230
  "reReviewingStage": "Nouvelle relecture du rapport de bogue mis à jour… Vous pouvez fermer ceci, nous vous avertirons seulement si plus d'informations sont nécessaires.",
3219
3231
  "answerLabel": "Réponse",
3220
3232
  "answerPlaceholder": "Répondez à ce constat…",
3221
- "refineAnswerPlaceholder": "Affinez votre réponse…",
3222
- "saveAnswer": "Enregistrer la réponse",
3223
3233
  "dismissIrrelevant": "Écarter comme non pertinent",
3224
3234
  "reopen": "Rouvrir",
3225
3235
  "docHeading": "Rapport de bogue clarifié",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "בצע סבב עיבוד אוטומטי נוסף, המשך לשלב הבא תוך קבלת הפלט הנוכחי, או עצור ואפס את המשימה כדי לערוך את הקלטים ולשלוח מחדש.",
964
964
  "infraAttempts": "ניסיונות תשתית",
965
965
  "hideInfraAttempts": "הסתר ניסיונות תשתית",
966
+ "executionHistory": "היסטוריית הרצה",
967
+ "hideExecutionHistory": "הסתר היסטוריית הרצה",
966
968
  "editingConclusions": "עריכת המסקנות",
967
969
  "editConclusionsPlaceholder": "ערוך את מסקנות הסוכן; העריכות שלך נשמרות כשתאשר…",
968
970
  "noProseOutput": "סוכן זה לא הפיק פלט טקסטואלי.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "פרומפט מערכת",
1097
1099
  "userPrompt": "פרומפט משתמש",
1098
1100
  "details": "פרטים"
1101
+ },
1102
+ "webSearch": "חיפוש באינטרנט",
1103
+ "loadingSearch": "טוען חיפושי אינטרנט…",
1104
+ "noSearch": "לא נרשמו חיפושי אינטרנט עבור הרצה זו. שאילתות נלכדות כאשר חיפוש האינטרנט פועל וכאשר במרחב העבודה מופעל 'אחסון הקשר סוכן מלא'.",
1105
+ "search": {
1106
+ "available": "זמין",
1107
+ "unavailable": "לא זמין",
1108
+ "provider": "ספק",
1109
+ "resultsCount": "תוצאה {count} | {count} תוצאות",
1110
+ "queriesTitle": "חיפושים שבוצעו"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3229,8 +3241,6 @@
3229
3241
  "reReviewingStage": "בודק מחדש את דוח הבאג המעודכן… תוכל לסגור את זה, נודיע לך רק אם נדרש קלט נוסף.",
3230
3242
  "answerLabel": "תשובה",
3231
3243
  "answerPlaceholder": "ענה על ממצא זה…",
3232
- "refineAnswerPlaceholder": "חדד את התשובה שלך…",
3233
- "saveAnswer": "שמור תשובה",
3234
3244
  "dismissIrrelevant": "דחה כלא רלוונטי",
3235
3245
  "reopen": "פתח מחדש",
3236
3246
  "docHeading": "דוח באג מובהר",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "自動の再作業をもう1回実行するか、現在の出力を受け入れて次のステップに進むか、タスクを停止してリセットし、入力を編集して再送信してください。",
964
964
  "infraAttempts": "インフラの試行",
965
965
  "hideInfraAttempts": "インフラの試行を非表示",
966
+ "executionHistory": "実行履歴",
967
+ "hideExecutionHistory": "実行履歴を非表示",
966
968
  "editingConclusions": "結論を編集中",
967
969
  "editConclusionsPlaceholder": "エージェントの結論を編集してください。編集内容は承認時に保存されます…",
968
970
  "noProseOutput": "このエージェントは文章出力を生成しませんでした。",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "システムプロンプト",
1097
1099
  "userPrompt": "ユーザープロンプト",
1098
1100
  "details": "詳細"
1101
+ },
1102
+ "webSearch": "ウェブ検索",
1103
+ "loadingSearch": "ウェブ検索を読み込み中…",
1104
+ "noSearch": "この実行のウェブ検索は記録されていません。クエリはウェブ検索の実行時、かつワークスペースで「エージェントコンテキスト全体を保存」が有効な場合に記録されます。",
1105
+ "search": {
1106
+ "available": "利用可能",
1107
+ "unavailable": "利用不可",
1108
+ "provider": "プロバイダー",
1109
+ "resultsCount": "{count} 件の結果 | {count} 件の結果",
1110
+ "queriesTitle": "実行した検索"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3230,8 +3242,6 @@
3230
3242
  "reReviewingStage": "更新されたバグ報告を再レビュー中… これを閉じてもかまいません。追加の入力が必要な場合のみ通知します。",
3231
3243
  "answerLabel": "回答",
3232
3244
  "answerPlaceholder": "この指摘に回答…",
3233
- "refineAnswerPlaceholder": "回答を改善…",
3234
- "saveAnswer": "回答を保存",
3235
3245
  "dismissIrrelevant": "無関係として却下",
3236
3246
  "reopen": "再オープン",
3237
3247
  "docHeading": "明確化されたバグ報告",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "Wykonaj jeszcze jedną automatyczną rundę przeróbki, przejdź do następnego kroku akceptując bieżący wynik, albo zatrzymaj i zresetuj zadanie, aby edytować dane wejściowe i przesłać ponownie.",
964
964
  "infraAttempts": "Próby infrastrukturalne",
965
965
  "hideInfraAttempts": "Ukryj próby infrastrukturalne",
966
+ "executionHistory": "Historia wykonania",
967
+ "hideExecutionHistory": "Ukryj historię wykonania",
966
968
  "editingConclusions": "Edytowanie wniosków",
967
969
  "editConclusionsPlaceholder": "Edytuj wnioski agenta; Twoje zmiany zostaną zapisane po zatwierdzeniu…",
968
970
  "noProseOutput": "Ten agent nie wytworzył wyniku tekstowego.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "Prompt systemowy",
1097
1099
  "userPrompt": "Prompt użytkownika",
1098
1100
  "details": "Szczegóły"
1101
+ },
1102
+ "webSearch": "Wyszukiwanie w sieci",
1103
+ "loadingSearch": "Ładowanie wyszukań w sieci…",
1104
+ "noSearch": "Nie zarejestrowano wyszukań w sieci dla tego uruchomienia. Zapytania są rejestrowane, gdy wyszukiwanie w sieci jest uruchomione, a przestrzeń robocza ma włączone „Przechowuj pełny kontekst agenta”.",
1105
+ "search": {
1106
+ "available": "Dostępne",
1107
+ "unavailable": "Niedostępne",
1108
+ "provider": "Dostawca",
1109
+ "resultsCount": "{count} wynik | {count} wyniki | {count} wyników",
1110
+ "queriesTitle": "Wykonane wyszukiwania"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3218,8 +3230,6 @@
3218
3230
  "reReviewingStage": "Ponowna recenzja zaktualizowanego zgłoszenia błędu… Możesz to zamknąć, powiadomimy Cię tylko, jeśli potrzebne będą dodatkowe informacje.",
3219
3231
  "answerLabel": "Odpowiedź",
3220
3232
  "answerPlaceholder": "Odpowiedz na tę uwagę…",
3221
- "refineAnswerPlaceholder": "Dopracuj swoją odpowiedź…",
3222
- "saveAnswer": "Zapisz odpowiedź",
3223
3233
  "dismissIrrelevant": "Odrzuć jako nieistotne",
3224
3234
  "reopen": "Otwórz ponownie",
3225
3235
  "docHeading": "Doprecyzowane zgłoszenie błędu",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "Bir otomatik yeniden çalışma turu daha yapın, mevcut çıktıyı kabul ederek sonraki adıma geçin ya da durup görevi sıfırlayarak girdileri düzenleyip yeniden gönderin.",
964
964
  "infraAttempts": "Altyapı denemeleri",
965
965
  "hideInfraAttempts": "Altyapı denemelerini gizle",
966
+ "executionHistory": "Yürütme geçmişi",
967
+ "hideExecutionHistory": "Yürütme geçmişini gizle",
966
968
  "editingConclusions": "Sonuçlar düzenleniyor",
967
969
  "editConclusionsPlaceholder": "Aracının sonuçlarını düzenleyin; düzenlemeleriniz onayladığınızda kaydedilir…",
968
970
  "noProseOutput": "Bu aracı herhangi bir metin çıktısı üretmedi.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "Sistem istemi",
1097
1099
  "userPrompt": "Kullanıcı istemi",
1098
1100
  "details": "Ayrıntılar"
1101
+ },
1102
+ "webSearch": "Web araması",
1103
+ "loadingSearch": "Web aramaları yükleniyor…",
1104
+ "noSearch": "Bu çalıştırma için web araması kaydedilmedi. Sorgular, web araması çalıştığında ve çalışma alanında 'Tam aracı bağlamını depola' etkinleştirildiğinde yakalanır.",
1105
+ "search": {
1106
+ "available": "Kullanılabilir",
1107
+ "unavailable": "Kullanılamıyor",
1108
+ "provider": "Sağlayıcı",
1109
+ "resultsCount": "{count} sonuç | {count} sonuç",
1110
+ "queriesTitle": "Yapılan aramalar"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3230,8 +3242,6 @@
3230
3242
  "reReviewingStage": "Güncellenen hata raporu yeniden inceleniyor… Bunu kapatabilirsiniz, yalnızca daha fazla girdi gerekirse sizi bilgilendireceğiz.",
3231
3243
  "answerLabel": "Yanıt",
3232
3244
  "answerPlaceholder": "Bu bulguyu yanıtlayın…",
3233
- "refineAnswerPlaceholder": "Yanıtınızı iyileştirin…",
3234
- "saveAnswer": "Yanıtı kaydet",
3235
3245
  "dismissIrrelevant": "İlgisiz olarak göz ardı et",
3236
3246
  "reopen": "Yeniden aç",
3237
3247
  "docHeading": "Netleştirilmiş hata raporu",
@@ -963,6 +963,8 @@
963
963
  "companionCapDetail": "Виконайте ще один автоматичний раунд переробки, перейдіть до наступного кроку, прийнявши поточний вивід, або зупиніться та скиньте завдання, щоб відредагувати вхідні дані й надіслати повторно.",
964
964
  "infraAttempts": "Спроби інфраструктури",
965
965
  "hideInfraAttempts": "Приховати спроби інфраструктури",
966
+ "executionHistory": "Історія виконання",
967
+ "hideExecutionHistory": "Приховати історію виконання",
966
968
  "editingConclusions": "Редагування висновків",
967
969
  "editConclusionsPlaceholder": "Відредагуйте висновки агента; ваші зміни зберігаються після затвердження…",
968
970
  "noProseOutput": "Цей агент не створив текстового виводу.",
@@ -1096,6 +1098,16 @@
1096
1098
  "systemPrompt": "Системний запит",
1097
1099
  "userPrompt": "Запит користувача",
1098
1100
  "details": "Деталі"
1101
+ },
1102
+ "webSearch": "Веб-пошук",
1103
+ "loadingSearch": "Завантаження веб-пошуків…",
1104
+ "noSearch": "Для цього запуску не зафіксовано веб-пошуків. Запити фіксуються, коли виконується веб-пошук і в робочому просторі увімкнено «Зберігати повний контекст агента».",
1105
+ "search": {
1106
+ "available": "Доступно",
1107
+ "unavailable": "Недоступно",
1108
+ "provider": "Провайдер",
1109
+ "resultsCount": "{count} результат | {count} результати | {count} результатів",
1110
+ "queriesTitle": "Виконані пошуки"
1099
1111
  }
1100
1112
  },
1101
1113
  "auth": {
@@ -3218,8 +3230,6 @@
3218
3230
  "reReviewingStage": "Повторне рецензування оновленого звіту про помилку… Ви можете це закрити, ми сповістимо вас лише якщо знадобиться більше інформації.",
3219
3231
  "answerLabel": "Відповідь",
3220
3232
  "answerPlaceholder": "Дайте відповідь на це зауваження…",
3221
- "refineAnswerPlaceholder": "Уточніть свою відповідь…",
3222
- "saveAnswer": "Зберегти відповідь",
3223
3233
  "dismissIrrelevant": "Відхилити як недоречне",
3224
3234
  "reopen": "Відкрити знову",
3225
3235
  "docHeading": "Уточнений звіт про помилку",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.104.0"
37
+ "@cat-factory/contracts": "0.105.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",