@cat-factory/app 0.96.0 → 0.96.2

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.
@@ -1,36 +1,44 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onBeforeUnmount, ref, watch } from 'vue'
3
3
 
4
- // A slim top strip shown when the real-time WebSocket has dropped and is reconnecting, so a
4
+ // A slim top strip shown when the real-time WebSocket isn't delivering events, so a
5
5
  // silently-frozen board (events stop arriving, nothing updates) is no longer indistinguishable
6
- // from an idle one. `useWorkspaceStream` already reconnects with exponential backoff and
7
- // resyncs on reconnect this only makes that state visible. The `connected` ref is passed in
8
- // as a prop (the page owns the single stream instance; creating another here would open a
9
- // second socket).
10
- const props = defineProps<{ connected: boolean }>()
6
+ // from an idle one. Two states:
7
+ // - RE-connecting: we were live and the socket dropped (`useWorkspaceStream` reconnects with
8
+ // exponential backoff and resyncs on reconnect this just makes that state visible).
9
+ // - Offline: the very first handshake keeps failing (`connectionFailed`), so the board loaded
10
+ // over REST but will never go live — a user watching a run would otherwise see a frozen
11
+ // board with no hint why.
12
+ // The `connected` / `everConnected` / `connectionFailed` refs are passed in as props (the page
13
+ // owns the single stream instance; creating another here would open a second socket).
14
+ const props = defineProps<{
15
+ connected: boolean
16
+ everConnected: boolean
17
+ connectionFailed: boolean
18
+ }>()
11
19
 
12
20
  const { t } = useI18n()
13
21
 
14
- // Only surface a RE-connection, never the initial connect: once we've been connected we know a
15
- // later drop is a real interruption worth flagging. A short debounce rides out a quick socket
16
- // flap so a momentary blip doesn't flash the strip.
17
- const everConnected = ref(false)
22
+ // A short debounce rides out a quick socket flap so a momentary blip doesn't flash the strip.
18
23
  const showAfterDelay = ref(false)
19
24
  let timer: ReturnType<typeof setTimeout> | null = null
20
25
 
26
+ function clearTimer() {
27
+ if (timer) {
28
+ clearTimeout(timer)
29
+ timer = null
30
+ }
31
+ }
32
+
21
33
  watch(
22
34
  () => props.connected,
23
35
  (connected) => {
24
36
  if (connected) {
25
- everConnected.value = true
26
37
  showAfterDelay.value = false
27
- if (timer) {
28
- clearTimeout(timer)
29
- timer = null
30
- }
38
+ clearTimer()
31
39
  return
32
40
  }
33
- if (!everConnected.value || timer) return
41
+ if (timer) return
34
42
  timer = setTimeout(() => {
35
43
  showAfterDelay.value = true
36
44
  timer = null
@@ -39,24 +47,24 @@ watch(
39
47
  { immediate: true },
40
48
  )
41
49
 
42
- const visible = computed(() => everConnected.value && !props.connected && showAfterDelay.value)
50
+ // Reconnection: only surface once we've been connected a later drop is a real interruption.
51
+ const reconnecting = computed(() => props.everConnected && !props.connected && showAfterDelay.value)
52
+ // Offline: never connected and repeated attempts failed. No debounce — it already took several
53
+ // backoff cycles to flag, so it's not a flap.
54
+ const offline = computed(() => props.connectionFailed && !props.connected && !props.everConnected)
43
55
 
44
56
  // Don't leave a pending debounce timer firing into a torn-down component.
45
- onBeforeUnmount(() => {
46
- if (timer) {
47
- clearTimeout(timer)
48
- timer = null
49
- }
50
- })
57
+ onBeforeUnmount(clearTimer)
51
58
  </script>
52
59
 
53
60
  <template>
54
61
  <Transition name="fade">
55
62
  <div
56
- v-if="visible"
63
+ v-if="reconnecting || offline"
57
64
  class="pointer-events-none absolute inset-x-0 top-0 z-50 flex justify-center px-4 pt-2"
58
65
  >
59
66
  <div
67
+ v-if="reconnecting"
60
68
  class="pointer-events-auto flex items-center gap-2 rounded-full border border-amber-500/60 bg-amber-950/90 px-3 py-1.5 text-xs text-amber-100 shadow-lg backdrop-blur"
61
69
  role="status"
62
70
  aria-live="polite"
@@ -65,6 +73,16 @@ onBeforeUnmount(() => {
65
73
  <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin" />
66
74
  <span>{{ t('app.reconnecting') }}</span>
67
75
  </div>
76
+ <div
77
+ v-else
78
+ class="pointer-events-auto flex items-center gap-2 rounded-full border border-rose-500/60 bg-rose-950/90 px-3 py-1.5 text-xs text-rose-100 shadow-lg backdrop-blur"
79
+ role="status"
80
+ aria-live="polite"
81
+ data-testid="stream-offline"
82
+ >
83
+ <UIcon name="i-lucide-wifi-off" class="h-3.5 w-3.5" />
84
+ <span>{{ t('app.offline') }}</span>
85
+ </div>
68
86
  </div>
69
87
  </Transition>
70
88
  </template>
@@ -37,6 +37,16 @@ const exporting = computed(
37
37
  const error = computed(() =>
38
38
  executionId.value ? (observability.errors[executionId.value] ?? null) : null,
39
39
  )
40
+ const contextError = computed(() =>
41
+ executionId.value ? (observability.contextErrors[executionId.value] ?? null) : null,
42
+ )
43
+
44
+ function retryCalls() {
45
+ if (executionId.value) void observability.load(executionId.value)
46
+ }
47
+ function retryContext() {
48
+ if (executionId.value) void observability.loadContext(executionId.value)
49
+ }
40
50
 
41
51
  // Which view is shown: per-call model activity, the complete provided context, or the
42
52
  // performed web searches.
@@ -333,12 +343,22 @@ function exportJson() {
333
343
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
334
344
  {{ t('observability.loadingActivity') }}
335
345
  </p>
336
- <p
346
+ <div
337
347
  v-else-if="error"
338
- class="rounded-lg border border-dashed border-rose-900/60 py-6 text-center text-sm text-rose-400"
348
+ class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-rose-900/60 py-6 text-center text-sm text-rose-400"
339
349
  >
340
350
  {{ error }}
341
- </p>
351
+ <UButton
352
+ icon="i-lucide-rotate-cw"
353
+ color="neutral"
354
+ variant="soft"
355
+ size="xs"
356
+ :loading="loading"
357
+ @click="retryCalls"
358
+ >
359
+ {{ t('common.retry') }}
360
+ </UButton>
361
+ </div>
342
362
  <p
343
363
  v-else-if="!calls.length"
344
364
  class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
@@ -493,6 +513,22 @@ function exportJson() {
493
513
  <UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
494
514
  {{ t('observability.loadingContext') }}
495
515
  </p>
516
+ <div
517
+ v-else-if="contextError && !contextSnapshots.length"
518
+ class="flex flex-col items-center gap-3 rounded-lg border border-dashed border-rose-900/60 py-8 text-center text-sm text-rose-400"
519
+ >
520
+ {{ t('observability.contextError') }}
521
+ <UButton
522
+ icon="i-lucide-rotate-cw"
523
+ color="neutral"
524
+ variant="soft"
525
+ size="xs"
526
+ :loading="contextLoading"
527
+ @click="retryContext"
528
+ >
529
+ {{ t('common.retry') }}
530
+ </UButton>
531
+ </div>
496
532
  <p
497
533
  v-else-if="!contextSnapshots.length"
498
534
  class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
@@ -88,6 +88,11 @@ function selectGroup(m: number, g: number) {
88
88
  selected.value = { m, g }
89
89
  }
90
90
 
91
+ // Re-fetch after a load failure — the only escape used to be close-and-reopen.
92
+ function retry() {
93
+ if (blockId.value) void serviceSpec.load(blockId.value)
94
+ }
95
+
91
96
  // Exhaustive priority → label/chip map. Literal `t()` keys keep the typed-key drift
92
97
  // guard live, vs a runtime-built `spec.priority.${value}`.
93
98
  const PRIORITY_META: Record<RequirementPriority, { label: string; chip: string }> = {
@@ -187,10 +192,20 @@ function kindLabel(item: RequirementItem): string {
187
192
  <!-- error -->
188
193
  <div
189
194
  v-else-if="errored"
190
- class="flex flex-1 flex-col items-center justify-center gap-2 p-8 text-center text-sm text-slate-400"
195
+ class="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center text-sm text-slate-400"
191
196
  >
192
197
  <UIcon name="i-lucide-triangle-alert" class="h-6 w-6 text-amber-400" />
193
198
  {{ t('spec.error') }}
199
+ <UButton
200
+ icon="i-lucide-rotate-cw"
201
+ color="neutral"
202
+ variant="soft"
203
+ size="xs"
204
+ :loading="loading"
205
+ @click="retry"
206
+ >
207
+ {{ t('common.retry') }}
208
+ </UButton>
194
209
  </div>
195
210
 
196
211
  <!-- empty: no spec on the repo's default branch yet -->
@@ -93,6 +93,9 @@ export function usePipelineErrorToast() {
93
93
  : (conflict.message ?? t('errors.conflict.fallbackMessage')),
94
94
  color: 'error',
95
95
  icon: 'i-lucide-cpu',
96
+ // Stay until dismissed: an actionable toast whose remedy button vanishes on the ~5s
97
+ // auto-dismiss takes the one-click fix with it before the user can reach it.
98
+ duration: 0,
96
99
  actions: [
97
100
  {
98
101
  label: t('errors.conflict.providersUnconfigured.action'),
@@ -117,6 +120,9 @@ export function usePipelineErrorToast() {
117
120
  : (conflict.message ?? t('errors.conflict.fallbackMessage')),
118
121
  color: 'error',
119
122
  icon: 'i-lucide-image',
123
+ // Sticky, like the providers-unconfigured toast above: keep the "Configure storage"
124
+ // remedy reachable instead of letting it auto-dismiss.
125
+ duration: 0,
120
126
  actions: [
121
127
  {
122
128
  label: t('errors.conflict.binaryStorageUnconfigured.action'),
@@ -31,6 +31,17 @@ export function useWorkspaceStream() {
31
31
  const apiBase = useRuntimeConfig().public.apiBase
32
32
 
33
33
  const connected = ref(false)
34
+ // Have we EVER been fully live (connected AND reconciled) for the current workspace? Drives the
35
+ // "reconnecting" vs "never connected" distinction in the banner. Set together with `connected`
36
+ // AFTER the on-open resync settles — NOT at `onopen` — so the initial resync window (socket open
37
+ // but not yet announced) can't be mistaken for a re-connection and flash the amber banner.
38
+ const everConnected = ref(false)
39
+ // The very first handshake keeps failing (proxy/firewall blocks WS while REST works, or the
40
+ // ticket mint throws) — the board loaded over REST but will never go live. Flagged after a
41
+ // few failed attempts so the banner can say "not receiving live updates" instead of nothing.
42
+ const connectionFailed = ref(false)
43
+ // Failed connect attempts before we ever go live gates the offline flag above.
44
+ const INITIAL_FAIL_ATTEMPTS = 3
34
45
 
35
46
  let socket: WebSocket | null = null
36
47
  let stopped = false
@@ -41,9 +52,29 @@ export function useWorkspaceStream() {
41
52
  // http→ws, https→wss (apiBase is an absolute origin, see nuxt.config.ts).
42
53
  const wsBase = String(apiBase).replace(/^http/, 'ws')
43
54
 
55
+ // A coarse board refresh (the resync on reconnect, and the `board` event fan-out) must not be
56
+ // left silently stale by ONE transient failure: retry a few times with backoff so a blip
57
+ // self-heals. Bounded (the socket-level reconnect + the offline banner are the backstop for a
58
+ // genuine outage). Aborts between attempts if the stream stopped or the workspace switched.
59
+ const REFRESH_MAX_ATTEMPTS = 4
60
+ const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
61
+ async function refreshWithRetry(workspaceId: string): Promise<void> {
62
+ for (let i = 0; i < REFRESH_MAX_ATTEMPTS; i++) {
63
+ if (stopped || workspace.workspaceId !== workspaceId) return
64
+ try {
65
+ await workspace.refresh()
66
+ return
67
+ } catch {
68
+ if (i < REFRESH_MAX_ATTEMPTS - 1) await sleep(Math.min(4_000, 400 * 2 ** i))
69
+ }
70
+ }
71
+ }
72
+
44
73
  function debouncedBoardRefresh() {
74
+ const workspaceId = workspace.workspaceId
75
+ if (!workspaceId) return
45
76
  if (boardDebounce) clearTimeout(boardDebounce)
46
- boardDebounce = setTimeout(() => void workspace.refresh(), 300)
77
+ boardDebounce = setTimeout(() => void refreshWithRetry(workspaceId), 300)
47
78
  }
48
79
 
49
80
  function onMessage(raw: string) {
@@ -143,6 +174,7 @@ export function useWorkspaceStream() {
143
174
 
144
175
  socket.onopen = () => {
145
176
  attempt = 0
177
+ connectionFailed.value = false
146
178
  // Resync on (re)connect BEFORE announcing `connected`: any event missed while
147
179
  // disconnected is reconciled first. The snapshot carries `bootstrapJobs` +
148
180
  // executions, so one refresh rehydrates agentRuns too — a missed terminal event
@@ -157,18 +189,20 @@ export function useWorkspaceStream() {
157
189
  // live "bootstrapping…" badge flickers out with no further board event to restore
158
190
  // it. Anything acting on a `connected` board (a user, or an e2e spec gating on
159
191
  // `data-connected`) then does so only after this reconcile, so a lagging resync
160
- // can't drop the state that action produces. `connected` is still set on failure
161
- // (we ARE connected; a transient refresh error must not wedge the indicator/tests).
162
- void workspace
163
- .refresh()
164
- .catch(() => {})
165
- .finally(() => {
166
- // A workspace switch (or stop()) may have happened while the refresh was in
167
- // flight don't announce a connection for a socket we've since abandoned.
168
- if (!stopped && socket && workspace.workspaceId === workspaceId) {
169
- connected.value = true
170
- }
171
- })
192
+ // can't drop the state that action produces. The resync RETRIES on a transient
193
+ // failure (`refreshWithRetry`) so a reconnect no longer presents as fully live while
194
+ // silently missing everything from the outage; `connected` is still set even if every
195
+ // retry fails (we ARE connected; a refresh error must not wedge the indicator/tests).
196
+ void refreshWithRetry(workspaceId).finally(() => {
197
+ // A workspace switch (or stop()) may have happened while the refresh was in
198
+ // flight don't announce a connection for a socket we've since abandoned.
199
+ if (!stopped && socket && workspace.workspaceId === workspaceId) {
200
+ // Flip `everConnected` here (not at onopen): only now are we "fully live", so a later
201
+ // drop reads as a real re-connection while this initial resync window does not.
202
+ everConnected.value = true
203
+ connected.value = true
204
+ }
205
+ })
172
206
  }
173
207
  socket.onmessage = (e) => onMessage(typeof e.data === 'string' ? e.data : '')
174
208
  socket.onclose = () => {
@@ -181,6 +215,10 @@ export function useWorkspaceStream() {
181
215
  function scheduleReconnect() {
182
216
  if (stopped) return
183
217
  socket = null
218
+ // If we've never gone live and keep failing, flag the board as offline so the banner can
219
+ // surface a "not receiving live updates" state (a REST-only board otherwise looks fine but
220
+ // silently never updates). Reset the moment a socket opens (see `onopen`).
221
+ if (!everConnected.value && attempt + 1 >= INITIAL_FAIL_ATTEMPTS) connectionFailed.value = true
184
222
  const delay = Math.min(30_000, 500 * 2 ** attempt) // 0.5s → 30s cap
185
223
  attempt += 1
186
224
  reconnectTimer = setTimeout(connect, delay)
@@ -188,6 +226,11 @@ export function useWorkspaceStream() {
188
226
 
189
227
  function start() {
190
228
  stopped = false
229
+ // Reset the per-workspace connection lifecycle so a switch to a NEW workspace whose socket
230
+ // fails is flagged offline on its own merits, not masked by the previous workspace's history.
231
+ attempt = 0
232
+ everConnected.value = false
233
+ connectionFailed.value = false
191
234
  connect()
192
235
  }
193
236
 
@@ -201,5 +244,5 @@ export function useWorkspaceStream() {
201
244
  }
202
245
 
203
246
  onScopeDispose(stop)
204
- return { start, stop, connected }
247
+ return { start, stop, connected, everConnected, connectionFailed }
205
248
  }
@@ -243,6 +243,8 @@ const stream = useWorkspaceStream()
243
243
  // in the template would not unwrap, since `stream` is a plain object). Drives the headless
244
244
  // `workspace-stream` readiness marker the e2e suite waits on.
245
245
  const streamConnected = computed(() => stream.connected.value)
246
+ const streamEverConnected = computed(() => stream.everConnected.value)
247
+ const streamConnectionFailed = computed(() => stream.connectionFailed.value)
246
248
  watch(
247
249
  () => workspace.workspaceId,
248
250
  (id) => {
@@ -320,7 +322,11 @@ watch(
320
322
  />
321
323
  <BoardToolbar />
322
324
  <SpendWarningBanner />
323
- <ConnectionStatusBanner :connected="streamConnected" />
325
+ <ConnectionStatusBanner
326
+ :connected="streamConnected"
327
+ :ever-connected="streamEverConnected"
328
+ :connection-failed="streamConnectionFailed"
329
+ />
324
330
  <InspectorPanel />
325
331
  <!-- Code-split focus view. The fade lives here (not inside the component) so the
326
332
  leave animation still plays when `focusBlockId` clears and the v-if unmounts
@@ -557,7 +557,18 @@ export const useBoardStore = defineStore('board', () => {
557
557
  const t = getBlock(targetId)
558
558
  if (!t || !t.dependsOn.includes(sourceId)) return
559
559
  // the backend exposes a single toggle; the edge exists, so toggling removes it
560
- upsert(await api.toggleDependency(useWorkspaceStore().requireId(), targetId, { sourceId }))
560
+ try {
561
+ upsert(await api.toggleDependency(useWorkspaceStore().requireId(), targetId, { sourceId }))
562
+ } catch (e) {
563
+ // Mirror `toggleDependency`: a failure must surface (and leave the edge visible) rather
564
+ // than rejecting unhandled with no feedback.
565
+ toast.add({
566
+ title: tr('board.toast.unlinkFailed'),
567
+ description: e instanceof Error ? e.message : String(e),
568
+ icon: 'i-lucide-triangle-alert',
569
+ color: 'error',
570
+ })
571
+ }
561
572
  }
562
573
 
563
574
  return {
@@ -28,6 +28,12 @@ export const useObservabilityStore = defineStore('observability', () => {
28
28
  const contextByExecution = ref<Record<string, AgentContextSnapshot[]>>({})
29
29
  /** Execution ids whose context is currently loading. */
30
30
  const contextLoading = ref<Set<string>>(new Set())
31
+ /**
32
+ * Last context-load error message per execution id, or null. Distinguishes a genuine fetch
33
+ * failure from a run with no captured context: without this, a swallowed error rendered as
34
+ * the "no context stored" empty state — indistinguishable from success-with-nothing.
35
+ */
36
+ const contextErrors = ref<Record<string, string | null>>({})
31
37
  /** Per-execution-id performed-search-query list (newest first). */
32
38
  const searchQueriesByExecution = ref<Record<string, AgentSearchQuery[]>>({})
33
39
  /** Execution ids whose search queries are currently loading. */
@@ -133,11 +139,17 @@ export const useObservabilityStore = defineStore('observability', () => {
133
139
  async function loadContext(executionId: string) {
134
140
  if (!workspace.workspaceId) return
135
141
  withFlag(contextLoading, executionId, true)
142
+ contextErrors.value = { ...contextErrors.value, [executionId]: null }
136
143
  try {
137
144
  const { snapshots } = await api.getAgentContext(workspace.requireId(), executionId)
138
145
  contextByExecution.value = { ...contextByExecution.value, [executionId]: snapshots }
139
- } catch {
140
- // Best-effort: the panel shows an empty state; nothing is persisted client-side.
146
+ } catch (err) {
147
+ // Record the error so the panel can offer a retry instead of masquerading the failure as
148
+ // the "no context stored" empty state.
149
+ contextErrors.value = {
150
+ ...contextErrors.value,
151
+ [executionId]: err instanceof Error ? err.message : 'Failed to load context',
152
+ }
141
153
  } finally {
142
154
  withFlag(contextLoading, executionId, false)
143
155
  }
@@ -199,6 +211,7 @@ export const useObservabilityStore = defineStore('observability', () => {
199
211
  appendCall,
200
212
  downloadExport,
201
213
  contextByExecution,
214
+ contextErrors,
202
215
  contextFor,
203
216
  isContextLoading,
204
217
  loadContext,
@@ -23,7 +23,11 @@ export const usePreviewStore = defineStore('preview', () => {
23
23
 
24
24
  // Active poll timers while a preview is `starting`, so a settled/left preview stops polling.
25
25
  const timers = new Map<string, ReturnType<typeof setTimeout>>()
26
+ // Consecutive poll-tick failures per frame while `starting`, so a transient blip keeps polling
27
+ // (self-heals) but a persistent failure eventually surfaces instead of spinning forever.
28
+ const pollErrors = new Map<string, number>()
26
29
  const POLL_INTERVAL_MS = 2_500
30
+ const POLL_MAX_ERRORS = 5
27
31
 
28
32
  function stopPolling(frameId: string) {
29
33
  const timer = timers.get(frameId)
@@ -31,6 +35,7 @@ export const usePreviewStore = defineStore('preview', () => {
31
35
  clearTimeout(timer)
32
36
  timers.delete(frameId)
33
37
  }
38
+ pollErrors.delete(frameId)
34
39
  }
35
40
 
36
41
  function apply(frameId: string, state: PreviewState) {
@@ -50,9 +55,36 @@ export const usePreviewStore = defineStore('preview', () => {
50
55
  async function refresh(frameId: string): Promise<void> {
51
56
  const ws = useWorkspaceStore()
52
57
  try {
53
- apply(frameId, await api.getPreview(ws.requireId(), frameId))
54
- } catch {
55
- // A transient error leaves the last known state; stop polling so we don't spin.
58
+ const state = await api.getPreview(ws.requireId(), frameId)
59
+ pollErrors.delete(frameId)
60
+ // Clear any stale error from an earlier failed request/poll so a now-successful fetch
61
+ // doesn't render a working preview under a leftover error banner.
62
+ requestError.value[frameId] = undefined
63
+ apply(frameId, state)
64
+ } catch (err) {
65
+ // If we were polling a `starting` preview, a transient error must NOT silently wedge the
66
+ // amber "Starting…" forever with no recovery: keep polling (it self-heals when the runtime
67
+ // recovers) up to POLL_MAX_ERRORS, then give up.
68
+ const prev = byFrame.value[frameId]
69
+ if (prev?.status === 'starting') {
70
+ const n = (pollErrors.get(frameId) ?? 0) + 1
71
+ if (n <= POLL_MAX_ERRORS) {
72
+ pollErrors.set(frameId, n)
73
+ timers.set(
74
+ frameId,
75
+ setTimeout(() => void refresh(frameId), POLL_INTERVAL_MS),
76
+ )
77
+ return
78
+ }
79
+ // Gave up: the preview never became reachable through the blips. Flip it out of the amber
80
+ // "Starting…" into a `failed` state carrying the error (with a Start to retry), rather than
81
+ // leaving the status claiming "Starting…" while polling has silently stopped.
82
+ byFrame.value[frameId] = {
83
+ ...prev,
84
+ status: 'failed',
85
+ error: err instanceof Error ? err.message : String(err),
86
+ }
87
+ }
56
88
  stopPolling(frameId)
57
89
  }
58
90
  }
@@ -3,7 +3,8 @@
3
3
  "loading": "Loading…",
4
4
  "loadingBoard": "Loading board…",
5
5
  "backendUnreachable": "Can't reach the backend",
6
- "reconnecting": "Reconnecting…"
6
+ "reconnecting": "Reconnecting…",
7
+ "offline": "Not receiving live updates"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Language",
@@ -96,6 +97,7 @@
96
97
  "moveFailed": "Could not move",
97
98
  "deleteFailed": "Could not delete",
98
99
  "linkFailed": "Could not link tasks",
100
+ "unlinkFailed": "Could not remove dependency",
99
101
  "recurringDeleteFailed": "Could not delete recurring pipeline",
100
102
  "deleted": "Deleted \"{name}\"",
101
103
  "moved": "Moved \"{name}\""
@@ -1095,6 +1097,7 @@
1095
1097
  "loadingContext": "Loading provided context…",
1096
1098
  "noCalls": "No model calls recorded for this run.",
1097
1099
  "noContext": "No agent context stored for this run. It is captured per dispatch when the workspace has 'Store full agent context' enabled.",
1100
+ "contextError": "Could not load the provided context.",
1098
1101
  "summary": {
1099
1102
  "calls": "Calls",
1100
1103
  "tokensInOut": "Tokens (in / out)",
@@ -3,7 +3,8 @@
3
3
  "loading": "Cargando…",
4
4
  "loadingBoard": "Cargando tablero…",
5
5
  "backendUnreachable": "No se puede conectar con el backend",
6
- "reconnecting": "Reconectando…"
6
+ "reconnecting": "Reconectando…",
7
+ "offline": "No se reciben actualizaciones en vivo"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Idioma",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "No se pudo mover",
82
83
  "deleteFailed": "No se pudo eliminar",
83
84
  "linkFailed": "No se pudieron vincular las tareas",
85
+ "unlinkFailed": "No se pudo eliminar la dependencia",
84
86
  "recurringDeleteFailed": "No se pudo eliminar el pipeline recurrente",
85
87
  "deleted": "\"{name}\" eliminado",
86
88
  "moved": "\"{name}\" movido"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "Cargando contexto proporcionado…",
1053
1055
  "noCalls": "No se registraron llamadas al modelo en esta ejecución.",
1054
1056
  "noContext": "No se almacenó contexto del agente para esta ejecución. Se captura por despacho cuando el espacio de trabajo tiene activado 'Almacenar contexto completo del agente'.",
1057
+ "contextError": "No se pudo cargar el contexto proporcionado.",
1055
1058
  "summary": {
1056
1059
  "calls": "Llamadas",
1057
1060
  "tokensInOut": "Tokens (entrada / salida)",
@@ -3,7 +3,8 @@
3
3
  "loading": "Chargement…",
4
4
  "loadingBoard": "Chargement du tableau…",
5
5
  "backendUnreachable": "Impossible de joindre le backend",
6
- "reconnecting": "Reconnexion…"
6
+ "reconnecting": "Reconnexion…",
7
+ "offline": "Mises à jour en direct non reçues"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Langue",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "Impossible de déplacer",
82
83
  "deleteFailed": "Impossible de supprimer",
83
84
  "linkFailed": "Impossible de lier les tâches",
85
+ "unlinkFailed": "Impossible de supprimer la dépendance",
84
86
  "recurringDeleteFailed": "Impossible de supprimer le pipeline récurrent",
85
87
  "deleted": "\"{name}\" supprimé",
86
88
  "moved": "\"{name}\" déplacé"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "Chargement du contexte fourni…",
1053
1055
  "noCalls": "Aucun appel de modèle enregistré pour cette exécution.",
1054
1056
  "noContext": "Aucun contexte d'agent stocké pour cette exécution. Il est capturé à chaque dispatch lorsque l'espace de travail a activé 'Stocker le contexte complet de l'agent'.",
1057
+ "contextError": "Impossible de charger le contexte fourni.",
1055
1058
  "summary": {
1056
1059
  "calls": "Appels",
1057
1060
  "tokensInOut": "Tokens (entrée / sortie)",
@@ -3,7 +3,8 @@
3
3
  "loading": "טוען…",
4
4
  "loadingBoard": "טוען לוח…",
5
5
  "backendUnreachable": "לא ניתן להתחבר לבקאנד",
6
- "reconnecting": "מתחבר מחדש…"
6
+ "reconnecting": "מתחבר מחדש…",
7
+ "offline": "לא מתקבלים עדכונים חיים"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "שפה",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "לא ניתן היה להעביר",
82
83
  "deleteFailed": "לא ניתן היה למחוק",
83
84
  "linkFailed": "לא ניתן היה לקשר משימות",
85
+ "unlinkFailed": "לא ניתן להסיר את התלות",
84
86
  "recurringDeleteFailed": "לא ניתן היה למחוק את הצינור החוזר",
85
87
  "deleted": "\"{name}\" נמחק",
86
88
  "moved": "\"{name}\" הועבר"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "טוען את ההקשר שסופק…",
1053
1055
  "noCalls": "לא נרשמו קריאות מודל עבור ריצה זו.",
1054
1056
  "noContext": "לא אוחסן הקשר סוכן עבור ריצה זו. הוא נלכד בכל שיגור כשבסביבת העבודה מופעל 'אחסן הקשר סוכן מלא'.",
1057
+ "contextError": "לא ניתן לטעון את ההקשר שסופק.",
1055
1058
  "summary": {
1056
1059
  "calls": "קריאות",
1057
1060
  "tokensInOut": "טוקנים (נכנס / יוצא)",
@@ -3,7 +3,8 @@
3
3
  "loading": "読み込み中…",
4
4
  "loadingBoard": "ボードを読み込み中…",
5
5
  "backendUnreachable": "バックエンドに接続できません",
6
- "reconnecting": "再接続中…"
6
+ "reconnecting": "再接続中…",
7
+ "offline": "ライブ更新を受信していません"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "言語",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "移動できませんでした",
82
83
  "deleteFailed": "削除できませんでした",
83
84
  "linkFailed": "タスクをリンクできませんでした",
85
+ "unlinkFailed": "依存関係を削除できませんでした",
84
86
  "recurringDeleteFailed": "定期パイプラインを削除できませんでした",
85
87
  "deleted": "\"{name}\" を削除しました",
86
88
  "moved": "\"{name}\" を移動しました"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "提供されたコンテキストを読み込み中…",
1053
1055
  "noCalls": "この実行で記録されたモデル呼び出しはありません。",
1054
1056
  "noContext": "この実行で保存されたエージェントコンテキストはありません。ワークスペースで「エージェントコンテキストを完全保存」が有効な場合、ディスパッチごとに記録されます。",
1057
+ "contextError": "提供されたコンテキストを読み込めませんでした。",
1055
1058
  "summary": {
1056
1059
  "calls": "呼び出し",
1057
1060
  "tokensInOut": "トークン (入力 / 出力)",
@@ -3,7 +3,8 @@
3
3
  "loading": "Ładowanie…",
4
4
  "loadingBoard": "Ładowanie tablicy…",
5
5
  "backendUnreachable": "Nie można połączyć się z backendem",
6
- "reconnecting": "Ponowne łączenie…"
6
+ "reconnecting": "Ponowne łączenie…",
7
+ "offline": "Brak aktualizacji na żywo"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Język",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "Nie udało się przenieść",
82
83
  "deleteFailed": "Nie udało się usunąć",
83
84
  "linkFailed": "Nie udało się powiązać zadań",
85
+ "unlinkFailed": "Nie udało się usunąć zależności",
84
86
  "recurringDeleteFailed": "Nie udało się usunąć cyklicznego pipeline'u",
85
87
  "deleted": "Usunięto \"{name}\"",
86
88
  "moved": "Przeniesiono \"{name}\""
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "Ładowanie dostarczonego kontekstu…",
1053
1055
  "noCalls": "Brak zarejestrowanych wywołań modelu dla tego uruchomienia.",
1054
1056
  "noContext": "Brak zapisanego kontekstu agenta dla tego uruchomienia. Jest on przechwytywany przy każdym wysłaniu, gdy w obszarze roboczym włączono opcję 'Przechowuj pełny kontekst agenta'.",
1057
+ "contextError": "Nie udało się załadować dostarczonego kontekstu.",
1055
1058
  "summary": {
1056
1059
  "calls": "Wywołania",
1057
1060
  "tokensInOut": "Tokeny (we / wy)",
@@ -3,7 +3,8 @@
3
3
  "loading": "Yükleniyor…",
4
4
  "loadingBoard": "Pano yükleniyor…",
5
5
  "backendUnreachable": "Arka uca ulaşılamıyor",
6
- "reconnecting": "Yeniden bağlanılıyor…"
6
+ "reconnecting": "Yeniden bağlanılıyor…",
7
+ "offline": "Canlı güncellemeler alınmıyor"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Dil",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "Taşınamadı",
82
83
  "deleteFailed": "Silinemedi",
83
84
  "linkFailed": "Görevler bağlanamadı",
85
+ "unlinkFailed": "Bağımlılık kaldırılamadı",
84
86
  "recurringDeleteFailed": "Yinelenen pipeline silinemedi",
85
87
  "deleted": "\"{name}\" silindi",
86
88
  "moved": "\"{name}\" taşındı"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "Sağlanan bağlam yükleniyor…",
1053
1055
  "noCalls": "Bu çalışma için kaydedilmiş model çağrısı yok.",
1054
1056
  "noContext": "Bu çalışma için saklanmış aracı bağlamı yok. Çalışma alanında 'Tam aracı bağlamını sakla' etkinleştirildiğinde her gönderim için yakalanır.",
1057
+ "contextError": "Sağlanan bağlam yüklenemedi.",
1055
1058
  "summary": {
1056
1059
  "calls": "Çağrılar",
1057
1060
  "tokensInOut": "Token (giriş / çıkış)",
@@ -3,7 +3,8 @@
3
3
  "loading": "Завантаження…",
4
4
  "loadingBoard": "Завантаження дошки…",
5
5
  "backendUnreachable": "Не вдається зʼєднатися з бекендом",
6
- "reconnecting": "Повторне зʼєднання…"
6
+ "reconnecting": "Повторне зʼєднання…",
7
+ "offline": "Оновлення в реальному часі не надходять"
7
8
  },
8
9
  "language": {
9
10
  "switcher": "Мова",
@@ -81,6 +82,7 @@
81
82
  "moveFailed": "Не вдалося перемістити",
82
83
  "deleteFailed": "Не вдалося видалити",
83
84
  "linkFailed": "Не вдалося звʼязати завдання",
85
+ "unlinkFailed": "Не вдалося видалити залежність",
84
86
  "recurringDeleteFailed": "Не вдалося видалити повторюваний пайплайн",
85
87
  "deleted": "\"{name}\" видалено",
86
88
  "moved": "\"{name}\" переміщено"
@@ -1052,6 +1054,7 @@
1052
1054
  "loadingContext": "Завантаження наданого контексту…",
1053
1055
  "noCalls": "Для цього запуску не записано викликів моделі.",
1054
1056
  "noContext": "Для цього запуску не збережено контексту агента. Він фіксується при кожному відправленні, коли для робочого простору ввімкнено 'Зберігати повний контекст агента'.",
1057
+ "contextError": "Не вдалося завантажити наданий контекст.",
1055
1058
  "summary": {
1056
1059
  "calls": "Виклики",
1057
1060
  "tokensInOut": "Токени (вхід / вихід)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.96.0",
3
+ "version": "0.96.2",
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.105.0"
37
+ "@cat-factory/contracts": "0.106.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",