@schlessera/brain-ui-react 0.36.0 → 0.37.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.
Files changed (79) hide show
  1. package/dist/components/activity/activity-page.d.ts.map +1 -1
  2. package/dist/components/activity/activity-page.js +13 -9
  3. package/dist/components/activity/activity-page.js.map +1 -1
  4. package/dist/components/activity/approval-card.d.ts +19 -1
  5. package/dist/components/activity/approval-card.d.ts.map +1 -1
  6. package/dist/components/activity/approval-card.js +30 -3
  7. package/dist/components/activity/approval-card.js.map +1 -1
  8. package/dist/components/activity/span-bits.d.ts +11 -0
  9. package/dist/components/activity/span-bits.d.ts.map +1 -1
  10. package/dist/components/activity/span-bits.js +41 -0
  11. package/dist/components/activity/span-bits.js.map +1 -1
  12. package/dist/components/chat/chat-page.d.ts.map +1 -1
  13. package/dist/components/chat/chat-page.js +3 -2
  14. package/dist/components/chat/chat-page.js.map +1 -1
  15. package/dist/components/chat/tool-call-timeline.d.ts.map +1 -1
  16. package/dist/components/chat/tool-call-timeline.js +9 -3
  17. package/dist/components/chat/tool-call-timeline.js.map +1 -1
  18. package/dist/components/chat/tool-views.d.ts +6 -0
  19. package/dist/components/chat/tool-views.d.ts.map +1 -1
  20. package/dist/components/chat/tool-views.js +8 -0
  21. package/dist/components/chat/tool-views.js.map +1 -1
  22. package/dist/components/files/file-panel.js +1 -1
  23. package/dist/components/layout/side-rail.d.ts +5 -1
  24. package/dist/components/layout/side-rail.d.ts.map +1 -1
  25. package/dist/components/layout/side-rail.js +13 -6
  26. package/dist/components/layout/side-rail.js.map +1 -1
  27. package/dist/components/layout/slide-panel.d.ts +14 -1
  28. package/dist/components/layout/slide-panel.d.ts.map +1 -1
  29. package/dist/components/layout/slide-panel.js +5 -5
  30. package/dist/components/layout/slide-panel.js.map +1 -1
  31. package/dist/components/quick-actions/add-form.d.ts.map +1 -1
  32. package/dist/components/quick-actions/add-form.js +4 -2
  33. package/dist/components/quick-actions/add-form.js.map +1 -1
  34. package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
  35. package/dist/components/quick-actions/search-modal.js +4 -2
  36. package/dist/components/quick-actions/search-modal.js.map +1 -1
  37. package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
  38. package/dist/components/quick-actions/streaming-modal.js +15 -2
  39. package/dist/components/quick-actions/streaming-modal.js.map +1 -1
  40. package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -1
  41. package/dist/components/quick-actions/whatsup-modal.js +16 -1
  42. package/dist/components/quick-actions/whatsup-modal.js.map +1 -1
  43. package/dist/components/settings/settings-panel.d.ts.map +1 -1
  44. package/dist/components/settings/settings-panel.js +8 -8
  45. package/dist/components/settings/settings-panel.js.map +1 -1
  46. package/dist/hooks/use-fine-pointer.d.ts +12 -0
  47. package/dist/hooks/use-fine-pointer.d.ts.map +1 -0
  48. package/dist/hooks/use-fine-pointer.js +15 -0
  49. package/dist/hooks/use-fine-pointer.js.map +1 -0
  50. package/dist/hooks/use-media-query.d.ts +5 -3
  51. package/dist/hooks/use-media-query.d.ts.map +1 -1
  52. package/dist/hooks/use-media-query.js +11 -7
  53. package/dist/hooks/use-media-query.js.map +1 -1
  54. package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -1
  55. package/dist/hooks/websocket-handlers/chat.js +1 -1
  56. package/dist/hooks/websocket-handlers/chat.js.map +1 -1
  57. package/dist/stores/chat-state.d.ts +22 -1
  58. package/dist/stores/chat-state.d.ts.map +1 -1
  59. package/dist/stores/chat-state.js +12 -1
  60. package/dist/stores/chat-state.js.map +1 -1
  61. package/package.json +3 -3
  62. package/src/components/activity/activity-page.tsx +16 -9
  63. package/src/components/activity/approval-card.tsx +42 -5
  64. package/src/components/activity/span-bits.tsx +52 -0
  65. package/src/components/chat/chat-page.tsx +3 -2
  66. package/src/components/chat/tool-call-timeline.tsx +18 -6
  67. package/src/components/chat/tool-views.tsx +9 -0
  68. package/src/components/files/file-panel.tsx +1 -1
  69. package/src/components/layout/side-rail.tsx +13 -6
  70. package/src/components/layout/slide-panel.tsx +20 -4
  71. package/src/components/quick-actions/add-form.tsx +9 -1
  72. package/src/components/quick-actions/search-modal.tsx +10 -2
  73. package/src/components/quick-actions/streaming-modal.tsx +16 -2
  74. package/src/components/quick-actions/whatsup-modal.tsx +17 -1
  75. package/src/components/settings/settings-panel.tsx +11 -8
  76. package/src/hooks/use-fine-pointer.ts +15 -0
  77. package/src/hooks/use-media-query.ts +10 -7
  78. package/src/hooks/websocket-handlers/chat.ts +2 -1
  79. package/src/stores/chat-state.ts +29 -2
@@ -1,7 +1,7 @@
1
1
  import { Suspense, lazy } from "react";
2
2
  import { KeyRound, Laptop, Puzzle, SlidersHorizontal } from "lucide-react";
3
3
  import { useUIStore, type SettingsTab } from "../../stores/ui-store.js";
4
- import { SlidePanel } from "../layout/slide-panel.js";
4
+ import { SlidePanel, type SlidePanelClosedBy } from "../layout/slide-panel.js";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { usePrincipalStore } from "../../stores/principal-store.js";
7
7
  import { useBrainUiRoot } from "../../root-context.js";
@@ -73,10 +73,10 @@ export function SettingsPanel({
73
73
  const oneTimeCredential = usePrincipalStore((s) => s.oneTimeCredential);
74
74
  const credentialProtected = mintPending || oneTimeCredential !== null;
75
75
  const pane = useMediaQuery(PANE_QUERY);
76
-
77
- function closeIfSafe() {
78
- if (!credentialProtected) onClose();
79
- }
76
+ // While a mint is in flight or its one-time value is unacknowledged, the
77
+ // panel does not light-dismiss: a stray click or Escape must not hide the
78
+ // surface the credential is about to land on. Its close control stays live.
79
+ const closedBy = credentialProtected ? "none" : "any";
80
80
 
81
81
  function select(id: SettingsTab) {
82
82
  if (!credentialProtected) setTab(id);
@@ -88,8 +88,9 @@ export function SettingsPanel({
88
88
  open={open}
89
89
  tab={tab}
90
90
  credentialProtected={credentialProtected}
91
+ closedBy={closedBy}
91
92
  onSelect={select}
92
- onClose={closeIfSafe}
93
+ onClose={onClose}
93
94
  />
94
95
  );
95
96
  }
@@ -99,7 +100,7 @@ export function SettingsPanel({
99
100
  const stripTab: StripTab = tab === "appearance" ? "models" : tab;
100
101
 
101
102
  return (
102
- <SlidePanel open={open} onClose={closeIfSafe} title="Settings" wide>
103
+ <SlidePanel open={open} onClose={onClose} title="Settings" wide closedBy={closedBy}>
103
104
  <div className="flex h-full flex-col">
104
105
  <div className="flex shrink-0 gap-1 border-b border-border px-2 pt-2">
105
106
  {TABS.map(({ id, label, icon: TabIcon }) => (
@@ -158,18 +159,20 @@ function SettingsPane({
158
159
  open,
159
160
  tab,
160
161
  credentialProtected,
162
+ closedBy,
161
163
  onSelect,
162
164
  onClose,
163
165
  }: {
164
166
  open: boolean;
165
167
  tab: SettingsTab;
166
168
  credentialProtected: boolean;
169
+ closedBy: SlidePanelClosedBy;
167
170
  onSelect: (tab: SettingsTab) => void;
168
171
  onClose: () => void;
169
172
  }) {
170
173
  const appName = useBrainUiRoot().config.appName;
171
174
  return (
172
- <SlidePanel open={open} onClose={onClose} title="Settings" mode="pane">
175
+ <SlidePanel open={open} onClose={onClose} title="Settings" mode="pane" closedBy={closedBy}>
173
176
  {/* The kit header is `width: 100%`; it needs a shrinking flex child
174
177
  around it, or it fills the row and pushes Close past the viewport
175
178
  (the Files pane wraps its header the same way). */}
@@ -0,0 +1,15 @@
1
+ import { useMediaQuery } from "./use-media-query.js";
2
+
3
+ /**
4
+ * Whether any pointer is fine: the display heuristic chosen in #86 for
5
+ * shortcut hints on touch devices. This does not detect a keyboard. A paired
6
+ * mouse or trackpad updates the query live; a keyboard alone does not, so a
7
+ * tablet with a keyboard but no trackpad prints none of these hints — ruled acceptable,
8
+ * with the platform fact behind it (no media query reports a keyboard), in
9
+ * the D36 addendum of docs/decisions/design-kit.md.
10
+ * Only printed hints follow this signal. Bindings remain active either way.
11
+ * Without matchMedia (SSR or a test DOM), preserve the existing hints.
12
+ */
13
+ export function useFinePointer(): boolean {
14
+ return useMediaQuery("(any-pointer: fine)", true);
15
+ }
@@ -2,11 +2,13 @@ import { useEffect, useState } from "react";
2
2
 
3
3
  /**
4
4
  * Whether a media query matches, kept current as the window changes. Reads
5
- * `false` where there is no `matchMedia` — the server, and the test DOM —
6
- * so a component that branches on it renders its mobile shape there.
5
+ * `fallback` where there is no `matchMedia` — the server, and the test DOM.
6
+ * It defaults to `false`, so a component that branches on a width query
7
+ * renders its mobile shape there; a capability query that must fail open
8
+ * passes `true` (see `useFinePointer`).
7
9
  */
8
- export function useMediaQuery(query: string): boolean {
9
- const [matches, setMatches] = useState(() => read(query));
10
+ export function useMediaQuery(query: string, fallback = false): boolean {
11
+ const [matches, setMatches] = useState(() => read(query, fallback));
10
12
  useEffect(() => {
11
13
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
12
14
  let list: MediaQueryList;
@@ -23,10 +25,11 @@ export function useMediaQuery(query: string): boolean {
23
25
  return matches;
24
26
  }
25
27
 
26
- function read(query: string): boolean {
28
+ function read(query: string, fallback: boolean): boolean {
27
29
  try {
28
- return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(query).matches;
30
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return fallback;
31
+ return window.matchMedia(query).matches;
29
32
  } catch {
30
- return false;
33
+ return fallback;
31
34
  }
32
35
  }
@@ -163,7 +163,8 @@ export const chatFrameHandlers = {
163
163
  msg.toolName,
164
164
  msg.input,
165
165
  msg.description,
166
- msg.kind
166
+ msg.kind,
167
+ msg.rememberable
167
168
  );
168
169
  },
169
170
  tool_result: (msg, context) => {
@@ -65,6 +65,20 @@ export interface ToolCall {
65
65
  * allow"; "command" (a destructive-bash confirmation) is per-use only.
66
66
  */
67
67
  approvalKind?: "tool" | "command";
68
+ /**
69
+ * `false` when the host said it will not keep an "always allow" given on
70
+ * this card (#147) — the request is outside the turn's enforced allowlist.
71
+ * Absent means `approvalKind` alone decides. Read it through
72
+ * `offersAlwaysAllow`, not directly.
73
+ */
74
+ approvalRememberable?: false;
75
+ /**
76
+ * The approval request's description. For a "command" confirmation it is
77
+ * what the command will do, in words — the matched confirm pattern's
78
+ * effect (#112) — and the card draws it; for a "tool" grant it is the
79
+ * runtime's own text and is not drawn.
80
+ */
81
+ approvalDescription?: string;
68
82
  /**
69
83
  * Execution timing for the duration badge. `startedAt` is (re)stamped when
70
84
  * the input finishes streaming or an approval is granted — so approval
@@ -189,7 +203,8 @@ export interface ChatState {
189
203
  toolName: string,
190
204
  input: Record<string, unknown>,
191
205
  description?: string,
192
- kind?: "tool" | "command"
206
+ kind?: "tool" | "command",
207
+ rememberable?: boolean
193
208
  ) => void;
194
209
  resolveToolApproval: (key: ChatKey, toolUseId: string, approved: boolean) => void;
195
210
  setToolResult: (key: ChatKey, toolUseId: string, output: string, isError: boolean) => void;
@@ -290,6 +305,16 @@ function revokeAttachmentUrls(messages: ChatMessage[]) {
290
305
  }
291
306
  }
292
307
 
308
+ /**
309
+ * Whether a pending approval may offer "always allow": never for a
310
+ * destructive-command confirmation, and never when the host said it would
311
+ * not keep the grant. Both approval surfaces and the Actions receipt read
312
+ * this one rule, so the button and the receipt cannot disagree.
313
+ */
314
+ export function offersAlwaysAllow(tool: Pick<ToolCall, "approvalKind" | "approvalRememberable">): boolean {
315
+ return tool.approvalKind !== "command" && tool.approvalRememberable !== false;
316
+ }
317
+
293
318
  /**
294
319
  * Every tool call waiting on the user, across every buffer (D37: an approval
295
320
  * that arrived while you were reading something else cannot only exist as a
@@ -533,7 +558,7 @@ export function createChatStore(env: StoreEnvironment, provider: StoreApi<Provid
533
558
  ),
534
559
  })),
535
560
 
536
- requestToolApproval: (key, toolUseId, toolName, input, _description, kind) =>
561
+ requestToolApproval: (key, toolUseId, toolName, input, description, kind, rememberable) =>
537
562
  mutateLastAssistant(key, (last) => {
538
563
  // Check if tool call already exists (from streaming)
539
564
  const existingIdx = last.toolCalls.findIndex(
@@ -547,6 +572,8 @@ export function createChatStore(env: StoreEnvironment, provider: StoreApi<Provid
547
572
  inputJson: JSON.stringify(input, null, 2),
548
573
  status: "pending_approval",
549
574
  ...(kind ? { approvalKind: kind } : {}),
575
+ ...(rememberable === false ? { approvalRememberable: false } : {}),
576
+ ...(description ? { approvalDescription: description } : {}),
550
577
  };
551
578
  let parts = last.parts;
552
579
  if (existingIdx >= 0) {