@djangocfg/ui-tools 2.1.409 → 2.1.412

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.
Files changed (83) hide show
  1. package/package.json +13 -13
  2. package/src/{tools/Chat/highlight → lib/browser-bridge}/README.md +46 -18
  3. package/src/lib/browser-bridge/commands/chat.ts +42 -0
  4. package/src/lib/browser-bridge/commands/highlight.ts +70 -0
  5. package/src/lib/browser-bridge/commands/index.ts +15 -0
  6. package/src/lib/browser-bridge/commands/inspect.ts +31 -0
  7. package/src/lib/browser-bridge/commands/scroll.ts +31 -0
  8. package/src/lib/browser-bridge/commands/write.ts +45 -0
  9. package/src/lib/browser-bridge/directive-bus.ts +120 -0
  10. package/src/lib/browser-bridge/index.ts +56 -0
  11. package/src/lib/browser-bridge/logger.ts +27 -0
  12. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/HighlightOverlay.tsx +14 -0
  13. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/HighlightOverlay.test.tsx +52 -0
  14. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/resolveRef.test.ts +39 -0
  15. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/index.ts +8 -5
  16. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/resolveRef.ts +5 -0
  17. package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/useHighlightTargets.ts +58 -27
  18. package/src/lib/browser-bridge/overlay/waitForVisible.ts +70 -0
  19. package/src/lib/browser-bridge/registry.ts +41 -0
  20. package/src/lib/browser-bridge/setBridgeResolver.ts +42 -0
  21. package/src/lib/browser-bridge/window.ts +76 -0
  22. package/src/lib/page-snapshot/capture/walk.ts +13 -5
  23. package/src/lib/page-snapshot/engine.ts +9 -4
  24. package/src/lib/page-snapshot/index.ts +5 -0
  25. package/src/lib/page-snapshot/react/provider.tsx +70 -3
  26. package/src/lib/page-snapshot/react/use-page-snapshot.ts +10 -0
  27. package/src/lib/page-snapshot/refs/__tests__/locator.test.ts +94 -0
  28. package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +59 -3
  29. package/src/lib/page-snapshot/refs/locator.ts +218 -0
  30. package/src/lib/page-snapshot/refs/registry.ts +29 -14
  31. package/src/tools/Chat/README.md +1 -1
  32. package/src/tools/Chat/composer/AttachContext.tsx +22 -0
  33. package/src/tools/Chat/composer/Composer.tsx +108 -6
  34. package/src/tools/Chat/composer/ComposerMenuButton.tsx +39 -2
  35. package/src/tools/Chat/composer/fileToAttachment.ts +53 -0
  36. package/src/tools/Chat/composer/index.ts +16 -1
  37. package/src/tools/Chat/composer/types.ts +71 -0
  38. package/src/tools/Chat/composer/useComposerAttach.tsx +218 -0
  39. package/src/tools/Chat/constants.ts +24 -1
  40. package/src/tools/Chat/context/ChatProvider.tsx +17 -2
  41. package/src/tools/Chat/core/logger.ts +15 -2
  42. package/src/tools/Chat/hooks/useChat.ts +32 -0
  43. package/src/tools/Chat/hooks/useChatComposer.ts +13 -0
  44. package/src/tools/Chat/index.ts +34 -2
  45. package/src/tools/Chat/launcher/ChatDock.tsx +13 -3
  46. package/src/tools/Chat/launcher/ChatFAB.tsx +4 -2
  47. package/src/tools/Chat/launcher/ChatGreeting.tsx +3 -2
  48. package/src/tools/Chat/launcher/ChatLauncher.tsx +42 -7
  49. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +3 -2
  50. package/src/tools/Chat/launcher/header/ChatHeader.tsx +2 -0
  51. package/src/tools/Chat/launcher/header/ChatHeaderActionButton.tsx +2 -0
  52. package/src/tools/Chat/launcher/header/ChatHeaderLanguageButton.tsx +2 -2
  53. package/src/tools/Chat/launcher/header/HeaderSlots.tsx +16 -9
  54. package/src/tools/Chat/lazy.tsx +34 -2
  55. package/src/tools/Chat/messages/MessageBubble.tsx +1 -1
  56. package/src/tools/Chat/public.ts +17 -0
  57. package/src/tools/Chat/settings/README.md +87 -0
  58. package/src/tools/Chat/settings/__tests__/useChatSettings.test.tsx +84 -0
  59. package/src/tools/Chat/settings/__tests__/useLocalStorage.test.tsx +138 -0
  60. package/src/tools/Chat/settings/index.ts +23 -0
  61. package/src/tools/Chat/settings/types.ts +108 -0
  62. package/src/tools/Chat/settings/useChatSettings.ts +168 -0
  63. package/src/tools/Chat/types/events.ts +50 -0
  64. package/src/tools/Chat/types/index.ts +1 -1
  65. package/src/tools/Chat/types/message.ts +5 -0
  66. package/src/tools/CronScheduler/CronScheduler.client.tsx +42 -15
  67. package/src/tools/CronScheduler/components/CustomInput.tsx +26 -7
  68. package/src/tools/CronScheduler/components/DayChips.tsx +20 -7
  69. package/src/tools/CronScheduler/components/MonthDayGrid.tsx +35 -10
  70. package/src/tools/CronScheduler/components/SchedulePreview.tsx +8 -5
  71. package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +12 -3
  72. package/src/tools/CronScheduler/components/TimeSelector.tsx +36 -13
  73. package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +4 -0
  74. package/src/tools/CronScheduler/context/hooks.ts +8 -0
  75. package/src/tools/CronScheduler/context/index.ts +1 -0
  76. package/src/tools/CronScheduler/index.tsx +2 -0
  77. package/src/tools/CronScheduler/lazy.tsx +1 -0
  78. package/src/tools/CronScheduler/types/index.ts +18 -1
  79. package/src/tools/Map/lazy.tsx +11 -4
  80. package/src/tools/Uploader/hooks/useClipboardPaste.ts +3 -1
  81. package/src/tools/index.ts +2 -0
  82. /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/SpotlightCanvas.tsx +0 -0
  83. /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/types.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.409",
3
+ "version": "2.1.412",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -154,13 +154,13 @@
154
154
  "test:watch": "vitest"
155
155
  },
156
156
  "peerDependencies": {
157
- "@djangocfg/i18n": "^2.1.409",
158
- "@djangocfg/ui-core": "^2.1.409",
157
+ "@djangocfg/i18n": "^2.1.412",
158
+ "@djangocfg/ui-core": "^2.1.412",
159
159
  "consola": "^3.4.2",
160
160
  "lodash-es": "^4.18.1",
161
161
  "lucide-react": "^0.545.0",
162
- "react": "^19.1.0",
163
- "react-dom": "^19.1.0",
162
+ "react": "^19.2.4",
163
+ "react-dom": "^19.2.4",
164
164
  "tailwindcss": "^4.1.18",
165
165
  "zustand": "^5.0.0"
166
166
  },
@@ -210,19 +210,19 @@
210
210
  "@maplibre/maplibre-gl-geocoder": "^1.7.0"
211
211
  },
212
212
  "devDependencies": {
213
- "@djangocfg/i18n": "^2.1.409",
214
- "@djangocfg/typescript-config": "^2.1.409",
215
- "@djangocfg/ui-core": "^2.1.409",
213
+ "@djangocfg/i18n": "^2.1.412",
214
+ "@djangocfg/typescript-config": "^2.1.412",
215
+ "@djangocfg/ui-core": "^2.1.412",
216
216
  "@types/lodash-es": "^4.17.12",
217
217
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
218
- "@types/node": "^24.7.2",
219
- "@types/react": "^19.1.0",
220
- "@types/react-dom": "^19.1.0",
218
+ "@types/node": "^25.2.3",
219
+ "@types/react": "^19.2.15",
220
+ "@types/react-dom": "^19.2.3",
221
221
  "jsdom": "^29.1.1",
222
222
  "lodash-es": "^4.18.1",
223
223
  "lucide-react": "^0.545.0",
224
- "react": "^19.1.0",
225
- "react-dom": "^19.1.0",
224
+ "react": "^19.2.4",
225
+ "react-dom": "^19.2.4",
226
226
  "tailwindcss": "^4.1.18",
227
227
  "tsup": "^8.5.0",
228
228
  "typescript": "^5.9.3",
@@ -1,8 +1,9 @@
1
- # Chat highlight — AI-driven `point` directives
1
+ # Chat bridgehow the AI drives the user's page
2
2
 
3
3
  When the assistant answers a question about the page, it can also
4
- **point at the screen** highlight and optionally focus the relevant
5
- elements. This module renders that.
4
+ **act on it** — point at the screen, move focus, scroll an element into
5
+ view. This module is that control surface, plus the overlay that draws
6
+ the spotlight.
6
7
 
7
8
  ---
8
9
 
@@ -22,8 +23,9 @@ the spotlight is the same, but *what* it points at is decided by the AI
22
23
  in context, not a hard-coded step list. The SVG renderer here is in
23
24
  fact adapted from that deleted Tour component.
24
25
 
25
- **Read-only.** A `point` directive highlights / focuses an element. It
26
- never types, clicks, or changes the user's data.
26
+ **Read-only by default.** A `point` directive highlights / focuses an
27
+ element; it never types, clicks, or changes the user's data. The
28
+ write-style commands (`fill`, `click`) are deliberately stubbed.
27
29
 
28
30
  ---
29
31
 
@@ -46,23 +48,42 @@ never types, clicks, or changes the user's data.
46
48
 
47
49
  ---
48
50
 
49
- ## Files
51
+ ## Layout
50
52
 
51
- | File | Responsibility |
52
- |------|----------------|
53
- | `types.ts` | `PointDirective`, `HighlightTarget`, `SpotlightRect`, `CSTRefId`. |
54
- | `resolveRef.ts` | `resolveRefs()` — CST ref → live DOM element via a `RefResolver`. Drops stale / detached refs. |
55
- | `useHighlightTargets.ts` | Hook: directives → geometry-tracked targets. Re-measures on scroll/resize, scrolls the first off-screen target in, focuses on request, drops a target when its element leaves the DOM. |
56
- | `SpotlightCanvas.tsx` | Pure SVG-mask renderer — dimmed scrim with rounded cut-outs, border, pulse ring. Takes geometry, not elements. |
57
- | `HighlightOverlay.tsx` | The component: hook + canvas + captions, portalled to `body`, auto-dismiss (TTL / Esc / scrim click). |
58
- | `index.ts` | Public barrel. |
53
+ The module has two layers — `browser-bridge/` is *how the AI drives the
54
+ browser*, `overlay/` is *how a highlight is drawn*.
55
+
56
+ ```
57
+ browser-bridge/
58
+ index.ts Public barrel.
59
+ registry.ts BridgeCommand interface + the command registry.
60
+ setBridgeResolver.ts Holds the live ref→element resolver the commands read.
61
+ directive-bus.ts SSE `directive` event → singleton bus.
62
+ window.ts installChatBridge — dev-only window.__chatBridge.
63
+ commands/
64
+ index.ts Registers + re-exports every built-in command.
65
+ highlight.ts highlight, focus, clear.
66
+ scroll.ts scrollTo.
67
+ inspect.ts inspect.
68
+ write.ts fill, click (stubs — need a confirmation gate).
69
+ overlay/
70
+ index.ts Overlay barrel.
71
+ types.ts PointDirective, HighlightTarget, SpotlightRect, CSTRefId.
72
+ resolveRef.ts resolveRefs() — CST ref → live element via a RefResolver.
73
+ useHighlightTargets.ts Hook: directives → geometry-tracked targets.
74
+ SpotlightCanvas.tsx Pure SVG-mask renderer (scrim, cut-outs, pulse).
75
+ HighlightOverlay.tsx The component: hook + canvas + captions.
76
+ ```
77
+
78
+ Commands are one-per-file: adding a capability is a new file in
79
+ `commands/`, registered in `commands/index.ts`.
59
80
 
60
81
  ---
61
82
 
62
83
  ## Usage
63
84
 
64
85
  ```tsx
65
- import { HighlightOverlay } from '@djangocfg/ui-tools/.../Chat/highlight';
86
+ import { HighlightOverlay } from '@djangocfg/ui-tools/chat';
66
87
 
67
88
  <HighlightOverlay
68
89
  directives={directivesFromLastDirectiveEvent}
@@ -78,6 +99,11 @@ import { HighlightOverlay } from '@djangocfg/ui-tools/.../Chat/highlight';
78
99
  `page-snapshot` engine's `RefRegistry` satisfies this. Structurally
79
100
  typed on purpose, so this module does not import the capture engine.
80
101
 
102
+ In development, `installChatBridge()` exposes the registry on
103
+ `window.__chatBridge` — run `__chatBridge.help()` for the command list,
104
+ or `__chatBridge.highlight(['@e4'])` to see the spotlight without an
105
+ AI / SSE round-trip.
106
+
81
107
  ---
82
108
 
83
109
  ## Staleness
@@ -91,9 +117,11 @@ only ever shows live elements.
91
117
 
92
118
  ## Decoupling notes
93
119
 
94
- - This module lives under `Chat/` because `point` is a chat feature and
95
- it has exactly one consumer. If a second consumer ever appears,
96
- promote it to a shared location not before.
120
+ - This module lives under `lib/` it is infrastructure ("the AI reads
121
+ and acts on the user's page"), the sibling of `lib/page-snapshot/`.
122
+ Chat is merely a consumer: it re-exports this barrel via
123
+ `@djangocfg/ui-tools/chat`. `lib/` must never import from `tools/`,
124
+ so the bridge carries its own minimal `logger.ts`.
97
125
  - It does not import the `page-snapshot` capture engine. It depends only
98
126
  on the structural `RefResolver` interface, so the two stay
99
127
  independent.
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Chat commands — drive the chat conversation from the bridge.
5
+ *
6
+ * `sendMessage` posts a message exactly as if typed into the composer
7
+ * (same transport, same SSE response). Useful for testing the chat —
8
+ * incl. the page-context highlight flow — from the devtools console
9
+ * without retyping prompts.
10
+ */
11
+
12
+ import { log } from '../logger';
13
+ import { registerBridgeCommand } from '../registry';
14
+ import { getActiveSender } from '../setBridgeResolver';
15
+
16
+ /**
17
+ * sendMessage — send a chat message programmatically.
18
+ *
19
+ * Goes through the same `sendMessage` the composer uses, so the full
20
+ * round-trip runs (transport, streamed reply, any directives). Resolves
21
+ * when the send is dispatched; the reply streams asynchronously after.
22
+ */
23
+ export const sendMessage = registerBridgeCommand({
24
+ name: 'sendMessage',
25
+ description: 'sendMessage(text) — send a chat message as if typed',
26
+ mutates: true,
27
+ run: async (text: string): Promise<{ sent: boolean }> => {
28
+ const sender = getActiveSender();
29
+ if (!sender) {
30
+ log.warn('bridge.sendMessage: no chat sender registered');
31
+ return { sent: false };
32
+ }
33
+ const content = String(text ?? '').trim();
34
+ if (!content) {
35
+ log.warn('bridge.sendMessage: empty message');
36
+ return { sent: false };
37
+ }
38
+ log.info('bridge.sendMessage', { chars: content.length });
39
+ await sender(content);
40
+ return { sent: true };
41
+ },
42
+ });
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Spotlight commands — `highlight`, `focus`, `clear`.
5
+ *
6
+ * All three drive the directive bus the AI uses, so the overlay reacts
7
+ * identically whether the source is an SSE event or a console call.
8
+ */
9
+
10
+ import { log } from '../logger';
11
+ import { pushDirectives, clearDirectives } from '../directive-bus';
12
+ import { registerBridgeCommand } from '../registry';
13
+ import type { CSTRefId, PointDirective } from '../overlay/types';
14
+
15
+ /** Coerce loose console input ("@e4" or ["@e4","@e5"]) into a ref list. */
16
+ function toRefList(input: CSTRefId | CSTRefId[]): CSTRefId[] {
17
+ return Array.isArray(input) ? input : [input];
18
+ }
19
+
20
+ /**
21
+ * highlight — spotlight one or more elements by CST ref.
22
+ * Drives the same directive bus the AI uses, so the overlay reacts
23
+ * identically whether the source is an SSE event or a console call.
24
+ */
25
+ export const highlight = registerBridgeCommand({
26
+ name: 'highlight',
27
+ description: 'highlight(refs, opts?) — spotlight element(s) by CST ref',
28
+ mutates: false,
29
+ run: (
30
+ refs: CSTRefId | CSTRefId[],
31
+ opts?: { focus?: boolean; label?: string },
32
+ ): { dispatched: number } => {
33
+ const list = toRefList(refs);
34
+ const directives: PointDirective[] = list.map((ref) => ({
35
+ type: 'point',
36
+ ref,
37
+ focus: opts?.focus,
38
+ label: opts?.label,
39
+ }));
40
+ log.info('bridge.highlight', { refs: list, opts });
41
+ pushDirectives(directives);
42
+ return { dispatched: directives.length };
43
+ },
44
+ });
45
+
46
+ /**
47
+ * focus — spotlight a single element AND move keyboard focus into it.
48
+ * A thin convenience over `highlight` with `focus: true`.
49
+ */
50
+ export const focus = registerBridgeCommand({
51
+ name: 'focus',
52
+ description: 'focus(ref, label?) — highlight an element and focus it',
53
+ mutates: false,
54
+ run: (ref: CSTRefId, label?: string): { dispatched: number } => {
55
+ log.info('bridge.focus', { ref, label });
56
+ pushDirectives([{ type: 'point', ref, focus: true, label }]);
57
+ return { dispatched: 1 };
58
+ },
59
+ });
60
+
61
+ /** clear — dismiss any active highlight overlay. */
62
+ export const clear = registerBridgeCommand({
63
+ name: 'clear',
64
+ description: 'clear() — remove the active highlight overlay',
65
+ mutates: false,
66
+ run: (): void => {
67
+ log.info('bridge.clear');
68
+ clearDirectives();
69
+ },
70
+ });
@@ -0,0 +1,15 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Built-in bridge commands.
5
+ *
6
+ * Importing this module registers every command into the shared
7
+ * registry (registration is a side effect of each command file). New
8
+ * capabilities are added as a new file here, re-exported below.
9
+ */
10
+
11
+ export { highlight, focus, clear } from './highlight';
12
+ export { scrollTo } from './scroll';
13
+ export { inspect } from './inspect';
14
+ export { fill, click } from './write';
15
+ export { sendMessage } from './chat';
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * inspect command — a read-only probe of ref freshness.
5
+ */
6
+
7
+ import { log } from '../logger';
8
+ import { registerBridgeCommand } from '../registry';
9
+ import { getActiveResolver } from '../setBridgeResolver';
10
+ import { resolveRefs } from '../overlay/resolveRef';
11
+ import type { CSTRefId } from '../overlay/types';
12
+
13
+ /**
14
+ * inspect — report which CST refs currently resolve to live elements.
15
+ * A read-only probe: useful for the AI (or a tester) to confirm the
16
+ * snapshot is still fresh before pointing at something.
17
+ */
18
+ export const inspect = registerBridgeCommand({
19
+ name: 'inspect',
20
+ description: 'inspect(refs) — report which refs resolve to live elements',
21
+ mutates: false,
22
+ run: (refs: CSTRefId[]): { resolved: CSTRefId[]; missing: CSTRefId[] } => {
23
+ const ok = new Set(resolveRefs(refs, getActiveResolver()).map((r) => r.ref));
24
+ const result = {
25
+ resolved: refs.filter((r) => ok.has(r)),
26
+ missing: refs.filter((r) => !ok.has(r)),
27
+ };
28
+ log.info('bridge.inspect', result);
29
+ return result;
30
+ },
31
+ });
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * scroll command — bring an element into view without a spotlight.
5
+ */
6
+
7
+ import { log } from '../logger';
8
+ import { registerBridgeCommand } from '../registry';
9
+ import { getActiveResolver } from '../setBridgeResolver';
10
+ import { resolveRefs } from '../overlay/resolveRef';
11
+ import type { CSTRefId } from '../overlay/types';
12
+
13
+ /**
14
+ * scrollTo — bring an element into view without drawing a spotlight.
15
+ * Resolves the ref directly (no overlay) and scrolls.
16
+ */
17
+ export const scrollTo = registerBridgeCommand({
18
+ name: 'scrollTo',
19
+ description: 'scrollTo(ref) — scroll an element into view',
20
+ mutates: false,
21
+ run: (ref: CSTRefId): { ok: boolean } => {
22
+ const [resolved] = resolveRefs([ref], getActiveResolver());
23
+ if (!resolved) {
24
+ log.warn('bridge.scrollTo: ref did not resolve', { ref });
25
+ return { ok: false };
26
+ }
27
+ resolved.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
28
+ log.info('bridge.scrollTo', { ref });
29
+ return { ok: true };
30
+ },
31
+ });
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Write-style command stubs — `fill`, `click`.
5
+ *
6
+ * Filling an input or clicking a control changes the user's data, so
7
+ * each needs an explicit confirmation gate before it can be enabled.
8
+ * They are registered (so they appear in `__chatBridge.help()`) but
9
+ * intentionally not implemented yet.
10
+ */
11
+
12
+ import { log } from '../logger';
13
+ import { registerBridgeCommand } from '../registry';
14
+ import type { CSTRefId } from '../overlay/types';
15
+
16
+ /**
17
+ * Stub for a future write-style command. Filling an input changes the
18
+ * user's data, so it needs an explicit confirmation gate before it can
19
+ * be enabled — intentionally not implemented yet.
20
+ */
21
+ export const fill = registerBridgeCommand({
22
+ name: 'fill',
23
+ description: 'fill(ref, value) — [not implemented] type into an input',
24
+ mutates: true,
25
+ run: (ref: CSTRefId, value: string): { ok: false; reason: string } => {
26
+ log.warn('bridge.fill: not implemented (needs a confirmation gate)', {
27
+ ref,
28
+ value,
29
+ });
30
+ return { ok: false, reason: 'not-implemented' };
31
+ },
32
+ });
33
+
34
+ /** Stub for a future write-style command — see `fill`. */
35
+ export const click = registerBridgeCommand({
36
+ name: 'click',
37
+ description: 'click(ref) — [not implemented] click an element',
38
+ mutates: true,
39
+ run: (ref: CSTRefId): { ok: false; reason: string } => {
40
+ log.warn('bridge.click: not implemented (needs a confirmation gate)', {
41
+ ref,
42
+ });
43
+ return { ok: false, reason: 'not-implemented' };
44
+ },
45
+ });
@@ -0,0 +1,120 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AI directive bus — receives `point` directives off the chat SSE
5
+ * stream and exposes them to the highlight overlay.
6
+ *
7
+ * The backend emits a `directive` SSE event carrying highlight/focus
8
+ * instructions keyed by CST ref ids. A host transport observes the raw
9
+ * frame and calls `pushDirectives`; the chat UI subscribes through
10
+ * `useChatDirectives` and feeds the result to `<HighlightOverlay>`.
11
+ *
12
+ * A singleton bus keeps the transport (which has no React context)
13
+ * decoupled from the consuming component.
14
+ */
15
+
16
+ import { useSyncExternalStore } from 'react';
17
+
18
+ import { log } from './logger';
19
+ import type { PointDirective } from './overlay/types';
20
+
21
+ // ─── Wire parsing ───────────────────────────────────────────────────
22
+
23
+ /** A CST ref looks like "@e4". */
24
+ const REF_RE = /^@e\d+$/;
25
+
26
+ /**
27
+ * Validate the raw `directives` array from a `directive` SSE frame.
28
+ *
29
+ * The backend already validates refs against the snapshot, but the wire
30
+ * is untyped at this boundary — drop anything malformed so a bad frame
31
+ * can never crash the overlay.
32
+ */
33
+ export function parseDirectives(raw: unknown): PointDirective[] {
34
+ if (!Array.isArray(raw)) {
35
+ log.warn('parseDirectives: payload is not an array', { raw });
36
+ return [];
37
+ }
38
+ const out: PointDirective[] = [];
39
+ let dropped = 0;
40
+ for (const item of raw) {
41
+ if (!item || typeof item !== 'object') {
42
+ dropped++;
43
+ continue;
44
+ }
45
+ const d = item as Record<string, unknown>;
46
+ if (d.type !== 'point') {
47
+ dropped++;
48
+ continue;
49
+ }
50
+ if (typeof d.ref !== 'string' || !REF_RE.test(d.ref)) {
51
+ log.warn('parseDirectives: dropped directive with bad ref', { ref: d.ref });
52
+ dropped++;
53
+ continue;
54
+ }
55
+ const directive: PointDirective = {
56
+ type: 'point',
57
+ ref: d.ref as PointDirective['ref'],
58
+ };
59
+ if (d.highlight === false) directive.highlight = false;
60
+ if (d.focus === true) directive.focus = true;
61
+ if (typeof d.label === 'string' && d.label.trim()) {
62
+ directive.label = d.label.trim();
63
+ }
64
+ out.push(directive);
65
+ }
66
+ log.info('parseDirectives', { in: raw.length, out: out.length, dropped });
67
+ return out;
68
+ }
69
+
70
+ // ─── Singleton bus ──────────────────────────────────────────────────
71
+
72
+ let current: PointDirective[] = [];
73
+ const subscribers = new Set<() => void>();
74
+
75
+ function notify(): void {
76
+ for (const s of subscribers) s();
77
+ }
78
+
79
+ /**
80
+ * Transport-side entry point. Replaces the active directive set — the
81
+ * latest `directive` event for a turn wins; an empty array clears the
82
+ * overlay. Called synchronously from a host transport's event handler.
83
+ */
84
+ export function pushDirectives(directives: PointDirective[]): void {
85
+ log.info('pushDirectives → bus', {
86
+ count: directives.length,
87
+ refs: directives.map((d) => d.ref),
88
+ subscribers: subscribers.size,
89
+ });
90
+ current = directives;
91
+ notify();
92
+ }
93
+
94
+ /** Clear the active directives (e.g. when the overlay is dismissed). */
95
+ export function clearDirectives(): void {
96
+ if (current.length === 0) return;
97
+ log.info('clearDirectives');
98
+ current = [];
99
+ notify();
100
+ }
101
+
102
+ function subscribe(cb: () => void): () => void {
103
+ subscribers.add(cb);
104
+ return () => subscribers.delete(cb);
105
+ }
106
+
107
+ function getSnapshot(): PointDirective[] {
108
+ return current;
109
+ }
110
+
111
+ /** Stable empty reference for the SSR snapshot. */
112
+ const EMPTY: PointDirective[] = [];
113
+
114
+ /**
115
+ * Subscribe to the active `point` directives. Re-renders the caller
116
+ * whenever a new `directive` event arrives or the set is cleared.
117
+ */
118
+ export function useChatDirectives(): PointDirective[] {
119
+ return useSyncExternalStore(subscribe, getSnapshot, () => EMPTY);
120
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Chat bridge — how the AI drives the user's live page.
5
+ *
6
+ * Two layers:
7
+ *
8
+ * - `bridge/` proper — the command registry, the built-in commands, and
9
+ * the directive transport (SSE → singleton bus). This is the AI's
10
+ * control surface over the browser.
11
+ * - `overlay/` — how a `point` directive is drawn: ref resolution,
12
+ * geometry tracking, and the SVG-mask spotlight.
13
+ *
14
+ * Read-only by default. Commands that would change the user's data
15
+ * (`fill`, `click`) are deliberately stubbed.
16
+ */
17
+
18
+ // Overlay — how a highlight is drawn.
19
+ export {
20
+ HighlightOverlay,
21
+ type HighlightOverlayProps,
22
+ SpotlightCanvas,
23
+ type SpotlightCanvasProps,
24
+ useHighlightTargets,
25
+ resolveRefs,
26
+ type RefResolver,
27
+ type PointDirective,
28
+ type HighlightTarget,
29
+ type SpotlightRect,
30
+ type CSTRefId,
31
+ } from './overlay';
32
+
33
+ // Directive transport — SSE `directive` event → singleton bus.
34
+ export {
35
+ useChatDirectives,
36
+ pushDirectives,
37
+ clearDirectives,
38
+ parseDirectives,
39
+ } from './directive-bus';
40
+
41
+ // Command registry + the ref resolver the commands read.
42
+ export {
43
+ registerBridgeCommand,
44
+ getBridgeCommand,
45
+ type BridgeCommand,
46
+ } from './registry';
47
+ export {
48
+ setBridgeResolver,
49
+ setBridgeSender,
50
+ type BridgeSender,
51
+ } from './setBridgeResolver';
52
+
53
+ // `window.__chatBridge` install (dev-gated). Importing this transitively
54
+ // pulls in `./commands`, whose modules register the built-in commands
55
+ // into the registry as a side effect.
56
+ export { installChatBridge, type ChatBridge } from './window';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Browser-bridge dev logger.
3
+ *
4
+ * A self-contained namespaced `consola` logger for the bridge subsystem
5
+ * (page-context `point` directives: SSE event → bus → overlay). It is a
6
+ * sibling of `lib/page-snapshot/`, so it must NOT reach up into `tools/` —
7
+ * the bridge is infrastructure, Chat is merely a consumer.
8
+ *
9
+ * Mirrors the gating of the chat logger (`tools/Chat/core/logger.ts`): the
10
+ * `chat:directives` sub-logger is silenced unless `isDev`, so noisy bridge
11
+ * events never leak into production builds. Kept local and minimal — only
12
+ * the `directives` scope is needed here.
13
+ */
14
+ import { consola } from 'consola';
15
+
16
+ import { isDev } from '@djangocfg/ui-core/lib';
17
+
18
+ /**
19
+ * Namespaced logger for bridge directives. Tagged `chat:directives` to stay
20
+ * consistent with the chat logger's scope naming. No-ops in production.
21
+ */
22
+ export const log = consola.withTag('chat').withTag('directives');
23
+
24
+ // Silence in production — bridge directive logs are dev-only diagnostics.
25
+ if (!isDev) {
26
+ log.level = -999;
27
+ }
@@ -3,6 +3,7 @@
3
3
  import { useEffect } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
 
6
+ import { log } from '../logger';
6
7
  import { SpotlightCanvas } from './SpotlightCanvas';
7
8
  import type { PointDirective, SpotlightRect } from './types';
8
9
  import { useHighlightTargets } from './useHighlightTargets';
@@ -40,6 +41,16 @@ export function HighlightOverlay({
40
41
  }: HighlightOverlayProps) {
41
42
  const targets = useHighlightTargets(directives, resolver);
42
43
 
44
+ // Trace why the overlay does or does not show — the common failure is
45
+ // directives arriving with no resolver, or refs that resolve to nothing.
46
+ useEffect(() => {
47
+ log.info('HighlightOverlay render', {
48
+ directives: directives.length,
49
+ hasResolver: resolver !== null,
50
+ resolvedTargets: targets.length,
51
+ });
52
+ }, [directives, resolver, targets.length]);
53
+
43
54
  // Auto-dismiss after the TTL once something is showing.
44
55
  useEffect(() => {
45
56
  if (targets.length === 0 || ttlMs <= 0) return;
@@ -67,6 +78,9 @@ export function HighlightOverlay({
67
78
 
68
79
  return createPortal(
69
80
  <div
81
+ // Above page content (sticky headers ~z-40) but below the chat
82
+ // companion/dock tiers (99/100) — it points at page elements, so it
83
+ // must not cover the chat or any ui-core overlay.
70
84
  className="pointer-events-none fixed inset-0 z-[60]"
71
85
  data-chat-highlight-overlay=""
72
86
  >