@cat-factory/app 0.319.2 → 0.320.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.
- package/app/components/queue/QueueCard.vue +4 -8
- package/app/composables/useFramePlacement.ts +82 -2
- package/app/composables/useRunDeepLink.spec.ts +60 -2
- package/app/composables/useRunDeepLink.ts +59 -22
- package/app/composables/useSemanticZoom.spec.ts +22 -0
- package/app/composables/useSemanticZoom.ts +10 -5
- package/app/composables/useTaskReveal.ts +26 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { agentKindMeta } from '~/utils/catalog'
|
|
3
3
|
import { formatDuration, useNowTick } from '~/composables/useStepTimer'
|
|
4
|
-
import {
|
|
4
|
+
import { useTaskReveal } from '~/composables/useTaskReveal'
|
|
5
5
|
import { LANE_REASON_LABEL_KEYS } from '~/utils/swimlanes'
|
|
6
6
|
import type { QueueEntry } from '~/utils/queueSections'
|
|
7
7
|
import IconButton from '~/components/common/IconButton.vue'
|
|
@@ -25,8 +25,7 @@ const { t } = useI18n()
|
|
|
25
25
|
const ui = useUiStore()
|
|
26
26
|
const execution = useExecutionStore()
|
|
27
27
|
const agentRuns = useAgentRunsStore()
|
|
28
|
-
const
|
|
29
|
-
const { focusFrame } = useFramePlacement()
|
|
28
|
+
const { revealTask } = useTaskReveal()
|
|
30
29
|
// A shared 30s clock, so "waited 4m" advances on a quiet board instead of freezing until some
|
|
31
30
|
// unrelated event re-evaluates the card.
|
|
32
31
|
const nowTick = useNowTick(30_000)
|
|
@@ -125,13 +124,10 @@ function openTask() {
|
|
|
125
124
|
/**
|
|
126
125
|
* Leave the queue for the canvas, with this task selected and its service in view. The canvas
|
|
127
126
|
* kept its own camera while the queue covered it, so without the pan the task is usually off
|
|
128
|
-
* screen.
|
|
129
|
-
* centres on the frame.
|
|
127
|
+
* screen.
|
|
130
128
|
*/
|
|
131
129
|
function showOnBoard() {
|
|
132
|
-
|
|
133
|
-
ui.select(props.entry.task.id)
|
|
134
|
-
void focusFrame(props.entry.service?.id ?? props.entry.task.id)
|
|
130
|
+
revealTask(props.entry.task.id)
|
|
135
131
|
}
|
|
136
132
|
</script>
|
|
137
133
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nextTick } from 'vue'
|
|
2
2
|
import { useVueFlow } from '@vue-flow/core'
|
|
3
3
|
import { BOARD_FLOW_ID } from '~/composables/useBoardFlow'
|
|
4
|
+
import { LOD_MIN_ZOOM } from '~/composables/useSemanticZoom'
|
|
4
5
|
import {
|
|
5
6
|
EMPTY_FRAME_SIZE,
|
|
6
7
|
EPIC_NODE_SIZE,
|
|
@@ -27,7 +28,8 @@ const MIN_FOCUS_ZOOM = 0.6
|
|
|
27
28
|
|
|
28
29
|
export function useFramePlacement() {
|
|
29
30
|
const board = useBoardStore()
|
|
30
|
-
const { viewport, setCenter, screenToFlowCoordinate } =
|
|
31
|
+
const { viewport, setCenter, screenToFlowCoordinate, findNode, onNodesInitialized, onMoveStart } =
|
|
32
|
+
useVueFlow(BOARD_FLOW_ID)
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Every existing top-level board node's rect in flow-space — both service frames
|
|
@@ -87,5 +89,83 @@ export function useFramePlacement() {
|
|
|
87
89
|
setCenter(cx, cy, { zoom, duration: opts?.duration ?? 400 })
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
/** Whether the canvas has drawn the frame, so there is a measured node to centre on. */
|
|
93
|
+
function isRendered(id: string): boolean {
|
|
94
|
+
const node = findNode(id)
|
|
95
|
+
return !!node && node.dimensions.width > 0
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves once the canvas has measured the frame's node. A deep link followed on a fresh load
|
|
100
|
+
* arrives before the board has measured its nodes, and centring on a frame with no size yet puts
|
|
101
|
+
* the camera on its corner, or nowhere.
|
|
102
|
+
*/
|
|
103
|
+
async function whenRendered(id: string): Promise<void> {
|
|
104
|
+
if (isRendered(id)) return
|
|
105
|
+
await new Promise<void>((resolve) => {
|
|
106
|
+
const { off } = onNodesInitialized(() => {
|
|
107
|
+
if (!isRendered(id)) return
|
|
108
|
+
off()
|
|
109
|
+
resolve()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Centre the camera on a task card, zoomed in to the level where the board renders a task's
|
|
116
|
+
* pipeline steps (`steps`), once the canvas has drawn its frame. A closer zoom is left alone. A
|
|
117
|
+
* task has no position of its own (it lives in its frame's swimlanes), so its centre is read off
|
|
118
|
+
* the card the frame drew; with no card to read, the camera centres on the frame instead.
|
|
119
|
+
*/
|
|
120
|
+
async function focusTaskWhenRendered(taskId: string, frameId: string): Promise<void> {
|
|
121
|
+
if (!board.getBlock(frameId)) return
|
|
122
|
+
await whenRendered(frameId)
|
|
123
|
+
await nextTick()
|
|
124
|
+
const frameSelector = `.vue-flow__node[data-id="${CSS.escape(frameId)}"]`
|
|
125
|
+
// The card inside the frame's own node, not another surface that names the same task.
|
|
126
|
+
const cardSelector = `${frameSelector} [data-block-id="${CSS.escape(taskId)}"]`
|
|
127
|
+
if (!document.querySelector(cardSelector)) {
|
|
128
|
+
await focusFrame(frameId)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const zoom = Math.max(viewport.value.zoom, LOD_MIN_ZOOM.steps)
|
|
132
|
+
await centreOn(cardSelector, zoom, 400)
|
|
133
|
+
keepCentred(frameSelector, cardSelector, zoom)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Keep the card centred while the board settles around it, until the person moves the camera.
|
|
138
|
+
* At the `steps` level a frame lays its swimlanes out again as each card renders its steps, so
|
|
139
|
+
* the card moves inside its frame after the camera has landed on it. Each change of the frame's
|
|
140
|
+
* or the card's size re-centres on it; the first pan or zoom the person makes ends the follow.
|
|
141
|
+
*/
|
|
142
|
+
function keepCentred(frameSelector: string, cardSelector: string, zoom: number): void {
|
|
143
|
+
const frame = document.querySelector(frameSelector)
|
|
144
|
+
const card = document.querySelector(cardSelector)
|
|
145
|
+
if (!frame || !card) return
|
|
146
|
+
const observer = new ResizeObserver(() => void centreOn(cardSelector, zoom, 0))
|
|
147
|
+
observer.observe(frame)
|
|
148
|
+
observer.observe(card)
|
|
149
|
+
const { off } = onMoveStart(({ event }) => {
|
|
150
|
+
// A programmatic move (this follow's own) carries no input event; a person's does.
|
|
151
|
+
const byPerson = 'sourceEvent' in event ? event.sourceEvent != null : true
|
|
152
|
+
if (!byPerson) return
|
|
153
|
+
observer.disconnect()
|
|
154
|
+
off()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Centre the camera on an element of the board at `zoom`, resolving when the move lands. */
|
|
159
|
+
async function centreOn(selector: string, zoom: number, duration: number): Promise<void> {
|
|
160
|
+
const element = document.querySelector(selector)
|
|
161
|
+
if (!element) return
|
|
162
|
+
const rect = element.getBoundingClientRect()
|
|
163
|
+
const centre = screenToFlowCoordinate({
|
|
164
|
+
x: rect.left + rect.width / 2,
|
|
165
|
+
y: rect.top + rect.height / 2,
|
|
166
|
+
})
|
|
167
|
+
await setCenter(centre.x, centre.y, { zoom, duration })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { freeFramePosition, focusFrame, focusTaskWhenRendered }
|
|
91
171
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createPinia, setActivePinia } from 'pinia'
|
|
2
|
-
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
|
-
import { captureRunDeepLink } from './useRunDeepLink'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { applyRunDeepLink, captureRunDeepLink, type PendingRunLink } from './useRunDeepLink'
|
|
4
4
|
|
|
5
5
|
/** Point `window.location` + `history` at a URL the capture can read and rewrite. */
|
|
6
6
|
function setUrl(search: string): void {
|
|
@@ -54,3 +54,61 @@ describe('captureRunDeepLink', () => {
|
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
describe('applyRunDeepLink', () => {
|
|
59
|
+
function surfaces() {
|
|
60
|
+
return {
|
|
61
|
+
revealTask: vi.fn(),
|
|
62
|
+
openRun: vi.fn(),
|
|
63
|
+
openOutcome: vi.fn(),
|
|
64
|
+
openObservability: vi.fn(),
|
|
65
|
+
openTestEvidence: vi.fn(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function link(view: string | null, blockId: string | null = 'blk_1'): PendingRunLink {
|
|
70
|
+
return { workspaceId: 'ws_1', blockId, runId: 'exec_1', view }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
it('brings the task into view and opens its running pipeline when no view is named', () => {
|
|
74
|
+
const opened = surfaces()
|
|
75
|
+
applyRunDeepLink(link(null), opened)
|
|
76
|
+
expect(opened.revealTask).toHaveBeenCalledWith('blk_1')
|
|
77
|
+
expect(opened.openRun).toHaveBeenCalledWith('blk_1')
|
|
78
|
+
expect(opened.openObservability).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('opens the running pipeline for view=run, the same as no view', () => {
|
|
82
|
+
const opened = surfaces()
|
|
83
|
+
applyRunDeepLink(link('run'), opened)
|
|
84
|
+
expect(opened.openRun).toHaveBeenCalledWith('blk_1')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it.each([
|
|
88
|
+
['observability', 'openObservability', ['exec_1']],
|
|
89
|
+
['test-evidence', 'openTestEvidence', ['exec_1']],
|
|
90
|
+
['outcome', 'openOutcome', ['exec_1', 'blk_1']],
|
|
91
|
+
] as const)('opens %s for its run, beside the task brought into view', (view, surface, args) => {
|
|
92
|
+
const opened = surfaces()
|
|
93
|
+
applyRunDeepLink(link(view), opened)
|
|
94
|
+
expect(opened.revealTask).toHaveBeenCalledWith('blk_1')
|
|
95
|
+
expect(opened[surface]).toHaveBeenCalledWith(...args)
|
|
96
|
+
expect(opened.openRun).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('lands on the task and opens nothing for a view it does not know', () => {
|
|
100
|
+
const opened = surfaces()
|
|
101
|
+
applyRunDeepLink(link('not-a-view'), opened)
|
|
102
|
+
expect(opened.revealTask).toHaveBeenCalledWith('blk_1')
|
|
103
|
+
for (const open of [opened.openRun, opened.openOutcome, opened.openObservability]) {
|
|
104
|
+
expect(open).not.toHaveBeenCalled()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('opens no focus view and moves no camera for a link naming no task', () => {
|
|
109
|
+
const opened = surfaces()
|
|
110
|
+
applyRunDeepLink(link(null, null), opened)
|
|
111
|
+
expect(opened.revealTask).not.toHaveBeenCalled()
|
|
112
|
+
expect(opened.openRun).not.toHaveBeenCalled()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Boot-time replay of a RUN deep link: `?ws=<id>&block=<id>&run=<id>&view=<panel>`.
|
|
3
3
|
*
|
|
4
|
+
* The task the link names is brought into view (`useTaskReveal`), and `view` picks what opens on
|
|
5
|
+
* it: `run` (also what a link with no `view` gets) opens the task's focus view, where its pipeline
|
|
6
|
+
* is followed step by step; `outcome`, `observability` and `test-evidence` open those result
|
|
7
|
+
* windows for the run. An unknown view still lands on the right board and task.
|
|
8
|
+
*
|
|
4
9
|
* The engine's PR verification report links each PR back into the run: to the observability
|
|
5
10
|
* panel (Model activity / Provided context), and to the Tester's result window holding the
|
|
6
11
|
* evidence its environment-lifecycle section lists. Both are built from the deployment's public
|
|
@@ -13,11 +18,12 @@
|
|
|
13
18
|
* is deleted rather than kept alongside it.
|
|
14
19
|
*/
|
|
15
20
|
import { watch } from 'vue'
|
|
21
|
+
import { useTaskReveal } from './useTaskReveal'
|
|
16
22
|
import { useUiStore } from '../stores/ui'
|
|
17
23
|
import { useWorkspaceStore } from '../stores/workspace'
|
|
18
24
|
|
|
19
25
|
/** The parsed link, or null when the URL carried none. */
|
|
20
|
-
interface PendingRunLink {
|
|
26
|
+
export interface PendingRunLink {
|
|
21
27
|
workspaceId: string | null
|
|
22
28
|
blockId: string | null
|
|
23
29
|
runId: string
|
|
@@ -50,13 +56,50 @@ export function captureRunDeepLink(): PendingRunLink | null {
|
|
|
50
56
|
return link
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** The view a link with no `view` opens: the task's running pipeline. */
|
|
60
|
+
export const RUN_LINK_DEFAULT_VIEW = 'run'
|
|
61
|
+
|
|
62
|
+
/** What applying a run link can do, so which link opens what is testable without a board. */
|
|
63
|
+
export interface RunLinkSurfaces {
|
|
64
|
+
revealTask: (blockId: string) => void
|
|
65
|
+
/** The task's focus view, with the pipeline of its current run. */
|
|
66
|
+
openRun: (blockId: string) => void
|
|
67
|
+
openOutcome: (runId: string, blockId: string | null) => void
|
|
68
|
+
openObservability: (runId: string) => void
|
|
69
|
+
openTestEvidence: (runId: string) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Apply a captured link to a hydrated board: the task brought into view, then its view. */
|
|
73
|
+
export function applyRunDeepLink(link: PendingRunLink, surfaces: RunLinkSurfaces): void {
|
|
74
|
+
if (link.blockId) surfaces.revealTask(link.blockId)
|
|
75
|
+
switch (link.view ?? RUN_LINK_DEFAULT_VIEW) {
|
|
76
|
+
case 'run':
|
|
77
|
+
if (link.blockId) surfaces.openRun(link.blockId)
|
|
78
|
+
break
|
|
79
|
+
// `outcome` is passed the block as well as the run: it is the one view that stays readable on
|
|
80
|
+
// a task whose run the snapshot no longer carries, which a link followed weeks later is.
|
|
81
|
+
case 'outcome':
|
|
82
|
+
surfaces.openOutcome(link.runId, link.blockId)
|
|
83
|
+
break
|
|
84
|
+
case 'observability':
|
|
85
|
+
surfaces.openObservability(link.runId)
|
|
86
|
+
break
|
|
87
|
+
case 'test-evidence':
|
|
88
|
+
surfaces.openTestEvidence(link.runId)
|
|
89
|
+
break
|
|
90
|
+
default:
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
/**
|
|
54
96
|
* Capture a run deep link and, once the board has hydrated, apply it. Returns nothing; the
|
|
55
97
|
* caller mounts it once, next to `workspace.init()`.
|
|
56
98
|
*
|
|
57
99
|
* Applying WAITS for `workspace.ready`: opening the panel against a half-loaded board would
|
|
58
|
-
* read an execution the snapshot hasn't delivered yet
|
|
59
|
-
*
|
|
100
|
+
* read an execution the snapshot hasn't delivered yet, and the camera waits further, for the
|
|
101
|
+
* canvas to have drawn the frame it centres on. The watcher is one-shot: a later refresh must
|
|
102
|
+
* not re-open a panel the user closed.
|
|
60
103
|
*/
|
|
61
104
|
export function useRunDeepLink(): void {
|
|
62
105
|
const link = captureRunDeepLink()
|
|
@@ -64,16 +107,17 @@ export function useRunDeepLink(): void {
|
|
|
64
107
|
|
|
65
108
|
const workspace = useWorkspaceStore()
|
|
66
109
|
const ui = useUiStore()
|
|
110
|
+
const { revealTask } = useTaskReveal()
|
|
67
111
|
|
|
68
112
|
// Pin the board the link names BEFORE init resolves the active one, so the run's own board
|
|
69
113
|
// opens rather than whichever one was last used on this device.
|
|
70
114
|
if (link.workspaceId) workspace.workspaceId = link.workspaceId
|
|
71
115
|
|
|
72
|
-
// Applied at most once
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
116
|
+
// Applied at most once. The guard is a flag rather than `{ once: true }` (which would burn the
|
|
117
|
+
// single run on the immediate not-ready tick and never navigate) and `stop` is a `let` rather
|
|
118
|
+
// than a `const` (with `immediate`, the callback runs synchronously BEFORE the binding
|
|
119
|
+
// initialises, so an already-hydrated board would hit the temporal dead zone instead of
|
|
120
|
+
// navigating).
|
|
77
121
|
let applied = false
|
|
78
122
|
let stop: (() => void) | undefined
|
|
79
123
|
stop = watch(
|
|
@@ -81,20 +125,13 @@ export function useRunDeepLink(): void {
|
|
|
81
125
|
(ready) => {
|
|
82
126
|
if (!ready || applied) return
|
|
83
127
|
applied = true
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// readable on a task whose run the snapshot no longer carries, and a link followed weeks
|
|
92
|
-
// after the work merged is exactly that case. The engine emits no `view=outcome` link
|
|
93
|
-
// today (its report links a REVIEWER to the two run-scoped panels); this is the entry
|
|
94
|
-
// point for one, and for a URL a person shares.
|
|
95
|
-
if (link.view === 'outcome') ui.openRunOutcome(link.runId, link.blockId)
|
|
96
|
-
else if (link.view === 'observability') ui.openObservability(link.runId)
|
|
97
|
-
else if (link.view === 'test-evidence') ui.openTestEvidence(link.runId)
|
|
128
|
+
applyRunDeepLink(link, {
|
|
129
|
+
revealTask,
|
|
130
|
+
openRun: (blockId) => ui.focus(blockId),
|
|
131
|
+
openOutcome: (runId, blockId) => ui.openRunOutcome(runId, blockId),
|
|
132
|
+
openObservability: (runId) => ui.openObservability(runId),
|
|
133
|
+
openTestEvidence: (runId) => ui.openTestEvidence(runId),
|
|
134
|
+
})
|
|
98
135
|
stop?.()
|
|
99
136
|
},
|
|
100
137
|
{ immediate: true },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { LOD_MIN_ZOOM, zoomToLod } from './useSemanticZoom'
|
|
3
|
+
|
|
4
|
+
describe('zoomToLod', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[0.2, 'far'],
|
|
7
|
+
[0.59, 'far'],
|
|
8
|
+
[0.6, 'mid'],
|
|
9
|
+
[1.19, 'mid'],
|
|
10
|
+
[1.2, 'close'],
|
|
11
|
+
[1.8, 'steps'],
|
|
12
|
+
[2.39, 'steps'],
|
|
13
|
+
[2.4, 'subtasks'],
|
|
14
|
+
[3, 'subtasks'],
|
|
15
|
+
] as const)('reads zoom %s as %s', (zoom, lod) => {
|
|
16
|
+
expect(zoomToLod(zoom)).toBe(lod)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('starts each level at its LOD_MIN_ZOOM', () => {
|
|
20
|
+
for (const [lod, zoom] of Object.entries(LOD_MIN_ZOOM)) expect(zoomToLod(zoom)).toBe(lod)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -3,6 +3,15 @@ import type { LodLevel } from '~/types/domain'
|
|
|
3
3
|
/** The LOD scale, shallow → deep. Index order lets callers ask "is at least". */
|
|
4
4
|
const LOD_ORDER: LodLevel[] = ['far', 'mid', 'close', 'steps', 'subtasks']
|
|
5
5
|
|
|
6
|
+
/** The zoom each level starts at; the level holds until the next one's. */
|
|
7
|
+
export const LOD_MIN_ZOOM: Record<LodLevel, number> = {
|
|
8
|
+
far: 0,
|
|
9
|
+
mid: 0.6,
|
|
10
|
+
close: 1.2,
|
|
11
|
+
steps: 1.8,
|
|
12
|
+
subtasks: 2.4,
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
/** Map a raw zoom factor to a level-of-detail bucket. Shared by the main board
|
|
7
16
|
* and the drill-down focus view so both honour the same thresholds.
|
|
8
17
|
*
|
|
@@ -11,11 +20,7 @@ const LOD_ORDER: LodLevel[] = ['far', 'mid', 'close', 'steps', 'subtasks']
|
|
|
11
20
|
* expands each step's live todo breakdown — the spatial analogue of opening a
|
|
12
21
|
* task in the inspector. */
|
|
13
22
|
export function zoomToLod(zoom: number): LodLevel {
|
|
14
|
-
|
|
15
|
-
if (zoom < 1.2) return 'mid'
|
|
16
|
-
if (zoom < 1.8) return 'close'
|
|
17
|
-
if (zoom < 2.4) return 'steps'
|
|
18
|
-
return 'subtasks'
|
|
23
|
+
return LOD_ORDER.findLast((level) => zoom >= LOD_MIN_ZOOM[level]) ?? 'far'
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/** True when `lod` is at least as deep as `min` on the LOD scale. */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useFramePlacement } from '~/composables/useFramePlacement'
|
|
2
|
+
import { useBoardStore } from '~/stores/board'
|
|
3
|
+
import { useUiStore } from '~/stores/ui'
|
|
4
|
+
import { useWorkViewStore } from '~/stores/workView'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bring a task in front of the person: the canvas rather than the queue, the task selected, and
|
|
8
|
+
* the camera centred on its card at the zoom where its pipeline steps render. What the queue's
|
|
9
|
+
* "show on board" does, and what a deep link naming a task does.
|
|
10
|
+
*/
|
|
11
|
+
export function useTaskReveal() {
|
|
12
|
+
const board = useBoardStore()
|
|
13
|
+
const ui = useUiStore()
|
|
14
|
+
const views = useWorkViewStore()
|
|
15
|
+
const { focusTaskWhenRendered } = useFramePlacement()
|
|
16
|
+
|
|
17
|
+
function revealTask(taskId: string): void {
|
|
18
|
+
views.show('board')
|
|
19
|
+
ui.select(taskId)
|
|
20
|
+
const task = board.getBlock(taskId)
|
|
21
|
+
if (!task) return
|
|
22
|
+
void focusTaskWhenRendered(taskId, board.serviceOf(task)?.id ?? taskId)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { revealTask }
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.320.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",
|