basalt-ui 1.8.0 → 1.10.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/AGENTS.md +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +64 -34
- package/agent/rules/basalt-agent.md +49 -6
- package/agent/rules/basalt-charts.md +74 -5
- package/configs/oxlint-plugin.js +279 -0
- package/configs/oxlint-plugin.test.ts +216 -1
- package/configs/oxlint.json +5 -2
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +1 -0
- package/dist/agent-chat/index.d.ts +4 -3
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/charts/hooks/useHoverSync.d.ts +18 -2
- package/dist/charts/hooks/useHoverSync.d.ts.map +1 -1
- package/dist/charts/hooks/useHoverSync.js +12 -7
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +6 -0
- package/dist/charts/kinds/Bars.d.ts +2 -0
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +3 -1
- package/dist/charts/kinds/Donut.d.ts +2 -0
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +8 -4
- package/dist/charts/kinds/DualPanel.d.ts +2 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +2 -1
- package/dist/charts/kinds/Heatmap.d.ts +8 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +7 -3
- package/dist/charts/kinds/MultiLine.d.ts +2 -0
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -1
- package/dist/charts/kinds/StackedArea.d.ts +2 -0
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -1
- package/dist/charts/kinds/ZonedLine.d.ts +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -1
- package/dist/charts/primitives/Axes.d.ts +6 -1
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +3 -2
- package/dist/charts/primitives/ChartFrame.d.ts +8 -1
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +8 -4
- package/dist/charts/primitives/ChartPending.d.ts +34 -0
- package/dist/charts/primitives/ChartPending.d.ts.map +1 -0
- package/dist/charts/primitives/ChartPending.js +23 -0
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -2
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts +5 -4
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +4 -2
- package/dist/cli/index.d.ts +55 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +217 -5
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +13 -5
- package/dist/surfaces.d.ts +10 -0
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +21 -0
- package/llms.txt +9 -2
- package/package.json +8 -5
- package/src/agent/ai-sdk-transport.test.ts +269 -0
- package/src/agent/parts.test.ts +261 -0
- package/src/agent/thread.test.ts +147 -0
- package/src/agent/use-agent-stream.test.tsx +231 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +261 -0
- package/src/agent/use-agent-thread-runs.test.tsx +398 -0
- package/src/agent/use-agent-thread-runs.ts +7 -1
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +299 -0
- package/src/agent-chat/index.ts +4 -3
- package/src/charts/hooks/useHoverSync.ts +43 -7
- package/src/charts/index.ts +6 -0
- package/src/charts/kinds/Bars.tsx +4 -0
- package/src/charts/kinds/Donut.tsx +10 -4
- package/src/charts/kinds/DualPanel.tsx +5 -1
- package/src/charts/kinds/Heatmap.tsx +19 -2
- package/src/charts/kinds/MultiLine.tsx +4 -1
- package/src/charts/kinds/StackedArea.tsx +4 -1
- package/src/charts/kinds/ZonedLine.tsx +4 -1
- package/src/charts/primitives/Axes.tsx +7 -1
- package/src/charts/primitives/ChartFrame.test.tsx +67 -0
- package/src/charts/primitives/ChartFrame.tsx +17 -4
- package/src/charts/primitives/ChartPending.tsx +59 -0
- package/src/charts/primitives/ChartTooltip.tsx +15 -3
- package/src/charts/primitives/HoverOverlay.tsx +12 -6
- package/src/cli/index.ts +439 -5
- package/src/cli/placement-engine.test.ts +104 -0
- package/src/dashboard/stat-card.test.tsx +5 -3
- package/src/dashboard/stat-card.tsx +12 -0
- package/src/guard/check-source.test.ts +20 -0
- package/src/guard/index.ts +20 -9
- package/src/provider/index.test.tsx +6 -5
- package/src/surfaces.ts +22 -0
- package/src/theme/use-basalt-spacing.test.tsx +7 -4
- package/src/tokens/build-fonts-css.test.ts +3 -2
package/configs/oxlint-plugin.js
CHANGED
|
@@ -14,6 +14,19 @@
|
|
|
14
14
|
* immediately above it. The three boundary rules deliberately do NOT — see their own comment for
|
|
15
15
|
* why.
|
|
16
16
|
*
|
|
17
|
+
* ── The two agent-chat guard rules (`agent-resume-guard` / `agent-no-raw-usechat`) + `ai-sdk-major` ──
|
|
18
|
+
* These are also correctness/architecture boundaries, not design guidance, so they do NOT honour
|
|
19
|
+
* `theme-allow`. All three instead honour their own line-comment escape, `basalt-agent-allow`
|
|
20
|
+
* (`hasAgentAllow`, a copy of `hasThemeAllow` with a different needle) — deliberately a separate
|
|
21
|
+
* token, so a color exemption can never double as a license to switch off a streaming guard or an
|
|
22
|
+
* intentional `ai` major skew. `ai-sdk-major`'s escape is line-level and per-import, distinct from
|
|
23
|
+
* `basalt.aiMajorSkewReason` — the repo-wide, mandatory-reason config exemption for the SAME skew
|
|
24
|
+
* read by `doctor`'s `ai-major-parity` check (`src/cli/index.ts`): a lint run only ever sees one
|
|
25
|
+
* file, so there's no reason string to echo here, just a marker on the import that intentionally
|
|
26
|
+
* crosses the declared skew. `ai-sdk-major` is the one rule in this file that touches the
|
|
27
|
+
* filesystem (reads ancestor `package.json`s, memoized per directory so a lint run does one walk
|
|
28
|
+
* per directory, not per file) — every other rule here is pure AST + filename-string matching.
|
|
29
|
+
*
|
|
17
30
|
* ── The three boundary rules (`visx-boundary` / `visx-tooltip` / `token-layer-boundary`) ─────────
|
|
18
31
|
* These used to be one bundled `import-boundary` rule sharing a single on/off toggle — that meant
|
|
19
32
|
* a consumer disabling the one check they disagreed with silently dropped the other two as well.
|
|
@@ -58,6 +71,9 @@
|
|
|
58
71
|
* oxlint's own parser accepts (see `ALLOWED_TOP_LEVEL_KEYS` in that script); do not add ad hoc keys
|
|
59
72
|
* to that file.
|
|
60
73
|
*/
|
|
74
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
75
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
76
|
+
import { fileURLToPath } from 'node:url'
|
|
61
77
|
|
|
62
78
|
// ── Shared helpers ──────────────────────────────────────────────────────────────────────────────
|
|
63
79
|
|
|
@@ -73,6 +89,23 @@ function hasThemeAllow(context, node) {
|
|
|
73
89
|
)
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
/**
|
|
93
|
+
* True when a `basalt-agent-allow` line comment sits on `node`'s own source line or the line above
|
|
94
|
+
* it. A copy of `hasThemeAllow` with a different needle — deliberately NOT the same token, so a
|
|
95
|
+
* color exemption can never double as a license to switch off a streaming guard (see the file-header
|
|
96
|
+
* comment for the two agent-chat rules).
|
|
97
|
+
*/
|
|
98
|
+
function hasAgentAllow(context, node) {
|
|
99
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
100
|
+
const comments = sourceCode?.getAllComments?.() ?? []
|
|
101
|
+
const nodeLine = node.loc.start.line
|
|
102
|
+
return comments.some(
|
|
103
|
+
(comment) =>
|
|
104
|
+
comment.value.includes('basalt-agent-allow') &&
|
|
105
|
+
(comment.loc.end.line === nodeLine || comment.loc.end.line === nodeLine - 1),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
function getFilename(context) {
|
|
77
110
|
return context.filename ?? context.getFilename?.() ?? ''
|
|
78
111
|
}
|
|
@@ -523,6 +556,249 @@ const tokenLayerBoundary = {
|
|
|
523
556
|
},
|
|
524
557
|
}
|
|
525
558
|
|
|
559
|
+
// ── Rule 8 — agent-resume-guard ─────────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
const AGENT_RESUME_GUARD_MESSAGE =
|
|
562
|
+
'Unguarded stream resume — useAgentThreadRuns owns single-consumer discipline and ' +
|
|
563
|
+
'StrictMode-safe reconnection; a raw resume re-fires on every effect re-run (vercel/ai#7891, no ' +
|
|
564
|
+
"merged fix). Mark the line 'basalt-agent-allow' if you own the guard. (basalt/agent-resume-guard)"
|
|
565
|
+
|
|
566
|
+
/** True for `resumeStream()` and `<anything>.resumeStream()` call expressions. */
|
|
567
|
+
function isResumeStreamCall(callee) {
|
|
568
|
+
if (callee.type === 'Identifier') return callee.name === 'resumeStream'
|
|
569
|
+
return (
|
|
570
|
+
callee.type === 'MemberExpression' &&
|
|
571
|
+
callee.property.type === 'Identifier' &&
|
|
572
|
+
callee.property.name === 'resumeStream'
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The `key` of an object Property as a string, covering both Identifier and Literal keys. */
|
|
577
|
+
function propertyKeyName(node) {
|
|
578
|
+
const key = node.key
|
|
579
|
+
if (key.type === 'Identifier') return key.name
|
|
580
|
+
if (key.type === 'Literal') return key.value
|
|
581
|
+
return undefined
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Deliberately NO `hasThemeAllow` escape — see basalt/visx-boundary's comment for why these
|
|
585
|
+
// guards use their own token (`basalt-agent-allow`, see the file-header comment) instead.
|
|
586
|
+
const agentResumeGuard = {
|
|
587
|
+
meta: {
|
|
588
|
+
type: 'problem',
|
|
589
|
+
docs: {
|
|
590
|
+
description:
|
|
591
|
+
'Disallow unguarded useChat({ resume: true }) / resumeStream() calls outside useAgentThreadRuns.',
|
|
592
|
+
},
|
|
593
|
+
schema: [],
|
|
594
|
+
},
|
|
595
|
+
create(context) {
|
|
596
|
+
return {
|
|
597
|
+
CallExpression(node) {
|
|
598
|
+
const callee = node.callee
|
|
599
|
+
|
|
600
|
+
if (callee.type === 'Identifier' && callee.name === 'useChat') {
|
|
601
|
+
const arg0 = node.arguments[0]
|
|
602
|
+
if (arg0 === undefined || arg0.type !== 'ObjectExpression') return
|
|
603
|
+
for (const prop of arg0.properties) {
|
|
604
|
+
if (prop.type !== 'Property') continue
|
|
605
|
+
if (propertyKeyName(prop) !== 'resume') continue
|
|
606
|
+
if (prop.value.type !== 'Literal' || prop.value.value !== true) continue
|
|
607
|
+
if (hasAgentAllow(context, prop)) continue
|
|
608
|
+
context.report({ node: prop, message: AGENT_RESUME_GUARD_MESSAGE })
|
|
609
|
+
}
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!isResumeStreamCall(callee)) return
|
|
614
|
+
if (hasAgentAllow(context, node)) return
|
|
615
|
+
context.report({ node, message: AGENT_RESUME_GUARD_MESSAGE })
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── Rule 9 — agent-no-raw-usechat ───────────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
const AGENT_NO_RAW_USECHAT_MESSAGE =
|
|
624
|
+
'Raw @ai-sdk/react useChat — use useAgentStream / useAgentThreadRuns over aiSdkTransport ' +
|
|
625
|
+
"(unmount abort, supersede guards, single-consumer resume). Mark the line 'basalt-agent-allow' to " +
|
|
626
|
+
'opt out. (basalt/agent-no-raw-usechat)'
|
|
627
|
+
|
|
628
|
+
const RAW_USE_CHAT_SOURCES = new Set(['@ai-sdk/react', 'ai/react'])
|
|
629
|
+
const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion'])
|
|
630
|
+
|
|
631
|
+
// Deliberately NO `hasThemeAllow` escape — see the file-header comment.
|
|
632
|
+
const agentNoRawUseChat = {
|
|
633
|
+
meta: {
|
|
634
|
+
type: 'problem',
|
|
635
|
+
docs: {
|
|
636
|
+
description:
|
|
637
|
+
'Disallow importing useChat/useCompletion directly from @ai-sdk/react or ai/react.',
|
|
638
|
+
},
|
|
639
|
+
schema: [],
|
|
640
|
+
},
|
|
641
|
+
create(context) {
|
|
642
|
+
return {
|
|
643
|
+
ImportDeclaration(node) {
|
|
644
|
+
const source = node.source?.value
|
|
645
|
+
if (typeof source !== 'string' || !RAW_USE_CHAT_SOURCES.has(source)) return
|
|
646
|
+
// A whole `import type { … } from '…'` is erased at compile time — nothing to guard.
|
|
647
|
+
if (node.importKind === 'type') return
|
|
648
|
+
|
|
649
|
+
for (const specifier of node.specifiers) {
|
|
650
|
+
if (specifier.type !== 'ImportSpecifier') continue
|
|
651
|
+
// `import { type useChat } from '…'` — the individual specifier is type-only.
|
|
652
|
+
if (specifier.importKind === 'type') continue
|
|
653
|
+
const imported = specifier.imported
|
|
654
|
+
const importedName = imported.type === 'Identifier' ? imported.name : imported.value
|
|
655
|
+
if (typeof importedName !== 'string' || !RAW_USE_CHAT_NAMES.has(importedName)) continue
|
|
656
|
+
if (hasAgentAllow(context, specifier)) continue
|
|
657
|
+
context.report({ node: specifier, message: AGENT_NO_RAW_USECHAT_MESSAGE })
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ── Rule 10 — ai-sdk-major ──────────────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* First run of digits in a semver range string (`^7.0.15` → `7`), or null if there is none.
|
|
668
|
+
*
|
|
669
|
+
* Duplicated verbatim in `src/cli/index.ts` — kept separate deliberately: this file must stay
|
|
670
|
+
* import-free from the package (it loads via `jsPlugins` out of a consumer's node_modules), so it
|
|
671
|
+
* cannot import the CLI's copy. Keep both copies in sync by hand; a future parsing fix (pre-release
|
|
672
|
+
* suffixes, say) must land in both.
|
|
673
|
+
*/
|
|
674
|
+
function majorOf(range) {
|
|
675
|
+
if (typeof range !== 'string') return null
|
|
676
|
+
const match = range.match(/\d+/)
|
|
677
|
+
return match === null ? null : Number(match[0])
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* `ai`'s declared major off one parsed package.json, checked in ONE fixed order shared by every
|
|
682
|
+
* reader in this file (basalt's own major below, and the linted file's nearest package.json in
|
|
683
|
+
* `aiMajorAtPackageJson`) — `dependencies` → `devDependencies` → `peerDependencies`. Chosen
|
|
684
|
+
* deliberately: `dependencies` is what actually gets installed and resolved at runtime, so it wins
|
|
685
|
+
* over a `devDependencies` pin (test-only) or a `peerDependencies` range (a floor, not a pin). Two
|
|
686
|
+
* readers disagreeing on this order is exactly the kind of skew this rule exists to catch — see
|
|
687
|
+
* `cli/index.ts`'s `aiMajorAt`, which uses the same order for doctor's cross-package walk.
|
|
688
|
+
*/
|
|
689
|
+
function aiMajorFromPkg(pkg) {
|
|
690
|
+
return majorOf(pkg.dependencies?.ai ?? pkg.devDependencies?.ai ?? pkg.peerDependencies?.ai)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* basalt-ui's OWN declared `ai` peer major, read once at module load from the package.json
|
|
695
|
+
* sitting next to this file's `configs/` directory — `../package.json` relative to this module,
|
|
696
|
+
* which resolves to `packages/basalt-ui/package.json` repo-locally AND to
|
|
697
|
+
* `node_modules/basalt-ui/package.json` when this file runs from a consumer's installed copy
|
|
698
|
+
* (`configs/oxlint-plugin.js` is one directory below the package root in both places). `null` when
|
|
699
|
+
* unreadable or undeclared — the rule then has nothing to compare against and stays silent.
|
|
700
|
+
*/
|
|
701
|
+
const PLUGIN_DIR = dirname(fileURLToPath(import.meta.url))
|
|
702
|
+
const BASALT_AI_MAJOR = (() => {
|
|
703
|
+
try {
|
|
704
|
+
const pkg = JSON.parse(readFileSync(resolvePath(PLUGIN_DIR, '..', 'package.json'), 'utf8'))
|
|
705
|
+
// Peer-first, unlike aiMajorFromPkg's dependencies-first order: `ai` is an optional PEER of
|
|
706
|
+
// basalt-ui, never its runtime dependency, so the "dependencies wins because runtime" rationale
|
|
707
|
+
// above does not apply to this self-read — a devDependencies test pin must not masquerade as
|
|
708
|
+
// the declared peer major.
|
|
709
|
+
return majorOf(pkg.peerDependencies?.ai ?? pkg.devDependencies?.ai ?? pkg.dependencies?.ai)
|
|
710
|
+
} catch {
|
|
711
|
+
return null
|
|
712
|
+
}
|
|
713
|
+
})()
|
|
714
|
+
|
|
715
|
+
// One JSON.parse per package.json path, and one directory-walk result per starting directory — so
|
|
716
|
+
// a lint run over N files in the same package does one filesystem walk total, not N.
|
|
717
|
+
const packageJsonMajorCache = new Map()
|
|
718
|
+
const nearestAiMajorCache = new Map()
|
|
719
|
+
|
|
720
|
+
function aiMajorAtPackageJson(pkgPath) {
|
|
721
|
+
if (packageJsonMajorCache.has(pkgPath)) return packageJsonMajorCache.get(pkgPath)
|
|
722
|
+
let major = null
|
|
723
|
+
try {
|
|
724
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
725
|
+
major = aiMajorFromPkg(pkg)
|
|
726
|
+
} catch {
|
|
727
|
+
major = null
|
|
728
|
+
}
|
|
729
|
+
packageJsonMajorCache.set(pkgPath, major)
|
|
730
|
+
return major
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Walks up from `startDir` to the nearest ancestor `package.json`'s declared `ai` major. */
|
|
734
|
+
function nearestAiMajor(startDir) {
|
|
735
|
+
if (nearestAiMajorCache.has(startDir)) return nearestAiMajorCache.get(startDir)
|
|
736
|
+
|
|
737
|
+
const visited = []
|
|
738
|
+
let dir = startDir
|
|
739
|
+
let result = null
|
|
740
|
+
for (;;) {
|
|
741
|
+
if (nearestAiMajorCache.has(dir)) {
|
|
742
|
+
result = nearestAiMajorCache.get(dir)
|
|
743
|
+
break
|
|
744
|
+
}
|
|
745
|
+
visited.push(dir)
|
|
746
|
+
const pkgPath = resolvePath(dir, 'package.json')
|
|
747
|
+
if (existsSync(pkgPath)) {
|
|
748
|
+
result = aiMajorAtPackageJson(pkgPath)
|
|
749
|
+
break
|
|
750
|
+
}
|
|
751
|
+
const parent = dirname(dir)
|
|
752
|
+
if (parent === dir) break // filesystem root — no package.json found
|
|
753
|
+
dir = parent
|
|
754
|
+
}
|
|
755
|
+
for (const d of visited) nearestAiMajorCache.set(d, result)
|
|
756
|
+
return result
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function aiSdkMajorMessage(consumerMajor) {
|
|
760
|
+
return (
|
|
761
|
+
`This file's nearest package.json declares ai@${consumerMajor}, but basalt-ui declares the ` +
|
|
762
|
+
`ai@${BASALT_AI_MAJOR} peer major — a producer/consumer pair on different ai majors can throw ` +
|
|
763
|
+
"'Unknown chunk type' at runtime (argo defect 1: apps/api on ai@5 vs apps/dashboard on ai@7). " +
|
|
764
|
+
'(basalt/ai-sdk-major)'
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Honours `basalt-agent-allow` (NOT `theme-allow`) like its two agent-chat siblings above — a
|
|
769
|
+
// version skew is still a fact about two package.json files, but a written, intentional
|
|
770
|
+
// producer/consumer pair (see `doctor`'s `aiMajorSkewReason`) needs a way to say so at the import
|
|
771
|
+
// site too, not just be permanently blocked.
|
|
772
|
+
const aiSdkMajor = {
|
|
773
|
+
meta: {
|
|
774
|
+
type: 'problem',
|
|
775
|
+
docs: {
|
|
776
|
+
description:
|
|
777
|
+
"Disallow importing 'ai'/'@ai-sdk/*' from a package whose declared ai major differs from basalt-ui's.",
|
|
778
|
+
},
|
|
779
|
+
schema: [],
|
|
780
|
+
},
|
|
781
|
+
create(context) {
|
|
782
|
+
return {
|
|
783
|
+
ImportDeclaration(node) {
|
|
784
|
+
if (BASALT_AI_MAJOR === null) return
|
|
785
|
+
const source = node.source?.value
|
|
786
|
+
if (typeof source !== 'string') return
|
|
787
|
+
if (source !== 'ai' && !source.startsWith('@ai-sdk/')) return
|
|
788
|
+
|
|
789
|
+
const filename = getFilename(context)
|
|
790
|
+
if (filename.length === 0) return
|
|
791
|
+
const consumerMajor = nearestAiMajor(dirname(filename))
|
|
792
|
+
if (consumerMajor === null) return // no `ai` in the nearest package.json — nothing to compare
|
|
793
|
+
if (consumerMajor === BASALT_AI_MAJOR) return
|
|
794
|
+
if (hasAgentAllow(context, node)) return
|
|
795
|
+
|
|
796
|
+
context.report({ node, message: aiSdkMajorMessage(consumerMajor) })
|
|
797
|
+
},
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
}
|
|
801
|
+
|
|
526
802
|
// ── Plugin export ───────────────────────────────────────────────────────────────────────────────
|
|
527
803
|
|
|
528
804
|
export default {
|
|
@@ -536,5 +812,8 @@ export default {
|
|
|
536
812
|
'visx-boundary': visxBoundary,
|
|
537
813
|
'visx-tooltip': visxTooltip,
|
|
538
814
|
'token-layer-boundary': tokenLayerBoundary,
|
|
815
|
+
'agent-resume-guard': agentResumeGuard,
|
|
816
|
+
'agent-no-raw-usechat': agentNoRawUseChat,
|
|
817
|
+
'ai-sdk-major': aiSdkMajor,
|
|
539
818
|
},
|
|
540
819
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* binary and returns the parsed set of `basalt/<rule>` diagnostics it printed.
|
|
9
9
|
*/
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
11
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { tmpdir } from 'node:os'
|
|
13
13
|
import { dirname, resolve } from 'node:path'
|
|
14
14
|
|
|
@@ -31,6 +31,9 @@ beforeEach(() => {
|
|
|
31
31
|
'basalt/visx-boundary': 'error',
|
|
32
32
|
'basalt/visx-tooltip': 'error',
|
|
33
33
|
'basalt/token-layer-boundary': 'error',
|
|
34
|
+
'basalt/agent-resume-guard': 'error',
|
|
35
|
+
'basalt/agent-no-raw-usechat': 'error',
|
|
36
|
+
'basalt/ai-sdk-major': 'error',
|
|
34
37
|
},
|
|
35
38
|
}),
|
|
36
39
|
)
|
|
@@ -232,3 +235,215 @@ describe('rule independence', () => {
|
|
|
232
235
|
expect(rules).toContain('visx-tooltip')
|
|
233
236
|
})
|
|
234
237
|
})
|
|
238
|
+
|
|
239
|
+
// ── agent-resume-guard ───────────────────────────────────────────────────────
|
|
240
|
+
// The two agent rules honour a SEPARATE escape token, `basalt-agent-allow`, not `theme-allow` — a
|
|
241
|
+
// color exemption must never be able to switch off a streaming guard. The `theme-allow` fixture
|
|
242
|
+
// below proves the wrong token does not suppress the finding.
|
|
243
|
+
|
|
244
|
+
describe('basalt/agent-resume-guard', () => {
|
|
245
|
+
it('flags useChat({ resume: true })', () => {
|
|
246
|
+
const { code, rules } = run(`const c = useChat({ id, resume: true })\n`)
|
|
247
|
+
expect(code).toBe(1)
|
|
248
|
+
expect(rules).toContain('agent-resume-guard')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('does NOT flag useChat({ resume: false })', () => {
|
|
252
|
+
const { code, rules } = run(`const c = useChat({ id, resume: false })\n`)
|
|
253
|
+
expect(code).toBe(0)
|
|
254
|
+
expect(rules).not.toContain('agent-resume-guard')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('does NOT flag a bare useChat({ id }) with no resume key', () => {
|
|
258
|
+
const { code, rules } = run(`const c = useChat({ id })\n`)
|
|
259
|
+
expect(code).toBe(0)
|
|
260
|
+
expect(rules).not.toContain('agent-resume-guard')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('flags chat.resumeStream() called from an effect', () => {
|
|
264
|
+
const { code, rules } = run(`useEffect(() => { chat.resumeStream() }, [])\n`)
|
|
265
|
+
expect(code).toBe(1)
|
|
266
|
+
expect(rules).toContain('agent-resume-guard')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('does NOT flag chat.resumeStream() marked basalt-agent-allow', () => {
|
|
270
|
+
const { code, rules } = run(`// basalt-agent-allow\nchat.resumeStream()\n`)
|
|
271
|
+
expect(code).toBe(0)
|
|
272
|
+
expect(rules).not.toContain('agent-resume-guard')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('STILL flags chat.resumeStream() marked only theme-allow (wrong escape token)', () => {
|
|
276
|
+
const { code, rules } = run(`// theme-allow\nchat.resumeStream()\n`)
|
|
277
|
+
expect(code).toBe(1)
|
|
278
|
+
expect(rules).toContain('agent-resume-guard')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('does NOT flag an unrelated call like runs.start(id, text)', () => {
|
|
282
|
+
const { code, rules } = run(`runs.start(id, text)\n`)
|
|
283
|
+
expect(code).toBe(0)
|
|
284
|
+
expect(rules).not.toContain('agent-resume-guard')
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// ── agent-no-raw-usechat ─────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
describe('basalt/agent-no-raw-usechat', () => {
|
|
291
|
+
it('flags importing useChat from @ai-sdk/react', () => {
|
|
292
|
+
const { code, rules } = run(`import { useChat } from '@ai-sdk/react'\n`, 'lib.ts')
|
|
293
|
+
expect(code).toBe(1)
|
|
294
|
+
expect(rules).toContain('agent-no-raw-usechat')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('flags importing useCompletion from @ai-sdk/react', () => {
|
|
298
|
+
const { code, rules } = run(`import { useCompletion } from '@ai-sdk/react'\n`, 'lib.ts')
|
|
299
|
+
expect(code).toBe(1)
|
|
300
|
+
expect(rules).toContain('agent-no-raw-usechat')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('does NOT flag a type-only import of UIMessage from the same module', () => {
|
|
304
|
+
const { code, rules } = run(`import type { UIMessage } from '@ai-sdk/react'\n`, 'lib.ts')
|
|
305
|
+
expect(code).toBe(0)
|
|
306
|
+
expect(rules).not.toContain('agent-no-raw-usechat')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('does NOT flag an unrelated import from ai', () => {
|
|
310
|
+
const { code, rules } = run(`import { DefaultChatTransport } from 'ai'\n`, 'lib.ts')
|
|
311
|
+
expect(code).toBe(0)
|
|
312
|
+
expect(rules).not.toContain('agent-no-raw-usechat')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('does NOT flag useChat imported from a local module', () => {
|
|
316
|
+
const { code, rules } = run(`import { useChat } from './my-hooks'\n`, 'lib.ts')
|
|
317
|
+
expect(code).toBe(0)
|
|
318
|
+
expect(rules).not.toContain('agent-no-raw-usechat')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// The matcher covers TWO source specifiers (RAW_USE_CHAT_SOURCES: '@ai-sdk/react' AND
|
|
322
|
+
// 'ai/react') — every fixture above only exercises '@ai-sdk/react'; this pins the other half.
|
|
323
|
+
it("flags importing useChat from 'ai/react' (the other matched source)", () => {
|
|
324
|
+
const { code, rules } = run(`import { useChat } from 'ai/react'\n`, 'lib.ts')
|
|
325
|
+
expect(code).toBe(1)
|
|
326
|
+
expect(rules).toContain('agent-no-raw-usechat')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('does NOT flag an import marked basalt-agent-allow', () => {
|
|
330
|
+
const { code, rules } = run(
|
|
331
|
+
`// basalt-agent-allow\nimport { useChat } from '@ai-sdk/react'\n`,
|
|
332
|
+
'lib.ts',
|
|
333
|
+
)
|
|
334
|
+
expect(code).toBe(0)
|
|
335
|
+
expect(rules).not.toContain('agent-no-raw-usechat')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('STILL flags an import marked only theme-allow (wrong escape token)', () => {
|
|
339
|
+
const { code, rules } = run(
|
|
340
|
+
`// theme-allow\nimport { useChat } from '@ai-sdk/react'\n`,
|
|
341
|
+
'lib.ts',
|
|
342
|
+
)
|
|
343
|
+
expect(code).toBe(1)
|
|
344
|
+
expect(rules).toContain('agent-no-raw-usechat')
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// ── ai-sdk-major ─────────────────────────────────────────────────────────────
|
|
349
|
+
// The lint rule compares the LINTED FILE's nearest package.json against basalt-ui's own declared
|
|
350
|
+
// `ai` peer major (read from packages/basalt-ui/package.json — the real file, since the plugin
|
|
351
|
+
// resolves it relative to its own module location regardless of where it's invoked from). basalt-ui
|
|
352
|
+
// declares "ai": "^7.0.15" (major 7) in both devDependencies and peerDependencies.
|
|
353
|
+
|
|
354
|
+
describe('basalt/ai-sdk-major', () => {
|
|
355
|
+
it("flags when the nearest package.json declares a different ai major (5 vs basalt's 7)", () => {
|
|
356
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '5.0.196' } }))
|
|
357
|
+
const { code, rules } = run(`import { streamText } from 'ai'\n`, 'lib.ts')
|
|
358
|
+
expect(code).toBe(1)
|
|
359
|
+
expect(rules).toContain('ai-sdk-major')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('does NOT flag when the nearest package.json declares the same major (^7.0.18)', () => {
|
|
363
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '^7.0.18' } }))
|
|
364
|
+
const { code, rules } = run(`import { streamText } from 'ai'\n`, 'lib.ts')
|
|
365
|
+
expect(code).toBe(0)
|
|
366
|
+
expect(rules).not.toContain('ai-sdk-major')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('does NOT flag when there is no ai in the nearest package.json', () => {
|
|
370
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: {} }))
|
|
371
|
+
const { code, rules } = run(`import { streamText } from 'ai'\n`, 'lib.ts')
|
|
372
|
+
expect(code).toBe(0)
|
|
373
|
+
expect(rules).not.toContain('ai-sdk-major')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('does NOT flag an unrelated import even with a skewed nearest package.json', () => {
|
|
377
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '5.0.196' } }))
|
|
378
|
+
const { code, rules } = run(`import { useState } from 'react'\n`, 'lib.ts')
|
|
379
|
+
expect(code).toBe(0)
|
|
380
|
+
expect(rules).not.toContain('ai-sdk-major')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// The matcher covers TWO forms (source === 'ai' OR source.startsWith('@ai-sdk/')) — every
|
|
384
|
+
// fixture above only exercises the bare 'ai' specifier; this pins the scoped-package half.
|
|
385
|
+
it("flags a scoped '@ai-sdk/*' import when the nearest package.json declares a different ai major", () => {
|
|
386
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '5.0.196' } }))
|
|
387
|
+
const { code, rules } = run(`import { openai } from '@ai-sdk/openai'\n`, 'lib.ts')
|
|
388
|
+
expect(code).toBe(1)
|
|
389
|
+
expect(rules).toContain('ai-sdk-major')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it("does NOT flag a scoped '@ai-sdk/*' import when the nearest package.json declares the same major", () => {
|
|
393
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '^7.0.18' } }))
|
|
394
|
+
const { code, rules } = run(`import { openai } from '@ai-sdk/openai'\n`, 'lib.ts')
|
|
395
|
+
expect(code).toBe(0)
|
|
396
|
+
expect(rules).not.toContain('ai-sdk-major')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// Unlike the other rules in this file, ai-sdk-major honours `basalt-agent-allow` — the SAME
|
|
400
|
+
// token as its two agent-chat siblings above — not `theme-allow`. A skewed producer/consumer
|
|
401
|
+
// import that is genuinely intentional (see doctor's `aiMajorSkewReason`) can mark the line.
|
|
402
|
+
it('does NOT flag an import marked basalt-agent-allow, even with a skewed nearest package.json', () => {
|
|
403
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '5.0.196' } }))
|
|
404
|
+
const { code, rules } = run(
|
|
405
|
+
`// basalt-agent-allow\nimport { streamText } from 'ai'\n`,
|
|
406
|
+
'lib.ts',
|
|
407
|
+
)
|
|
408
|
+
expect(code).toBe(0)
|
|
409
|
+
expect(rules).not.toContain('ai-sdk-major')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('STILL flags an import marked only theme-allow (wrong escape token)', () => {
|
|
413
|
+
writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ dependencies: { ai: '5.0.196' } }))
|
|
414
|
+
const { code, rules } = run(`// theme-allow\nimport { streamText } from 'ai'\n`, 'lib.ts')
|
|
415
|
+
expect(code).toBe(1)
|
|
416
|
+
expect(rules).toContain('ai-sdk-major')
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
// ── raw-scroll-container — promotion (off → warn shipped) ────────────────────
|
|
421
|
+
|
|
422
|
+
describe('basalt/raw-scroll-container promotion', () => {
|
|
423
|
+
it('ships "warn" (not "off") in the consumer preset', () => {
|
|
424
|
+
const shipped = JSON.parse(
|
|
425
|
+
readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8'),
|
|
426
|
+
) as { rules: Record<string, unknown> }
|
|
427
|
+
expect(shipped.rules['basalt/raw-scroll-container']).toBe('warn')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('stays "warn" repo-local', () => {
|
|
431
|
+
const repoLocal = JSON.parse(
|
|
432
|
+
readFileSync(resolve(import.meta.dirname, '..', '..', '..', '.oxlintrc.json'), 'utf8'),
|
|
433
|
+
) as { rules: Record<string, unknown> }
|
|
434
|
+
expect(repoLocal.rules['basalt/raw-scroll-container']).toBe('warn')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('still flags a raw overflow:auto style property at the shipped level', () => {
|
|
438
|
+
writeFileSync(
|
|
439
|
+
resolve(dir, '.oxlintrc.json'),
|
|
440
|
+
JSON.stringify({
|
|
441
|
+
plugins: [],
|
|
442
|
+
jsPlugins: [PLUGIN_PATH],
|
|
443
|
+
rules: { 'basalt/raw-scroll-container': 'warn' },
|
|
444
|
+
}),
|
|
445
|
+
)
|
|
446
|
+
const { rules } = run(`const s = { overflowY: 'auto' }\n`, 'lib.ts')
|
|
447
|
+
expect(rules).toContain('raw-scroll-container')
|
|
448
|
+
})
|
|
449
|
+
})
|
package/configs/oxlint.json
CHANGED
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
"basalt/raw-size-literal": "warn",
|
|
21
21
|
"basalt/card-inset": "error",
|
|
22
22
|
"basalt/chart-in-raw-surface": "error",
|
|
23
|
-
"basalt/raw-scroll-container": "
|
|
23
|
+
"basalt/raw-scroll-container": "warn",
|
|
24
24
|
"basalt/visx-boundary": "error",
|
|
25
|
-
"basalt/visx-tooltip": "error"
|
|
25
|
+
"basalt/visx-tooltip": "error",
|
|
26
|
+
"basalt/agent-resume-guard": "warn",
|
|
27
|
+
"basalt/agent-no-raw-usechat": "warn",
|
|
28
|
+
"basalt/ai-sdk-major": "warn"
|
|
26
29
|
},
|
|
27
30
|
"overrides": [
|
|
28
31
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AA0ID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,GAAG,SAAS,EAAE,EACpD,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"use-agent-thread-runs.d.ts","sourceRoot":"","sources":["../../src/agent/use-agent-thread-runs.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,WAAW,CAAA;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,UAAU,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,GAAG,SAAS,IAAI;IAC9C,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,CAAA;CACxB,CAAA;AAID,MAAM,MAAM,sBAAsB,CAAC,KAAK,GAAG,SAAS,IAAI;IACtD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EACd,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC,KAAK,CAAC,CAAA;IAC/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,EAAE,CAAA;CAClD,CAAA;AAID,MAAM,MAAM,wBAAwB,CAAC,KAAK,GAAG,SAAS,IAAI;IACxD,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;IACzD,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAC7B,CAAA;AA0ID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,GAAG,SAAS,EAAE,EACpD,SAAS,EACT,KAAK,EACL,cAAc,EACd,WAAW,GACZ,EAAE,sBAAsB,CAAC,KAAK,CAAC,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAmLjE"}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Mantine-styled thread-chat components over the headless `basalt-ui/agent` layer, capped by the
|
|
3
3
|
* flagship `ThreadWorkspace` composite.
|
|
4
4
|
*
|
|
5
|
-
* This module is Mantine-coupled by design and
|
|
6
|
-
*
|
|
5
|
+
* This module is Mantine-coupled by design and ships its own `basalt-ui/agent-chat` subpath
|
|
6
|
+
* (also re-exported from the root entry, unchanged for existing consumers) — unlike
|
|
7
|
+
* `basalt-ui/agent`, which stays headless so it can be styled by any consumer.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
|
-
* import { ThreadWorkspace } from 'basalt-ui'
|
|
10
|
+
* import { ThreadWorkspace } from 'basalt-ui/agent-chat'
|
|
10
11
|
* import { createThreadsStore, edenTransport, heuristicOutcome } from 'basalt-ui/agent'
|
|
11
12
|
*/
|
|
12
13
|
export { ThreadWorkspace } from './thread-workspace.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent-chat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAG9D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAGpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAGnE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -12,16 +12,32 @@ type XScale = (x: string) => number | undefined;
|
|
|
12
12
|
* closest-point loop inline — drift across 15+ charts is the exact problem
|
|
13
13
|
* this hook prevents.
|
|
14
14
|
*/
|
|
15
|
-
export declare function useHoverSync<T>({ data, chartId, getKey, xScale, marginLeft, }: {
|
|
15
|
+
export declare function useHoverSync<T>({ data, chartId, getKey, xScale, marginLeft, resolveKey, }: {
|
|
16
16
|
data: T[];
|
|
17
17
|
chartId: string;
|
|
18
18
|
getKey: (d: T) => string;
|
|
19
19
|
xScale: XScale;
|
|
20
20
|
marginLeft: number;
|
|
21
|
+
/**
|
|
22
|
+
* Overrides how a SIBLING's broadcast key resolves to one of THIS chart's own points. Only
|
|
23
|
+
* affects reading another chart's hover — this chart's own hover (via `getKey`/`xScale`) is
|
|
24
|
+
* untouched.
|
|
25
|
+
*
|
|
26
|
+
* Cross-chart sync is exact string match by default (`pointByKey.get(ctx.key)`), so two charts
|
|
27
|
+
* share a cursor only if they emit identical key strings. A chart that downsamples/folds its
|
|
28
|
+
* domain to fit a narrow viewport (e.g. ~288 buckets folded into ~97 columns at 390px) no longer
|
|
29
|
+
* owns most of the keys its unfolded siblings broadcast — the shared crosshair then appears on
|
|
30
|
+
* roughly one hover in three, with no rule a reader can infer, which is worse than no shared
|
|
31
|
+
* cursor at all. Pass `resolveKey` to map a foreign key onto whichever of this chart's own points
|
|
32
|
+
* swallowed it (e.g. the folded bucket the key falls into).
|
|
33
|
+
*
|
|
34
|
+
* Omit it and behaviour is byte-identical to today.
|
|
35
|
+
*/
|
|
36
|
+
resolveKey?: (key: string) => T | null;
|
|
21
37
|
}): {
|
|
22
38
|
tip: import("./useChartTooltip.js").TooltipState<T>;
|
|
23
39
|
tooltipRef: import("react").RefObject<HTMLDivElement | null>;
|
|
24
|
-
syncedPoint:
|
|
40
|
+
syncedPoint: T | null;
|
|
25
41
|
isDirectHover: boolean;
|
|
26
42
|
handleMouse: (event: React.MouseEvent<SVGRectElement>) => void;
|
|
27
43
|
handleLeave: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHoverSync.d.ts","sourceRoot":"","sources":["../../../src/charts/hooks/useHoverSync.ts"],"names":[],"mappings":"AAKA,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAC9B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,MAAM,EACN,UAAU,GACX,EAAE;IACD,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"useHoverSync.d.ts","sourceRoot":"","sources":["../../../src/charts/hooks/useHoverSync.ts"],"names":[],"mappings":"AAKA,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,EAC9B,IAAI,EACJ,OAAO,EACP,MAAM,EACN,MAAM,EACN,UAAU,EACV,UAAU,GACX,EAAE;IACD,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,IAAI,CAAA;CACvC;;;;;yBA6CW,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC;;EAyD3C"}
|