@cat-factory/app 0.137.2 → 0.138.1

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.
@@ -8,7 +8,7 @@
8
8
  // the same seam the requirements / tester windows use.
9
9
  import { computed } from 'vue'
10
10
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
11
- import StepRestartControl from '~/components/panels/StepRestartControl.vue'
11
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
12
12
  import MarkdownProse from '~/components/common/MarkdownProse.vue'
13
13
  import CopyButton from '~/components/common/CopyButton.vue'
14
14
 
@@ -17,9 +17,12 @@ const execution = useExecutionStore()
17
17
  const agents = useAgentsStore()
18
18
  const { t } = useI18n()
19
19
 
20
- // Shared seam contract (open/blockId/close + Escape). No `onOpen` loader: this window reads
21
- // its data straight off the execution step, so there's nothing to fetch on open.
22
- const { open, blockId, instanceId, stepIndex, close } = useResultView('generic-structured')
20
+ // Shared seam contract (open/blockId/close). No `onOpen` loader: this window reads its data
21
+ // straight off the execution step, so there's nothing to fetch on open. `manageEscape: false`
22
+ // `ResultWindowShell` owns Escape (and focus trap + scroll lock + stacking).
23
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('generic-structured', {
24
+ manageEscape: false,
25
+ })
23
26
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
24
27
 
25
28
  const instance = computed(() =>
@@ -54,94 +57,63 @@ const customJson = computed<string | null>(() => {
54
57
  </script>
55
58
 
56
59
  <template>
57
- <Teleport to="body">
58
- <div
59
- v-if="open"
60
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
61
- @click.self="close"
62
- >
63
- <div
64
- class="m-4 flex w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
65
- role="dialog"
66
- aria-modal="true"
67
- >
68
- <!-- Header -->
69
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
70
- <span
71
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-cyan-500/15 text-cyan-300"
72
- >
73
- <UIcon :name="meta?.icon ?? 'i-lucide-braces'" class="h-4 w-4" />
74
- </span>
75
- <div class="min-w-0 flex-1">
76
- <h2 class="truncate text-sm font-semibold text-slate-100">
77
- {{ headerTitle }}
78
- </h2>
79
- <p class="truncate text-[11px] text-slate-400">
80
- {{ meta?.description ?? t('panels.structuredResult.fallbackDescription') }}
81
- </p>
82
- </div>
83
- <StepRestartControl
84
- :instance-id="instanceId"
85
- :step-index="stepIndex"
86
- @restarted="close"
87
- />
88
- <button
89
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
90
- @click="close"
91
- >
92
- <UIcon name="i-lucide-x" class="h-4 w-4" />
93
- </button>
94
- </header>
95
-
96
- <div class="flex min-h-0 flex-1">
97
- <!-- Main: prose summary + structured JSON -->
98
- <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
99
- <MarkdownProse
100
- v-if="step?.output"
101
- :text="step.output"
102
- class="mb-4 text-[13px] leading-relaxed text-slate-300"
103
- />
60
+ <ResultWindowShell
61
+ :open="open"
62
+ :icon="meta?.icon ?? 'i-lucide-braces'"
63
+ icon-class="bg-cyan-500/15 text-cyan-300"
64
+ :title="headerTitle"
65
+ :subtitle="meta?.description ?? t('panels.structuredResult.fallbackDescription')"
66
+ :step-ref="{ instanceId, stepIndex }"
67
+ width="4xl"
68
+ @close="close"
69
+ >
70
+ <div class="flex min-h-0 flex-1">
71
+ <!-- Main: prose summary + structured JSON -->
72
+ <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
73
+ <MarkdownProse
74
+ v-if="step?.output"
75
+ :text="step.output"
76
+ class="mb-4 text-[13px] leading-relaxed text-slate-300"
77
+ />
104
78
 
105
- <template v-if="customJson">
106
- <div class="mb-2 flex items-center gap-2">
107
- <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
108
- {{ t('panels.structuredResult.structuredOutput') }}
109
- </h3>
110
- <CopyButton :text="customJson" class="-my-1" />
111
- </div>
112
- <pre
113
- class="overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-3 text-[12px] leading-relaxed text-slate-200"
114
- ><code>{{ customJson }}</code></pre>
115
- </template>
116
-
117
- <div
118
- v-else-if="!step?.output"
119
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
120
- >
121
- <UIcon name="i-lucide-braces" class="h-8 w-8 opacity-40" />
122
- <p class="text-sm">{{ t('panels.structuredResult.noResult') }}</p>
123
- <p class="max-w-sm text-[11px] text-slate-500">
124
- {{ t('panels.structuredResult.noResultHint') }}
125
- </p>
126
- </div>
79
+ <template v-if="customJson">
80
+ <div class="mb-2 flex items-center gap-2">
81
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
82
+ {{ t('panels.structuredResult.structuredOutput') }}
83
+ </h3>
84
+ <CopyButton :text="customJson" class="-my-1" />
127
85
  </div>
86
+ <pre
87
+ class="overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-3 text-[12px] leading-relaxed text-slate-200"
88
+ ><code>{{ customJson }}</code></pre>
89
+ </template>
128
90
 
129
- <!-- Sidebar: shared run metadata + observability rollup -->
130
- <aside
131
- class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
132
- >
133
- <StepRunMeta
134
- v-if="step"
135
- :step="step"
136
- :instance-id="instanceId ?? undefined"
137
- :step-number="stepIndex === null ? undefined : stepIndex + 1"
138
- :total-steps="instance?.steps.length"
139
- :run-failed="instance?.status === 'failed'"
140
- :failure-at="instance?.failure?.occurredAt"
141
- />
142
- </aside>
91
+ <div
92
+ v-else-if="!step?.output"
93
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
94
+ >
95
+ <UIcon name="i-lucide-braces" class="h-8 w-8 opacity-40" />
96
+ <p class="text-sm">{{ t('panels.structuredResult.noResult') }}</p>
97
+ <p class="max-w-sm text-[11px] text-slate-500">
98
+ {{ t('panels.structuredResult.noResultHint') }}
99
+ </p>
143
100
  </div>
144
101
  </div>
102
+
103
+ <!-- Sidebar: shared run metadata + observability rollup -->
104
+ <aside
105
+ class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
106
+ >
107
+ <StepRunMeta
108
+ v-if="step"
109
+ :step="step"
110
+ :instance-id="instanceId ?? undefined"
111
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
112
+ :total-steps="instance?.steps.length"
113
+ :run-failed="instance?.status === 'failed'"
114
+ :failure-at="instance?.failure?.occurredAt"
115
+ />
116
+ </aside>
145
117
  </div>
146
- </Teleport>
118
+ </ResultWindowShell>
147
119
  </template>
@@ -7,7 +7,7 @@
7
7
  import { computed } from 'vue'
8
8
  import { agentKindMeta } from '~/utils/catalog'
9
9
  import type { RalphStepState } from '~/types/execution'
10
- import StepRestartControl from '~/components/panels/StepRestartControl.vue'
10
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
11
11
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
12
12
  import CopyButton from '~/components/common/CopyButton.vue'
13
13
 
@@ -15,8 +15,15 @@ const board = useBoardStore()
15
15
  const execution = useExecutionStore()
16
16
  const { t, d } = useI18n()
17
17
 
18
- const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop')
18
+ // `manageEscape: false` `ResultWindowShell` owns Escape (and focus trap + scroll lock +
19
+ // stacking) via the shared overlay behaviour.
20
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('ralph-loop', {
21
+ manageEscape: false,
22
+ })
19
23
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
24
+ const headerTitle = computed(
25
+ () => `${meta.value.label}${block.value ? ` — ${block.value.title}` : ''}`,
26
+ )
20
27
  const prUrl = computed(() => block.value?.pullRequest?.url ?? null)
21
28
 
22
29
  const instance = computed(() =>
@@ -88,185 +95,157 @@ const STATUS_META = computed<
88
95
  </script>
89
96
 
90
97
  <template>
91
- <Teleport to="body">
92
- <div
93
- v-if="open"
94
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
95
- @click.self="close"
96
- >
97
- <div
98
- class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
99
- role="dialog"
100
- aria-modal="true"
101
- data-testid="ralph-loop-window"
98
+ <ResultWindowShell
99
+ :open="open"
100
+ :icon="meta.icon"
101
+ icon-class="bg-violet-500/15 text-violet-300"
102
+ :title="headerTitle"
103
+ :subtitle="t('ralph.subtitle')"
104
+ :step-ref="{ instanceId, stepIndex }"
105
+ width="3xl"
106
+ testid="ralph-loop-window"
107
+ @close="close"
108
+ >
109
+ <template #header-extras>
110
+ <UBadge
111
+ :color="STATUS_META[status].badge"
112
+ variant="subtle"
113
+ size="sm"
114
+ data-testid="ralph-status"
102
115
  >
103
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
104
- <span
105
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500/15 text-violet-300"
106
- >
107
- <UIcon :name="meta.icon" class="h-4 w-4" />
108
- </span>
109
- <div class="min-w-0 flex-1">
110
- <h2 class="truncate text-sm font-semibold text-slate-100">
111
- {{ meta.label }}{{ block ? ` — ${block.title}` : '' }}
112
- </h2>
113
- <p class="truncate text-[11px] text-slate-400">{{ t('ralph.subtitle') }}</p>
116
+ {{ STATUS_META[status].label }}
117
+ </UBadge>
118
+ </template>
119
+ <div class="flex min-h-0 flex-1">
120
+ <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
121
+ <div
122
+ v-if="!ralph"
123
+ class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
124
+ >
125
+ <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
126
+ <p class="text-sm">{{ t('ralph.noActivity') }}</p>
127
+ </div>
128
+
129
+ <template v-else>
130
+ <!-- The completion criterion. -->
131
+ <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
132
+ {{ t('ralph.validationCommand') }}
133
+ </h3>
134
+ <div class="relative rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2">
135
+ <CopyButton :text="ralph.validationCommand" class="absolute end-1 top-1" />
136
+ <code class="block whitespace-pre-wrap pe-8 font-mono text-[12px] text-slate-200">{{
137
+ ralph.validationCommand
138
+ }}</code>
114
139
  </div>
115
- <UBadge
116
- :color="STATUS_META[status].badge"
117
- variant="subtle"
118
- size="sm"
119
- data-testid="ralph-status"
120
- >
121
- {{ STATUS_META[status].label }}
122
- </UBadge>
123
- <StepRestartControl
124
- :instance-id="instanceId"
125
- :step-index="stepIndex"
126
- @restarted="close"
127
- />
128
- <button
129
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
130
- @click="close"
131
- >
132
- <UIcon name="i-lucide-x" class="h-4 w-4" />
133
- </button>
134
- </header>
135
140
 
136
- <div class="flex min-h-0 flex-1">
137
- <div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
138
- <div
139
- v-if="!ralph"
140
- class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
141
+ <!-- The most recent validation output. -->
142
+ <template v-if="ralph.lastValidationTail">
143
+ <h3
144
+ class="mb-1.5 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
141
145
  >
142
- <UIcon :name="meta.icon" class="h-8 w-8 opacity-40" />
143
- <p class="text-sm">{{ t('ralph.noActivity') }}</p>
146
+ {{ t('ralph.lastOutput', { exit: ralph.lastExitCode ?? '?' }) }}
147
+ </h3>
148
+ <div class="relative rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2">
149
+ <CopyButton :text="ralph.lastValidationTail" class="absolute end-1 top-1" />
150
+ <pre
151
+ class="whitespace-pre-wrap pe-8 font-mono text-[11px] leading-relaxed text-slate-400"
152
+ >{{ ralph.lastValidationTail }}</pre
153
+ >
144
154
  </div>
155
+ </template>
145
156
 
146
- <template v-else>
147
- <!-- The completion criterion. -->
148
- <h3 class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
149
- {{ t('ralph.validationCommand') }}
150
- </h3>
151
- <div class="relative rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2">
152
- <CopyButton :text="ralph.validationCommand" class="absolute end-1 top-1" />
153
- <code class="block whitespace-pre-wrap pe-8 font-mono text-[12px] text-slate-200">{{
154
- ralph.validationCommand
155
- }}</code>
156
- </div>
157
-
158
- <!-- The most recent validation output. -->
159
- <template v-if="ralph.lastValidationTail">
160
- <h3
161
- class="mb-1.5 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
162
- >
163
- {{ t('ralph.lastOutput', { exit: ralph.lastExitCode ?? '?' }) }}
164
- </h3>
165
- <div class="relative rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2">
166
- <CopyButton :text="ralph.lastValidationTail" class="absolute end-1 top-1" />
167
- <pre
168
- class="whitespace-pre-wrap pe-8 font-mono text-[11px] leading-relaxed text-slate-400"
169
- >{{ ralph.lastValidationTail }}</pre
170
- >
171
- </div>
172
- </template>
157
+ <a
158
+ v-if="prUrl"
159
+ :href="prUrl"
160
+ target="_blank"
161
+ rel="noopener"
162
+ class="mt-3 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
163
+ >
164
+ {{ t('ralph.viewPr') }}
165
+ <UIcon name="i-lucide-external-link" class="h-3 w-3" />
166
+ </a>
173
167
 
174
- <a
175
- v-if="prUrl"
176
- :href="prUrl"
177
- target="_blank"
178
- rel="noopener"
179
- class="mt-3 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200 hover:underline"
168
+ <!-- Iteration history: what each pass produced and whether its validation passed. -->
169
+ <section v-if="attempts.length" class="mt-5">
170
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
171
+ {{ t('ralph.iterationsHeading') }}
172
+ </h3>
173
+ <ol class="space-y-2">
174
+ <li
175
+ v-for="a in attempts"
176
+ :key="a.attempt"
177
+ class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
178
+ data-testid="ralph-iteration"
180
179
  >
181
- {{ t('ralph.viewPr') }}
182
- <UIcon name="i-lucide-external-link" class="h-3 w-3" />
183
- </a>
184
-
185
- <!-- Iteration history: what each pass produced and whether its validation passed. -->
186
- <section v-if="attempts.length" class="mt-5">
187
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
188
- {{ t('ralph.iterationsHeading') }}
189
- </h3>
190
- <ol class="space-y-2">
191
- <li
192
- v-for="a in attempts"
193
- :key="a.attempt"
194
- class="rounded-md border border-slate-800 bg-slate-950/40 px-3 py-2"
195
- data-testid="ralph-iteration"
196
- >
197
- <div class="flex items-center gap-2">
198
- <UIcon
199
- :name="a.validationPassed ? 'i-lucide-circle-check' : 'i-lucide-circle-x'"
200
- class="h-3.5 w-3.5"
201
- :class="a.validationPassed ? 'text-emerald-400' : 'text-rose-400'"
202
- />
203
- <span class="text-[12px] font-medium text-slate-200">
204
- {{ t('ralph.iteration', { number: a.attempt }) }}
205
- </span>
206
- <span class="text-[11px] text-slate-500">
207
- {{
208
- a.validationPassed
209
- ? t('ralph.iterationPassed')
210
- : t('ralph.iterationFailed', { exit: a.exitCode ?? '?' })
211
- }}
212
- </span>
213
- <span class="ms-auto text-[10px] text-slate-600">{{
214
- d(new Date(a.at), 'long')
215
- }}</span>
216
- </div>
217
- <p
218
- v-if="a.summary"
219
- class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
220
- >
221
- {{ a.summary }}
222
- </p>
223
- </li>
224
- </ol>
225
- </section>
226
- </template>
227
- </div>
180
+ <div class="flex items-center gap-2">
181
+ <UIcon
182
+ :name="a.validationPassed ? 'i-lucide-circle-check' : 'i-lucide-circle-x'"
183
+ class="h-3.5 w-3.5"
184
+ :class="a.validationPassed ? 'text-emerald-400' : 'text-rose-400'"
185
+ />
186
+ <span class="text-[12px] font-medium text-slate-200">
187
+ {{ t('ralph.iteration', { number: a.attempt }) }}
188
+ </span>
189
+ <span class="text-[11px] text-slate-500">
190
+ {{
191
+ a.validationPassed
192
+ ? t('ralph.iterationPassed')
193
+ : t('ralph.iterationFailed', { exit: a.exitCode ?? '?' })
194
+ }}
195
+ </span>
196
+ <span class="ms-auto text-[10px] text-slate-600">{{
197
+ d(new Date(a.at), 'long')
198
+ }}</span>
199
+ </div>
200
+ <p
201
+ v-if="a.summary"
202
+ class="mt-1 whitespace-pre-wrap text-[12px] leading-relaxed text-slate-400"
203
+ >
204
+ {{ a.summary }}
205
+ </p>
206
+ </li>
207
+ </ol>
208
+ </section>
209
+ </template>
210
+ </div>
228
211
 
229
- <aside
230
- class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
231
- >
232
- <div v-if="ralph">
233
- <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
234
- {{ t('ralph.sidebar.state') }}
235
- </h4>
236
- <div class="flex items-center gap-2 text-[13px]">
237
- <UIcon
238
- :name="STATUS_META[status].icon"
239
- class="h-4 w-4"
240
- :class="STATUS_META[status].text"
241
- />
242
- <span :class="STATUS_META[status].text">{{ STATUS_META[status].label }}</span>
243
- </div>
244
- </div>
245
- <div v-if="ralph">
246
- <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
247
- {{ t('ralph.sidebar.iterations') }}
248
- </h4>
249
- <p class="text-[12px] text-slate-300" data-testid="ralph-iteration-count">
250
- {{
251
- t('ralph.sidebar.count', { attempts: ralph.attempts, max: ralph.maxIterations })
252
- }}
253
- </p>
254
- </div>
255
- <StepRunMeta
256
- v-if="step"
257
- :step="step"
258
- :instance-id="instanceId ?? undefined"
259
- :step-number="stepIndex === null ? undefined : stepIndex + 1"
260
- :total-steps="instance?.steps.length"
261
- :run-failed="instance?.status === 'failed'"
262
- :failure-at="instance?.failure?.occurredAt"
212
+ <aside
213
+ class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
214
+ >
215
+ <div v-if="ralph">
216
+ <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
217
+ {{ t('ralph.sidebar.state') }}
218
+ </h4>
219
+ <div class="flex items-center gap-2 text-[13px]">
220
+ <UIcon
221
+ :name="STATUS_META[status].icon"
222
+ class="h-4 w-4"
223
+ :class="STATUS_META[status].text"
263
224
  />
264
- <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
265
- {{ t('ralph.sidebar.footer') }}
266
- </p>
267
- </aside>
225
+ <span :class="STATUS_META[status].text">{{ STATUS_META[status].label }}</span>
226
+ </div>
268
227
  </div>
269
- </div>
228
+ <div v-if="ralph">
229
+ <h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
230
+ {{ t('ralph.sidebar.iterations') }}
231
+ </h4>
232
+ <p class="text-[12px] text-slate-300" data-testid="ralph-iteration-count">
233
+ {{ t('ralph.sidebar.count', { attempts: ralph.attempts, max: ralph.maxIterations }) }}
234
+ </p>
235
+ </div>
236
+ <StepRunMeta
237
+ v-if="step"
238
+ :step="step"
239
+ :instance-id="instanceId ?? undefined"
240
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
241
+ :total-steps="instance?.steps.length"
242
+ :run-failed="instance?.status === 'failed'"
243
+ :failure-at="instance?.failure?.occurredAt"
244
+ />
245
+ <p class="mt-auto text-[10px] leading-relaxed text-slate-600">
246
+ {{ t('ralph.sidebar.footer') }}
247
+ </p>
248
+ </aside>
270
249
  </div>
271
- </Teleport>
250
+ </ResultWindowShell>
272
251
  </template>