@cat-factory/app 0.151.0 → 0.152.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.
@@ -17,9 +17,14 @@
17
17
  // The pick-one SELECTION of which window is active stays exactly the slice-2
18
18
  // `resolveComponentRegistry` in `StepResultViewHost.vue` — this shell only owns the
19
19
  // per-window chrome + behaviour, so windows convert one at a time behind it.
20
- import { computed } from 'vue'
20
+ //
21
+ // It also owns the one trailing section every step-backed window shows: the agent's effort
22
+ // self-assessment (see the footer block below), so a window never renders it itself.
23
+ import { computed, ref } from 'vue'
21
24
  import { useModalBehavior } from '@modular-vue/core'
22
25
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
26
+ import StepEffortReport from '~/components/panels/StepEffortReport.vue'
27
+ import { effortBand, effortHint } from '~/utils/effort'
23
28
 
24
29
  /** A pipeline step reference — passed by step-result windows to surface the shared
25
30
  * "restart from here" control. `StepRestartControl` self-hides for an off-path open
@@ -72,6 +77,32 @@ const { dialogRef } = useModalBehavior({
72
77
  onClose: requestClose,
73
78
  })
74
79
 
80
+ // The active step's effort self-assessment (how hard the work was, what reduced its
81
+ // effectiveness, the obstacles it hit), rendered as a collapsible footer under EVERY window.
82
+ // Resolved from the result-view seam itself rather than a per-window prop: the host mounts
83
+ // exactly one window — the active `ui.resultView` — so a window can't opt out, forget to pass
84
+ // it, or drift in where it puts it. An off-path open (a block-keyed window with no step) and a
85
+ // step whose agent wrote no report both resolve to null, and the footer disappears.
86
+ const ui = useUiStore()
87
+ const execution = useExecutionStore()
88
+ const effortReport = computed(() => {
89
+ const view = ui.resultView
90
+ if (!view || view.instanceId === null || view.stepIndex === null) return null
91
+ return execution.getInstance(view.instanceId)?.steps[view.stepIndex]?.effortReport ?? null
92
+ })
93
+ // Collapsed by default — the windows own the vertical space, and the row already carries the
94
+ // difficulty plus the gist of what held the agent back.
95
+ const effortOpen = ref(false)
96
+ const hint = computed(() => (effortReport.value ? effortHint(effortReport.value) : null))
97
+ const CHIP_CLASS = {
98
+ easy: 'bg-emerald-500/15 text-emerald-300',
99
+ moderate: 'bg-amber-500/15 text-amber-300',
100
+ hard: 'bg-rose-500/15 text-rose-300',
101
+ } as const
102
+ const chipClass = computed(() =>
103
+ effortReport.value ? CHIP_CLASS[effortBand(effortReport.value.difficulty)] : '',
104
+ )
105
+
75
106
  const WIDTH: Record<'3xl' | '4xl' | '5xl', string> = {
76
107
  '3xl': 'max-w-3xl',
77
108
  '4xl': 'max-w-4xl',
@@ -135,6 +166,44 @@ const panelClass = computed(() => [
135
166
  </header>
136
167
  <!-- The window body. -->
137
168
  <slot />
169
+
170
+ <!-- Shared trailing section: the container agent's effort self-assessment, under the
171
+ window's own detail. Collapsed to a one-line row (difficulty + what held it back)
172
+ so it can't crowd a window out; expands in place. -->
173
+ <section
174
+ v-if="effortReport"
175
+ class="shrink-0 border-t border-slate-800 bg-slate-900/60"
176
+ data-testid="result-window-effort"
177
+ >
178
+ <button
179
+ type="button"
180
+ class="flex w-full items-center gap-2 px-5 py-2 text-start hover:bg-slate-800/40"
181
+ :aria-expanded="effortOpen"
182
+ data-testid="result-window-effort-toggle"
183
+ @click="effortOpen = !effortOpen"
184
+ >
185
+ <UIcon name="i-lucide-gauge" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
186
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
187
+ {{ t('panels.stepDetail.effort.heading') }}
188
+ </span>
189
+ <span
190
+ class="shrink-0 rounded px-1.5 py-0.5 text-[11px] font-medium tabular-nums"
191
+ :class="chipClass"
192
+ >
193
+ {{ t('panels.stepDetail.effort.outOfTen', { value: effortReport.difficulty }) }}
194
+ </span>
195
+ <span v-if="hint" class="min-w-0 flex-1 truncate text-[12px] text-slate-400">
196
+ {{ hint }}
197
+ </span>
198
+ <UIcon
199
+ :name="effortOpen ? 'i-lucide-chevron-down' : 'i-lucide-chevron-up'"
200
+ class="ms-auto h-3.5 w-3.5 shrink-0 text-slate-500"
201
+ />
202
+ </button>
203
+ <div v-if="effortOpen" class="max-h-56 overflow-y-auto px-5 pb-3">
204
+ <StepEffortReport :report="effortReport" variant="flat" />
205
+ </div>
206
+ </section>
138
207
  </div>
139
208
  </div>
140
209
  </Teleport>
@@ -3,9 +3,18 @@
3
3
  // (1..10), what reduced its effectiveness, and the key obstacles it hit. Populated by the harness
4
4
  // from the agent's sentinel file and recorded on the step (`step.effortReport`). Rendered only when
5
5
  // present, so a run on an older harness image (or an agent that wrote none) shows nothing.
6
+ //
7
+ // Two callers, one renderer: the generic step-detail panel drops it in as a `card` (its own
8
+ // heading + border, sitting among the other detail sections), and `ResultWindowShell`'s
9
+ // collapsible footer embeds it `flat` — the disclosure row is already the heading there, so a
10
+ // second one plus a nested border would just be chrome inside chrome.
6
11
  import type { AgentEffortReport } from '~/types/execution'
12
+ import { effortBand } from '~/utils/effort'
7
13
 
8
- const props = defineProps<{ report: AgentEffortReport }>()
14
+ const props = withDefaults(
15
+ defineProps<{ report: AgentEffortReport; variant?: 'card' | 'flat' }>(),
16
+ { variant: 'card' },
17
+ )
9
18
  const { t } = useI18n()
10
19
 
11
20
  // Clamp for the bar width; the schema already bounds 1..10 but be defensive against a stray value.
@@ -13,21 +22,19 @@ const difficultyPct = computed(() =>
13
22
  Math.min(100, Math.max(0, (props.report.difficulty / 10) * 100)),
14
23
  )
15
24
  // Colour the difficulty by band: easy (emerald) → moderate (amber) → hard (rose).
16
- const difficultyClass = computed(() =>
17
- props.report.difficulty >= 8
18
- ? 'bg-rose-400'
19
- : props.report.difficulty >= 5
20
- ? 'bg-amber-400'
21
- : 'bg-emerald-400',
22
- )
25
+ const BAR_CLASS = { easy: 'bg-emerald-400', moderate: 'bg-amber-400', hard: 'bg-rose-400' } as const
26
+ const difficultyClass = computed(() => BAR_CLASS[effortBand(props.report.difficulty)])
23
27
  </script>
24
28
 
25
29
  <template>
26
30
  <section
27
31
  data-testid="step-effort-report"
28
- class="scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4"
32
+ :class="
33
+ variant === 'card' ? 'scroll-mt-4 rounded-xl border border-slate-800 bg-slate-900/50 p-4' : ''
34
+ "
29
35
  >
30
36
  <div
37
+ v-if="variant === 'card'"
31
38
  class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
32
39
  >
33
40
  <UIcon name="i-lucide-gauge" class="h-3.5 w-3.5" />
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { effortBand, effortHint } from './effort'
3
+
4
+ describe('effortBand', () => {
5
+ it('bands the 1..10 self-rating at the shared thresholds', () => {
6
+ expect(effortBand(1)).toBe('easy')
7
+ expect(effortBand(4)).toBe('easy')
8
+ expect(effortBand(5)).toBe('moderate')
9
+ expect(effortBand(7)).toBe('moderate')
10
+ expect(effortBand(8)).toBe('hard')
11
+ expect(effortBand(10)).toBe('hard')
12
+ })
13
+ })
14
+
15
+ describe('effortHint', () => {
16
+ it('prefers what held the agent back over the work summary', () => {
17
+ expect(
18
+ effortHint({
19
+ difficulty: 7,
20
+ summary: 'Refactored the parser.',
21
+ reducedEffectiveness: 'Flaky test tooling.',
22
+ }),
23
+ ).toBe('Flaky test tooling.')
24
+ })
25
+
26
+ it('falls back to the summary, then to nothing', () => {
27
+ expect(effortHint({ difficulty: 3, summary: 'Straightforward.' })).toBe('Straightforward.')
28
+ expect(effortHint({ difficulty: 3 })).toBeNull()
29
+ })
30
+ })
@@ -0,0 +1,29 @@
1
+ import type { AgentEffortReport } from '~/types/execution'
2
+
3
+ /**
4
+ * Presentation helpers for a container agent's effort self-assessment
5
+ * (`PipelineStep.effortReport` — how hard the work was, what reduced its effectiveness,
6
+ * the obstacles it hit). Shared by the two surfaces that render it: the full card in the
7
+ * generic step-detail panel (`StepEffortReport.vue`) and the collapsible footer every
8
+ * dedicated result window gets from `ResultWindowShell.vue`. The band thresholds live
9
+ * here so the two can't drift into disagreeing about what counts as a hard run.
10
+ */
11
+
12
+ /** The difficulty band a 1..10 self-rating falls into. */
13
+ export type EffortBand = 'easy' | 'moderate' | 'hard'
14
+
15
+ /** Band a report's difficulty: 1-4 easy, 5-7 moderate, 8-10 hard. */
16
+ export function effortBand(difficulty: number): EffortBand {
17
+ if (difficulty >= 8) return 'hard'
18
+ if (difficulty >= 5) return 'moderate'
19
+ return 'easy'
20
+ }
21
+
22
+ /**
23
+ * The one-line gist for a collapsed footer row: what held the agent back, else its
24
+ * summary of the work. Null when the agent rated the run but wrote no prose, so the
25
+ * row falls back to the difficulty chip alone.
26
+ */
27
+ export function effortHint(report: AgentEffortReport): string | null {
28
+ return report.reducedEffectiveness ?? report.summary ?? null
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.151.0",
3
+ "version": "0.152.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",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.159.0"
43
+ "@cat-factory/contracts": "0.160.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",