update script element ordering in AGENTS

This commit is contained in:
swithek 2026-09-19 10:47:03 +02:00
parent 196c188320
commit 361605c93d
4 changed files with 136 additions and 120 deletions

View File

@ -37,6 +37,19 @@ arbitrary and is not. Test: does it still earn its lines for someone reading
the file a year from now with no idea what was asked? The same test applies
to the prose in these AGENTS.md files.
Keep comments as simple and short as possible, so one read is enough:
short sentences, plain words, no nested clauses or dashes. Good:
```ts
// the simulation ends once the metric has data, but the cache may
// still hold an empty result from before. Refetch, but only when the
// query can run, because refetch does not check that itself.
```
Bad: "the cached answer is keyed by the time range and not by the window it
resolves to, so the one waiting here was taken while the metric was still
missing — which is what the block has just been told it no longer is."
## 5. Goal-driven execution
Turn the task into a verifiable goal ("fix the bug" becomes a test that

View File

@ -184,6 +184,9 @@ sentinel or a restructure over a non-null assertion.
`CalendarInput.vue` → `CalendarInput.nuxt.test.ts`), never a mirror tree.
A test spanning two files lives with the one that initiates the
behaviour. Top-level `describe`s follow the source file's order.
- **Test file shape**: top-level constants and variables above the first
`describe`, never between tests; file-local helper functions below all
tests, at the bottom.
- **The suffix encodes the environment**, never `.spec.ts`, never an
`@vitest-environment` pragma:
@ -336,9 +339,13 @@ test context).
- **One assignment per statement** (`no-multi-assign`): no chaining, no
assignment inside an expression such as `const group = (groups[key] ??=
…)`. `??=` as its own statement is fine.
- `<script setup>` order: imports; `defineProps`/`defineEmits`/`defineExpose`;
store and composable initializations; `ref`/`computed`; lifecycle hooks;
- `<script setup>` order: imports; compiler macros (`defineProps`,
`defineEmits`, `defineModel`, `defineExpose`, …); store and composable
initializations; `ref`/`computed` and plain variables; lifecycle hooks;
watchers; functions.
- Plain `.ts` modules follow the same shape: imports, then constants and
module-level variables, then functions. No declarations between
functions.
- Tailwind utilities by default; custom rules go in `main.css` and are
applied by class. No `<style>` blocks or static inline `style=`.

View File

@ -52,36 +52,6 @@ const queryCache = useQueryCache()
const wsState = useWebSocketStateStore()
let unsubWsHooksChange: (() => void) | null | undefined = null
// the hook menus refetch after their own writes, but a hook the assistant or
// an MCP client adds arrives only on this topic: without it the indicators
// would sit stale until the list expired
watchImmediate(
() => editorStore.activeDocumentId,
(newId) => {
unsubWsHooksChange?.()
unsubWsHooksChange = null
if (!newId) {
return
}
unsubWsHooksChange = wsState.state?.subscribe(
makeWsHooksChangeTopic(newId),
(rawPayload) => {
const payload = rawPayload as WSHooksChangePayload
void queryCache.invalidateQueries({
key: DOCUMENT_HOOK_QUERY_KEYS.list(newId, payload.branchId),
})
},
)
},
)
onUnmounted(() => {
unsubWsHooksChange?.()
})
const userCaretDetails = computed(() => {
const caretColors = editorCaretColors()
const randomIndex = Math.floor(Math.random() * caretColors.length)
@ -136,8 +106,6 @@ import.meta.hot?.accept(() => {
refreshGapDecorationsInBackground(contentEditor as Ref<Editor | null>)
})
onBeforeUnmount(destroyBranchProviders)
// closing the tab never unmounts anything, so without this the provider is
// only noticed as gone when its socket drops — and until then everyone else
// still sees this user's caret. pagehide is the event that survives a tab
@ -151,6 +119,38 @@ useEventListener(window, "pagehide", (event: PageTransitionEvent) => {
destroyBranchProviders()
})
onBeforeUnmount(destroyBranchProviders)
onUnmounted(() => {
unsubWsHooksChange?.()
})
// the hook menus refetch after their own writes, but a hook the assistant or
// an MCP client adds arrives only on this topic: without it the indicators
// would sit stale until the list expired
watchImmediate(
() => editorStore.activeDocumentId,
(newId) => {
unsubWsHooksChange?.()
unsubWsHooksChange = null
if (!newId) {
return
}
unsubWsHooksChange = wsState.state?.subscribe(
makeWsHooksChangeTopic(newId),
(rawPayload) => {
const payload = rawPayload as WSHooksChangePayload
void queryCache.invalidateQueries({
key: DOCUMENT_HOOK_QUERY_KEYS.list(newId, payload.branchId),
})
},
)
},
)
watchImmediate(
[
() => editorStore.preloadedBranchIds,

View File

@ -73,6 +73,7 @@ const simulationPreset = computed(() => {
// NOTE: the debounced config cannot be changed (the changes won't be saved)
const debouncedConfig = ref(clone(props.config))
watchDebounced(
// keep the watch here because it acts as a config modifier
() => props.config,
(newV) => {
debouncedConfig.value = clone(newV)
@ -145,36 +146,12 @@ const fetchMetricData = useMultipleGenericQueries(
// draw generated data for good.
const clearedPreset = ref<MetricSimulationPreset | null>(null)
watch(simulationPreset, (preset, previous) => {
if (preset) {
clearedPreset.value = null
return
}
if (previous) {
clearedPreset.value = previous
// the cached answer for this query is keyed by the block's time
// range and not by the window it resolves to, so the one waiting
// here was taken while the metric was still missing — which is
// what the block has just been told it no longer is. refetch
// ignores whether the query may run, so it is asked only when it
// may.
if (queryRunnable.value) {
void fetchMetricData.refetch()
}
}
})
// set on every refresh tick so the generated window re-resolves against
// the current time and the chart slides forward like a live one
const simulationNow = ref<Date>()
// a block can only simulate what it will later query, so the same
// configuration a real query needs has to be in place first
// the same rows the query needs: a block that could simulate but never
// query would be stranded on generated data once the simulation lifted
// a block simulates only when its real query could run. Otherwise it would
// be stuck on generated data after the simulation ends.
const queryRunnable = computed(() => {
return canRunGenericQueries(
props.config.dataSourceId,
@ -187,7 +164,7 @@ const canSimulate = computed(() => {
})
// what the block draws, which outlives the attribute by one load
const drawnPreset = computed(() => {
const drawnSimulationPreset = computed(() => {
return (
simulationPreset.value ??
(fetchMetricData.asyncStatus.value === "loading"
@ -219,7 +196,7 @@ const data = computed<{
data?:
MultipleLineChartData | MultipleBarChartData | MultipleGaugeChartData | null
}>(() => {
const preset = drawnPreset.value
const preset = drawnSimulationPreset.value
if (preset) {
return mergeVisualizationResults(
props.config.visualizationType,
@ -257,6 +234,82 @@ const data = computed<{
)
})
useIntervalFn(
() => {
void refreshData()
},
() => refreshIntervalToMs(props.config.refreshInterval || RefreshInterval.M5),
{
immediate: true,
immediateCallback: false,
},
)
const simulationCheck = useIntervalFn(
() => {
void checkSimulation()
},
SIMULATION_CHECK_INTERVAL_MS,
{ immediate: false },
)
onMounted(async () => {
await refreshData()
setTimeout(() => {
canUseDebouncedConfigInParams.value = true
}, 3000)
})
watch(simulationPreset, (preset, previous) => {
if (preset) {
clearedPreset.value = null
return
}
if (previous) {
clearedPreset.value = previous
// the simulation ends once the metric has data, but the cache may
// still hold an empty result from before. Refetch, but only when the
// query can run, because refetch does not check that itself.
if (queryRunnable.value) {
void fetchMetricData.refetch()
}
}
})
// the document and branch ids arrive with the route, often after the
// block has mounted, so the first check waits for them rather than being
// skipped until the next tick
watchImmediate(
[
simulationPreset,
() => editorStore.activeDocumentId,
() => editorStore.activeBranchId,
],
([preset]) => {
if (!preset) {
simulationCheck.pause()
return
}
void checkSimulation()
simulationCheck.resume()
},
)
watch(
() => props.uid,
() => {
canUseDebouncedConfigInParams.value = false
setTimeout(() => {
canUseDebouncedConfigInParams.value = true
}, 3000)
},
)
// labels series the query gave no legend format for, counting up per
// merge call ("Line A", "Line B", ...)
function orderedLegendLabel() {
@ -283,63 +336,6 @@ function orderedLegendLabel() {
}
}
useIntervalFn(
() => {
void refreshData()
},
() => refreshIntervalToMs(props.config.refreshInterval || RefreshInterval.M5),
{
immediate: true,
immediateCallback: false,
},
)
const simulationCheck = useIntervalFn(
() => {
void checkSimulation()
},
SIMULATION_CHECK_INTERVAL_MS,
{ immediate: false },
)
// the document and branch ids arrive with the route, often after the
// block has mounted, so the first check waits for them rather than being
// skipped until the next tick
watchImmediate(
[
simulationPreset,
() => editorStore.activeDocumentId,
() => editorStore.activeBranchId,
],
([preset]) => {
if (!preset) {
simulationCheck.pause()
return
}
void checkSimulation()
simulationCheck.resume()
},
)
onMounted(async () => {
await refreshData()
setTimeout(() => {
canUseDebouncedConfigInParams.value = true
}, 3000)
})
watch(
() => props.uid,
() => {
canUseDebouncedConfigInParams.value = false
setTimeout(() => {
canUseDebouncedConfigInParams.value = true
}, 3000)
},
)
async function refreshData() {
if (
!editorStore.activeDocumentId ||
@ -421,7 +417,7 @@ function openModal() {
<template>
<div class="relative flex size-full min-w-0 items-center justify-center">
<div
v-if="drawnPreset"
v-if="drawnSimulationPreset"
class="pointer-events-none absolute top-2.5 left-1/2 z-1 flex -translate-x-1/2 items-center gap-1 text-2sm text-foreground/70"
>
<Icon name="lucide:hourglass" class="size-3" />