@cat-factory/app 0.280.0 → 0.280.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.
- package/README.md +26 -1
- package/app/components/board/TaskDependencyEdges.vue +26 -15
- package/app/components/board/nodes/TaskCard.vue +6 -1
- package/app/components/common/AsyncViewError.vue +30 -0
- package/app/components/panels/AgentStepDetail.vue +651 -659
- package/app/components/panels/InspectorPanel.vue +6 -0
- package/app/composables/useBoardActivity.ts +62 -6
- package/app/composables/useTaskExpansion.ts +10 -25
- package/app/docs/consumer-extensions.md +9 -0
- package/app/modular/result-views.ts +58 -21
- package/app/pages/index.vue +68 -64
- package/app/utils/asyncView.ts +24 -0
- package/app/utils/blockRects.spec.ts +82 -0
- package/app/utils/blockRects.ts +61 -0
- package/app/utils/boardWakeGate.spec.ts +101 -0
- package/app/utils/boardWakeGate.ts +78 -0
- package/i18n/locales/de.json +5 -0
- package/i18n/locales/en.json +5 -0
- package/i18n/locales/es.json +5 -0
- package/i18n/locales/fr.json +5 -0
- package/i18n/locales/he.json +5 -0
- package/i18n/locales/it.json +5 -0
- package/i18n/locales/ja.json +5 -0
- package/i18n/locales/pl.json +5 -0
- package/i18n/locales/tr.json +5 -0
- package/i18n/locales/uk.json +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,29 @@ the animation that follows, parking once the output has held still for a few fra
|
|
|
86
86
|
half works alone: a signal fires one frame BEFORE the transition it starts has any geometry, and
|
|
87
87
|
a bare frame loop never stops.
|
|
88
88
|
|
|
89
|
+
**The pulse does not treat its signals alike, and a driver must not assume it does.** What the
|
|
90
|
+
user is moving (pointer, wheel, scroll, resize, the camera's own `pulse()`) wakes the loops
|
|
91
|
+
immediately, because a lagging arrow under a drag is the bug this whole design exists to fix.
|
|
92
|
+
RENDERS do not: a live board re-renders its cards on every execution event, and admitting each
|
|
93
|
+
one kept the loops awake forever on exactly the board where measuring costs most, so mutations
|
|
94
|
+
go through a rate limit (`utils/boardWakeGate.ts`, one wake led in immediately and then at most
|
|
95
|
+
one per 250ms while the stream lasts). The cost is stated rather than hidden: a geometry change
|
|
96
|
+
caused purely by a re-render, a badge appearing and growing a card, can take up to that interval
|
|
97
|
+
to be followed. A driver that needs a signal the DOM cannot show, a link set changing with no
|
|
98
|
+
card moving, watches its own reactive source and pokes, the way `TaskDependencyEdges` watches
|
|
99
|
+
its four link lists.
|
|
100
|
+
|
|
101
|
+
The gesture listeners are on the WINDOW, not on the canvas element. A drag does not stop at the
|
|
102
|
+
canvas's edge (`useBlockDrag` tracks the pointer on the window for exactly that reason) and the
|
|
103
|
+
top overlay region and the inspector are siblings painted OVER the canvas, so a canvas-bound
|
|
104
|
+
listener went quiet for as long as the cursor crossed one of them.
|
|
105
|
+
|
|
106
|
+
**Measure through `utils/blockRects.ts`, never a `querySelector` per card.** `measureBlocks()`
|
|
107
|
+
hands a pass one snapshot: the cards resolved in one query, first-in-document-order per id, and
|
|
108
|
+
each rect read at most once. It is what makes a wake cheap enough for the rate limit above to be
|
|
109
|
+
a saving rather than a way of hiding an expensive pass, and it is lazy, so a pass that resolves
|
|
110
|
+
nothing (a board with no links at all) touches no DOM.
|
|
111
|
+
|
|
89
112
|
Two things this cost, both worth knowing before adding a third driver. `compute` returning
|
|
90
113
|
`true` unconditionally silently restores the old behaviour, which is why the loop's contract is
|
|
91
114
|
stated in terms of what the user can see rather than what the function did. And the pulse
|
|
@@ -102,7 +125,9 @@ font resizing a card. That leaves an arrow stale until the next pulse of any kin
|
|
|
102
125
|
deliberate trade: firing too often costs a handful of frames, and the alternative is the loop
|
|
103
126
|
that never sleeps.
|
|
104
127
|
|
|
105
|
-
`app/utils/settlingLoop.spec.ts` pins the loop against a hand-driven frame clock
|
|
128
|
+
`app/utils/settlingLoop.spec.ts` pins the loop against a hand-driven frame clock;
|
|
129
|
+
`boardWakeGate.spec.ts` pins that the rate limit delivers every suppressed wake rather than
|
|
130
|
+
dropping it, and `blockRects.spec.ts` that a snapshot resolves and measures each card once.
|
|
106
131
|
|
|
107
132
|
### A store must be instantiable outside a component `setup`
|
|
108
133
|
|
|
@@ -3,6 +3,7 @@ import { ref, shallowRef, computed, watch } from 'vue'
|
|
|
3
3
|
import { useBoardActivity } from '~/composables/useBoardActivity'
|
|
4
4
|
import { useSettlingRaf } from '~/composables/useSettlingRaf'
|
|
5
5
|
import { commitSegments, type EdgeSegment } from '~/utils/edgeSegments'
|
|
6
|
+
import { measureBlocks, type BlockMeasurements } from '~/utils/blockRects'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Draws dependency arrows between task cards as an SVG overlay on top of the
|
|
@@ -11,9 +12,11 @@ import { commitSegments, type EdgeSegment } from '~/utils/edgeSegments'
|
|
|
11
12
|
* zoom / drag / expand for free. When a task's frame is collapsed (its card
|
|
12
13
|
* isn't rendered), the arrow anchors to the frame card instead.
|
|
13
14
|
*
|
|
14
|
-
* Measuring
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Measuring costs forced layout reads, so it runs only while something is actually
|
|
16
|
+
* moving: the board's activity pulse wakes it and `useSettlingRaf` parks it again
|
|
17
|
+
* once the resolved segments hold still. Within one pass the cards are resolved and
|
|
18
|
+
* measured through a single shared snapshot (`measureBlocks`), so a task with five
|
|
19
|
+
* dependencies is found and measured once rather than five times.
|
|
17
20
|
*/
|
|
18
21
|
const board = useBoardStore()
|
|
19
22
|
|
|
@@ -91,10 +94,10 @@ const connectionLinks = computed(() => {
|
|
|
91
94
|
|
|
92
95
|
/** Resolve a task's anchor: walk up task → module → service to the first card
|
|
93
96
|
* that's actually rendered (a container may be collapsed). */
|
|
94
|
-
function anchorEl(taskId: string): HTMLElement | null {
|
|
97
|
+
function anchorEl(taskId: string, blocks: BlockMeasurements): HTMLElement | null {
|
|
95
98
|
let cur = board.getBlock(taskId)
|
|
96
99
|
while (cur) {
|
|
97
|
-
const el =
|
|
100
|
+
const el = blocks.elementFor(cur.id)
|
|
98
101
|
if (el) return el
|
|
99
102
|
cur = cur.parentId ? board.getBlock(cur.parentId) : undefined
|
|
100
103
|
}
|
|
@@ -112,12 +115,17 @@ function border(cx: number, cy: number, hw: number, hh: number, tx: number, ty:
|
|
|
112
115
|
|
|
113
116
|
/** Resolve the on-screen, origin-relative border-to-border segment between two blocks,
|
|
114
117
|
* or null when either end is missing or both collapsed into the same frame. */
|
|
115
|
-
function segmentBetween(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
function segmentBetween(
|
|
119
|
+
sourceId: string,
|
|
120
|
+
targetId: string,
|
|
121
|
+
origin: DOMRect,
|
|
122
|
+
blocks: BlockMeasurements,
|
|
123
|
+
) {
|
|
124
|
+
const a = anchorEl(sourceId, blocks)
|
|
125
|
+
const b = anchorEl(targetId, blocks)
|
|
118
126
|
if (!a || !b || a === b) return null // missing, or both collapsed into the same frame
|
|
119
|
-
const ra =
|
|
120
|
-
const rb =
|
|
127
|
+
const ra = blocks.rectFor(a)
|
|
128
|
+
const rb = blocks.rectFor(b)
|
|
121
129
|
const ax = ra.left + ra.width / 2 - origin.left
|
|
122
130
|
const ay = ra.top + ra.height / 2 - origin.top
|
|
123
131
|
const bx = rb.left + rb.width / 2 - origin.left
|
|
@@ -131,10 +139,11 @@ function segmentBetween(sourceId: string, targetId: string, origin: DOMRect) {
|
|
|
131
139
|
function linkSegments(
|
|
132
140
|
links: { id: string; source: string; target: string }[],
|
|
133
141
|
origin: DOMRect,
|
|
142
|
+
blocks: BlockMeasurements,
|
|
134
143
|
): EdgeSegment[] {
|
|
135
144
|
const out: EdgeSegment[] = []
|
|
136
145
|
for (const link of links) {
|
|
137
|
-
const seg = segmentBetween(link.source, link.target, origin)
|
|
146
|
+
const seg = segmentBetween(link.source, link.target, origin, blocks)
|
|
138
147
|
if (seg) out.push({ id: link.id, ...seg })
|
|
139
148
|
}
|
|
140
149
|
return out
|
|
@@ -145,10 +154,12 @@ function recompute(): boolean {
|
|
|
145
154
|
const el = svg.value
|
|
146
155
|
if (!el) return false
|
|
147
156
|
const origin = el.getBoundingClientRect()
|
|
157
|
+
// One snapshot for the whole pass: every overlay below resolves and measures through it.
|
|
158
|
+
const blocks = measureBlocks()
|
|
148
159
|
|
|
149
160
|
const deps: EdgeSegment[] = []
|
|
150
161
|
for (const d of taskDeps.value) {
|
|
151
|
-
const seg = segmentBetween(d.source, d.target, origin)
|
|
162
|
+
const seg = segmentBetween(d.source, d.target, origin, blocks)
|
|
152
163
|
if (!seg) continue
|
|
153
164
|
deps.push({ id: d.id, ...seg, done: board.getBlock(d.source)?.status === 'done' })
|
|
154
165
|
}
|
|
@@ -157,9 +168,9 @@ function recompute(): boolean {
|
|
|
157
168
|
// would short-circuit and leave the later overlays drawn at stale coordinates.
|
|
158
169
|
return [
|
|
159
170
|
commitSegments(segments, deps),
|
|
160
|
-
commitSegments(memberSegments, linkSegments(epicLinks.value, origin)),
|
|
161
|
-
commitSegments(frontendSegments, linkSegments(frontendLinks.value, origin)),
|
|
162
|
-
commitSegments(connectionSegments, linkSegments(connectionLinks.value, origin)),
|
|
171
|
+
commitSegments(memberSegments, linkSegments(epicLinks.value, origin, blocks)),
|
|
172
|
+
commitSegments(frontendSegments, linkSegments(frontendLinks.value, origin, blocks)),
|
|
173
|
+
commitSegments(connectionSegments, linkSegments(connectionLinks.value, origin, blocks)),
|
|
163
174
|
].some(Boolean)
|
|
164
175
|
}
|
|
165
176
|
|
|
@@ -372,8 +372,13 @@ function selectTask() {
|
|
|
372
372
|
</div>
|
|
373
373
|
|
|
374
374
|
<!-- title gets a full-width row so long titles wrap to two lines rather than
|
|
375
|
-
truncating to an unreadable stub; the full text stays available on hover.
|
|
375
|
+
truncating to an unreadable stub; the full text stays available on hover.
|
|
376
|
+
|
|
377
|
+
It is also the card's SELECTION affordance for tests: every action button below stops
|
|
378
|
+
propagation, so a click resolved to one of them never reaches `selectTask`, and the
|
|
379
|
+
title is the one always-rendered part of the body that no control can occupy. -->
|
|
376
380
|
<div
|
|
381
|
+
data-testid="task-title"
|
|
377
382
|
class="mt-1 line-clamp-2 break-words text-[11px] font-semibold leading-snug text-slate-100"
|
|
378
383
|
:title="task.title"
|
|
379
384
|
>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shown in place of a code-split surface whose chunk failed to load. See `utils/asyncView.ts`
|
|
3
|
+
// for why every async surface gets one.
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
|
|
6
|
+
function reload() {
|
|
7
|
+
window.location.reload()
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
data-testid="async-view-error"
|
|
14
|
+
class="fixed inset-0 z-50 grid place-items-center bg-slate-950/80 p-6 backdrop-blur-sm"
|
|
15
|
+
role="alert"
|
|
16
|
+
>
|
|
17
|
+
<div class="max-w-sm rounded-2xl border border-slate-700 bg-slate-900 p-5 shadow-2xl">
|
|
18
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-100">
|
|
19
|
+
<UIcon name="i-lucide-unplug" class="h-4 w-4 shrink-0 text-amber-400" />
|
|
20
|
+
{{ t('errors.asyncView.title') }}
|
|
21
|
+
</div>
|
|
22
|
+
<p class="mt-2 text-[12px] leading-relaxed text-slate-400">
|
|
23
|
+
{{ t('errors.asyncView.body') }}
|
|
24
|
+
</p>
|
|
25
|
+
<UButton class="mt-4" color="primary" variant="soft" size="xs" @click="reload">
|
|
26
|
+
{{ t('errors.asyncView.reload') }}
|
|
27
|
+
</UButton>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|