@tai42/studio-sdk 17.1.2 → 18.0.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 (77) hide show
  1. package/dist/components/execution-key-picker.d.ts +37 -0
  2. package/dist/components/execution-key-picker.d.ts.map +1 -0
  3. package/dist/components/execution-key-picker.js +49 -0
  4. package/dist/components/execution-key-picker.js.map +1 -0
  5. package/dist/components/json-tree-nodes.d.ts.map +1 -1
  6. package/dist/components/json-tree-nodes.js +24 -9
  7. package/dist/components/json-tree-nodes.js.map +1 -1
  8. package/dist/components/overflow-measure.d.ts +11 -3
  9. package/dist/components/overflow-measure.d.ts.map +1 -1
  10. package/dist/components/overflow-measure.js +8 -5
  11. package/dist/components/overflow-measure.js.map +1 -1
  12. package/dist/components/scroll-region.d.ts +12 -2
  13. package/dist/components/scroll-region.d.ts.map +1 -1
  14. package/dist/components/scroll-region.js +7 -5
  15. package/dist/components/scroll-region.js.map +1 -1
  16. package/dist/components/subject-section.d.ts +78 -0
  17. package/dist/components/subject-section.d.ts.map +1 -0
  18. package/dist/components/subject-section.js +66 -0
  19. package/dist/components/subject-section.js.map +1 -0
  20. package/dist/components/tool-picker.d.ts.map +1 -1
  21. package/dist/components/tool-picker.js +11 -1
  22. package/dist/components/tool-picker.js.map +1 -1
  23. package/dist/hooks/useApi.d.ts +96 -45
  24. package/dist/hooks/useApi.d.ts.map +1 -1
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/schema-form/array-field.d.ts.map +1 -1
  30. package/dist/schema-form/array-field.js +3 -3
  31. package/dist/schema-form/array-field.js.map +1 -1
  32. package/dist/schema-form/context.d.ts +19 -0
  33. package/dist/schema-form/context.d.ts.map +1 -1
  34. package/dist/schema-form/context.js.map +1 -1
  35. package/dist/schema-form/field-model.d.ts +19 -0
  36. package/dist/schema-form/field-model.d.ts.map +1 -1
  37. package/dist/schema-form/field-node.d.ts +10 -3
  38. package/dist/schema-form/field-node.d.ts.map +1 -1
  39. package/dist/schema-form/field-node.js +16 -2
  40. package/dist/schema-form/field-node.js.map +1 -1
  41. package/dist/schema-form/keyed-callbacks.d.ts +2 -0
  42. package/dist/schema-form/keyed-callbacks.d.ts.map +1 -0
  43. package/dist/schema-form/keyed-callbacks.js +45 -0
  44. package/dist/schema-form/keyed-callbacks.js.map +1 -0
  45. package/dist/schema-form/object-fields.d.ts.map +1 -1
  46. package/dist/schema-form/object-fields.js +5 -3
  47. package/dist/schema-form/object-fields.js.map +1 -1
  48. package/dist/schema-form/record-field.d.ts.map +1 -1
  49. package/dist/schema-form/record-field.js +4 -6
  50. package/dist/schema-form/record-field.js.map +1 -1
  51. package/dist/schema-form/string-annotations.d.ts.map +1 -1
  52. package/dist/schema-form/string-annotations.js +26 -0
  53. package/dist/schema-form/string-annotations.js.map +1 -1
  54. package/dist/schema-form/string-field.js +22 -6
  55. package/dist/schema-form/string-field.js.map +1 -1
  56. package/dist/testing.d.ts +9 -7
  57. package/dist/testing.d.ts.map +1 -1
  58. package/dist/testing.js +16 -13
  59. package/dist/testing.js.map +1 -1
  60. package/package.json +3 -3
  61. package/src/components/execution-key-picker.tsx +116 -0
  62. package/src/components/json-tree-nodes.tsx +36 -18
  63. package/src/components/overflow-measure.ts +16 -5
  64. package/src/components/scroll-region.tsx +16 -3
  65. package/src/components/subject-section.tsx +177 -0
  66. package/src/components/tool-picker.tsx +14 -2
  67. package/src/index.ts +11 -0
  68. package/src/schema-form/array-field.tsx +3 -3
  69. package/src/schema-form/context.ts +20 -0
  70. package/src/schema-form/field-model.ts +20 -0
  71. package/src/schema-form/field-node.tsx +11 -3
  72. package/src/schema-form/keyed-callbacks.ts +46 -0
  73. package/src/schema-form/object-fields.tsx +8 -3
  74. package/src/schema-form/record-field.tsx +7 -6
  75. package/src/schema-form/string-annotations.ts +32 -1
  76. package/src/schema-form/string-field.tsx +25 -6
  77. package/src/testing.ts +21 -13
package/dist/testing.d.ts CHANGED
@@ -28,16 +28,18 @@ export declare function flushResizeObservers(): void;
28
28
  */
29
29
  export declare function flushResizeObserversFor(target: Element): void;
30
30
  /**
31
- * Makes `element` report itself as horizontally overflowing (or not). jsdom runs
32
- * no layout, so `scrollWidth` and `clientWidth` are both 0 and every
33
- * overflow test reads as "fits"; this shadows the prototype getters with own,
34
- * configurable values so a test can drive either state. Call it again with the
35
- * opposite flag to flip the element back.
31
+ * Makes `element` report itself as overflowing (or not) on `axis`. jsdom runs no
32
+ * layout, so `scroll*` and `client*` are both 0 and every overflow test reads as
33
+ * "fits"; this shadows the prototype getters with own, configurable values so a
34
+ * test can drive either state. Call it again with the opposite flag to flip the
35
+ * element back.
36
36
  *
37
37
  * @param element - the element whose scroll metrics to fake.
38
- * @param overflowing - true to report content wider than the box.
38
+ * @param overflowing - true to report content larger than the box on `axis`.
39
+ * @param axis - the dimension to fake: `horizontal` (width, the default) or
40
+ * `vertical` (height).
39
41
  */
40
- export declare function setElementOverflow(element: HTMLElement, overflowing: boolean): void;
42
+ export declare function setElementOverflow(element: HTMLElement, overflowing: boolean, axis?: 'horizontal' | 'vertical'): void;
41
43
  /**
42
44
  * jsdom omits a handful of browser APIs the Studio components reach for while a
43
45
  * test drives them. Installing every stub in one place keeps each feature's
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAC9G,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,0FAA0F;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,qFAAqF;AACrF,OAAO,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAC;AAuE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAK7D;AAMD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,GAAG,IAAI,CAMnF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CA4GxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,KAAK;IAC7B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC,CAQA"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAC9G,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,0FAA0F;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,qFAAqF;AACrF,OAAO,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAC;AAuE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAK7D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,OAAO,EACpB,IAAI,GAAE,YAAY,GAAG,UAAyB,GAC7C,IAAI,CAQN;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CA4GxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,KAAK;IAC7B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC,CAQA"}
package/dist/testing.js CHANGED
@@ -88,24 +88,27 @@ export function flushResizeObserversFor(target) {
88
88
  live.callback([resizeObserverEntry(target)], live.observer);
89
89
  }
90
90
  }
91
- /** The faked widths: equal reads as "fits", unequal as "scrolls". */
92
- const FITTING_WIDTH = 100;
93
- const OVERFLOWING_SCROLL_WIDTH = 400;
91
+ /** The faked extents: equal reads as "fits", unequal as "scrolls". */
92
+ const FITTING_EXTENT = 100;
93
+ const OVERFLOWING_SCROLL_EXTENT = 400;
94
94
  /**
95
- * Makes `element` report itself as horizontally overflowing (or not). jsdom runs
96
- * no layout, so `scrollWidth` and `clientWidth` are both 0 and every
97
- * overflow test reads as "fits"; this shadows the prototype getters with own,
98
- * configurable values so a test can drive either state. Call it again with the
99
- * opposite flag to flip the element back.
95
+ * Makes `element` report itself as overflowing (or not) on `axis`. jsdom runs no
96
+ * layout, so `scroll*` and `client*` are both 0 and every overflow test reads as
97
+ * "fits"; this shadows the prototype getters with own, configurable values so a
98
+ * test can drive either state. Call it again with the opposite flag to flip the
99
+ * element back.
100
100
  *
101
101
  * @param element - the element whose scroll metrics to fake.
102
- * @param overflowing - true to report content wider than the box.
102
+ * @param overflowing - true to report content larger than the box on `axis`.
103
+ * @param axis - the dimension to fake: `horizontal` (width, the default) or
104
+ * `vertical` (height).
103
105
  */
104
- export function setElementOverflow(element, overflowing) {
105
- Object.defineProperty(element, 'clientWidth', { configurable: true, value: FITTING_WIDTH });
106
- Object.defineProperty(element, 'scrollWidth', {
106
+ export function setElementOverflow(element, overflowing, axis = 'horizontal') {
107
+ const [client, scroll] = axis === 'vertical' ? ['clientHeight', 'scrollHeight'] : ['clientWidth', 'scrollWidth'];
108
+ Object.defineProperty(element, client, { configurable: true, value: FITTING_EXTENT });
109
+ Object.defineProperty(element, scroll, {
107
110
  configurable: true,
108
- value: overflowing ? OVERFLOWING_SCROLL_WIDTH : FITTING_WIDTH,
111
+ value: overflowing ? OVERFLOWING_SCROLL_EXTENT : FITTING_EXTENT,
109
112
  });
110
113
  }
111
114
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tai42/studio-sdk",
3
- "version": "17.1.2",
3
+ "version": "18.0.0",
4
4
  "description": "The Studio plugin contract, shared UI components, and hooks that every feature builds on.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "tai42 <oss@tai42.ai>",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@microsoft/api-extractor": "7.58.12",
76
- "@tai42/jq-studio": "^0.1.3",
76
+ "@tai42/jq-studio": "^0.1.6",
77
77
  "@testing-library/jest-dom": "^6.9.1",
78
78
  "@testing-library/react": "^16.3.2",
79
79
  "@testing-library/user-event": "^14.6.6",
@@ -96,7 +96,7 @@
96
96
  "@radix-ui/react-select": "^2.3.2",
97
97
  "@radix-ui/react-tabs": "^1.1.21",
98
98
  "@radix-ui/react-tooltip": "^1.2.11",
99
- "@tai42/api-client": "17.1.2"
99
+ "@tai42/api-client": "18.0.0"
100
100
  },
101
101
  "scripts": {
102
102
  "build": "tsc -b tsconfig.build.json && node scripts/copy-assets.mjs",
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Picks the api key a fire / recurring run RUNS AS. Shared by every fire-path form
3
+ * (hooks, trigger links, schedules). Presentational and prop-driven — the host
4
+ * feature owns the api-key list query and hands it in, so this component (and the
5
+ * SDK) stay free of a data-fetching library, exactly as {@link ToolPicker} does.
6
+ *
7
+ * The server decides authorization; this only lists the pickable keys and renders
8
+ * the read's loading / empty / error states. The host form renders the server's
9
+ * refusal verbatim elsewhere.
10
+ */
11
+ import type { TokensPayload } from '@tai42/api-client';
12
+ import type { ReactNode } from 'react';
13
+
14
+ import { errorMessage } from '../errors';
15
+ import { Field } from './field';
16
+ import { ErrorState } from './primitives';
17
+ import { Select } from './select';
18
+
19
+ /**
20
+ * The subset of the api-key list query this picker reads. A caller's TanStack
21
+ * `UseQueryResult<TokensPayload>` satisfies it structurally, so the SDK names no
22
+ * query library.
23
+ */
24
+ export interface ExecutionKeyQuery {
25
+ readonly data: TokensPayload | undefined;
26
+ readonly isPending: boolean;
27
+ readonly isError: boolean;
28
+ readonly isSuccess: boolean;
29
+ readonly error: unknown;
30
+ readonly refetch: () => void;
31
+ }
32
+
33
+ /** Resolved, and holding no key at all. */
34
+ export function isExecutionKeyListEmpty(query: ExecutionKeyQuery): boolean {
35
+ return query.isSuccess && (query.data?.length ?? 0) === 0;
36
+ }
37
+
38
+ /** The mint's stable fingerprint (never key material), nested under `policy_data`. */
39
+ function keyFingerprint(key: TokensPayload[number]): string | undefined {
40
+ const data = key.policy_data;
41
+ if (typeof data !== 'object' || data === null) return undefined;
42
+ const value = (data as Record<string, unknown>).key_fingerprint;
43
+ return typeof value === 'string' ? value : undefined;
44
+ }
45
+
46
+ /** A key's label: its id, plus whichever of description / fingerprint it has. */
47
+ function keyLabel(key: TokensPayload[number]): string {
48
+ const suffix = [key.description, keyFingerprint(key)]
49
+ .filter((part) => part !== undefined && part !== '')
50
+ .join(' · ');
51
+ return suffix === '' ? key.user_id : `${key.user_id} — ${suffix}`;
52
+ }
53
+
54
+ /** One option per `user_id` (what a binding names), first occurrence wins. */
55
+ function pickableKeys(keys: TokensPayload): TokensPayload {
56
+ const seen = new Set<string>();
57
+ return keys.filter((key) => {
58
+ if (seen.has(key.user_id)) return false;
59
+ seen.add(key.user_id);
60
+ return true;
61
+ });
62
+ }
63
+
64
+ export interface ExecutionKeyPickerProps {
65
+ /** The api-key list read the host feature owns and hands in. */
66
+ readonly query: ExecutionKeyQuery;
67
+ readonly value: string;
68
+ readonly onValueChange: (value: string) => void;
69
+ /** The host form's required-field error, shown under the control. */
70
+ readonly error?: string | undefined;
71
+ }
72
+
73
+ export function ExecutionKeyPicker({
74
+ query,
75
+ value,
76
+ onValueChange,
77
+ error,
78
+ }: ExecutionKeyPickerProps): ReactNode {
79
+ const listEmpty = isExecutionKeyListEmpty(query);
80
+
81
+ return (
82
+ <Field
83
+ label="Execution key"
84
+ description="The api key this fire runs AS. Every tool call it makes is authorized against that key's live grants — prefer a least-privilege service key over a human's broad one."
85
+ // Suppressed: the empty note / the ErrorState already say it.
86
+ error={listEmpty || query.isError ? undefined : error}
87
+ // Only the Select branch claims the field's control id; the error and
88
+ // empty branches render no labelable element, so `for` would dangle.
89
+ group={query.isError || listEmpty}
90
+ >
91
+ {query.isError ? (
92
+ <ErrorState
93
+ message={errorMessage(query.error)}
94
+ onRetry={() => {
95
+ query.refetch();
96
+ }}
97
+ />
98
+ ) : listEmpty ? (
99
+ <p role="status" style={{ margin: 0, color: 'var(--tai-color-text-muted)' }}>
100
+ No api keys available to run as. Mint one on the settings API-keys tab first.
101
+ </p>
102
+ ) : (
103
+ <Select
104
+ options={pickableKeys(query.data ?? []).map((key) => ({
105
+ value: key.user_id,
106
+ label: keyLabel(key),
107
+ }))}
108
+ value={value}
109
+ placeholder={query.isPending ? 'Loading keys…' : 'Select an execution key'}
110
+ disabled={query.isPending}
111
+ onValueChange={onValueChange}
112
+ />
113
+ )}
114
+ </Field>
115
+ );
116
+ }
@@ -40,8 +40,19 @@ const summaryStyle: CSSProperties = {
40
40
  cursor: 'pointer',
41
41
  };
42
42
 
43
- /** The copy control sits at the far end of a disclosure's row. */
44
- const nodeCopyStyle: CSSProperties = { marginLeft: 'auto' };
43
+ /**
44
+ * A disclosure and its copy control share one row: the disclosure fills it and the
45
+ * copy control sits at the far end, top-aligned with the summary so it stays on the
46
+ * summary's line even when the open node's children run tall below it.
47
+ */
48
+ const nodeRowStyle: CSSProperties = {
49
+ display: 'flex',
50
+ alignItems: 'flex-start',
51
+ gap: 'var(--tai-space-2)',
52
+ };
53
+
54
+ /** The disclosure takes the row's width, leaving the copy control at its end. */
55
+ const nodeDetailsStyle: CSSProperties = { flex: 1, minWidth: 0 };
45
56
 
46
57
  /**
47
58
  * The per-node copy button's geometry: borrows `tai-icon-btn` for the focus ring,
@@ -89,11 +100,7 @@ export function CopyButton({ value, label, variant }: CopyButtonProps): ReactEle
89
100
  };
90
101
  }, []);
91
102
 
92
- const handleClick = (event: ReactMouseEvent<HTMLButtonElement>): void => {
93
- // Inside a `<summary>` a click both toggles the disclosure natively and reaches
94
- // the summary's own toggle handler; neither should fire for a copy.
95
- event.preventDefault();
96
- event.stopPropagation();
103
+ const handleClick = (): void => {
97
104
  void copy(value).then((ok) => {
98
105
  if (!ok || !mounted.current) return;
99
106
  setCopied(true);
@@ -117,7 +124,7 @@ export function CopyButton({ value, label, variant }: CopyButtonProps): ReactEle
117
124
  <button
118
125
  type="button"
119
126
  className="tai-icon-btn"
120
- style={{ ...nodeCopyStyle, ...nodeCopyButtonStyle }}
127
+ style={nodeCopyButtonStyle}
121
128
  aria-label={copied ? COPIED_LABEL : label}
122
129
  onClick={handleClick}
123
130
  >
@@ -196,23 +203,34 @@ export function JsonNode({ name, value, depth, path }: NodeProps): ReactElement
196
203
  setOpen(path, !open);
197
204
  };
198
205
 
199
- return (
200
- <details open={open}>
206
+ const disclosure = (
207
+ <details open={open} style={depth > 0 ? nodeDetailsStyle : undefined}>
201
208
  <summary style={summaryStyle} onClick={handleToggle}>
202
209
  {name !== undefined ? <span className="tai-syntax-key">{name}: </span> : null}
203
210
  <span className="tai-muted">{summary}</span>
204
- {/* The root's copy-node control would duplicate the toolbar's copy-whole. */}
205
- {depth > 0 ? (
206
- <CopyButton
207
- value={value}
208
- variant="icon"
209
- label={name !== undefined ? `Copy ${name}` : COPY_LABEL}
210
- />
211
- ) : null}
212
211
  </summary>
213
212
  {open ? <NodeChildren value={value} count={count} depth={depth} path={path} /> : null}
214
213
  </details>
215
214
  );
215
+
216
+ // The root's copy-node control would duplicate the toolbar's copy-whole.
217
+ if (depth === 0) return disclosure;
218
+
219
+ // The copy control is a SIBLING of the disclosure, never a child of its
220
+ // interactive `<summary>`, so no interactive control nests inside another
221
+ // (WCAG 4.1.2 name/role/value — a nested-interactive control has no reliable
222
+ // accessible role). Both the disclosure toggle and the copy button stay
223
+ // independently keyboard operable.
224
+ return (
225
+ <div style={nodeRowStyle}>
226
+ {disclosure}
227
+ <CopyButton
228
+ value={value}
229
+ variant="icon"
230
+ label={name !== undefined ? `Copy ${name}` : COPY_LABEL}
231
+ />
232
+ </div>
233
+ );
216
234
  }
217
235
 
218
236
  return (
@@ -7,12 +7,23 @@
7
7
 
8
8
  export const SCROLL_REGION_CLASS = 'tai-scroll-region';
9
9
 
10
- export function overflows(element: HTMLElement): boolean {
11
- return element.scrollWidth > element.clientWidth;
10
+ /**
11
+ * The axis a scroll region is measured on. A prose table or a code line scrolls
12
+ * SIDEWAYS (`horizontal`, the default); a bounded, capped-height list scrolls
13
+ * DOWN (`vertical`). Both wear the same conditional region attributes — only the
14
+ * dimension read to decide whether they overflow differs.
15
+ */
16
+ export type OverflowAxis = 'horizontal' | 'vertical';
17
+
18
+ export function overflows(element: HTMLElement, axis: OverflowAxis = 'horizontal'): boolean {
19
+ return axis === 'vertical'
20
+ ? element.scrollHeight > element.clientHeight
21
+ : element.scrollWidth > element.clientWidth;
12
22
  }
13
23
 
14
24
  /**
15
- * Whether the element should carry the region attributes right now.
25
+ * Whether the element should carry the region attributes right now, measured on
26
+ * `axis`.
16
27
  *
17
28
  * A region that stops overflowing keeps its tab stop for as long as it holds
18
29
  * focus: taking `tabindex` off the focused element drops the reader onto the
@@ -20,8 +31,8 @@ export function overflows(element: HTMLElement): boolean {
20
31
  * already has the stop can be the active element, so this can hold a stop open
21
32
  * but never invent one.
22
33
  */
23
- export function needsRegion(element: HTMLElement): boolean {
24
- return overflows(element) || element.ownerDocument.activeElement === element;
34
+ export function needsRegion(element: HTMLElement, axis: OverflowAxis = 'horizontal'): boolean {
35
+ return overflows(element, axis) || element.ownerDocument.activeElement === element;
25
36
  }
26
37
 
27
38
  /** A tracked scrollable surface, with the name it wears while it scrolls. */
@@ -34,6 +34,7 @@ import {
34
34
  import {
35
35
  clearScrollRegionAttributes,
36
36
  needsRegion,
37
+ type OverflowAxis,
37
38
  refreshRegions,
38
39
  SCROLL_REGION_CLASS,
39
40
  type TrackedSurface,
@@ -46,12 +47,20 @@ import {
46
47
  uniquelyNamed,
47
48
  } from './prose-regions';
48
49
 
50
+ export type { OverflowAxis } from './overflow-measure';
51
+
49
52
  export interface ScrollRegionProps {
50
53
  /** The region's accessible name, applied only while it actually scrolls. */
51
54
  readonly label: string;
52
55
  readonly children: ReactNode;
53
56
  readonly className?: string;
54
57
  readonly style?: CSSProperties;
58
+ /**
59
+ * The dimension overflow is measured on. `horizontal` (the default) fits a pane
60
+ * that scrolls sideways; `vertical` fits a bounded, capped-height box that scrolls
61
+ * down. The caller styles the box's own overflow to match.
62
+ */
63
+ readonly axis?: OverflowAxis;
55
64
  /**
56
65
  * The one arbitrary attribute this component forwards. The measured attributes
57
66
  * — `tabindex`, `role` and `aria-label` — are the component's own and appear
@@ -118,10 +127,13 @@ export interface OverflowRegionAttributes {
118
127
  *
119
128
  * @param ref - a consumer ref that wants the same element, or `undefined`.
120
129
  * @param label - its accessible name, applied only while it actually scrolls.
130
+ * @param axis - the dimension to measure overflow on (`horizontal` by default;
131
+ * `vertical` for a bounded, capped-height box that scrolls down).
121
132
  */
122
133
  export function useOverflowRegion(
123
134
  ref: Ref<HTMLElement> | undefined,
124
135
  label: string,
136
+ axis: OverflowAxis = 'horizontal',
125
137
  ): OverflowRegionAttributes {
126
138
  const [scrollable, setScrollable] = useState(false);
127
139
 
@@ -132,7 +144,7 @@ export function useOverflowRegion(
132
144
  const detachConsumer = attachRef(ref, box);
133
145
 
134
146
  const measure = (): void => {
135
- setScrollable(needsRegion(box));
147
+ setScrollable(needsRegion(box, axis));
136
148
  };
137
149
 
138
150
  // The box gives resize; its children give the overflowing width. The
@@ -177,7 +189,7 @@ export function useOverflowRegion(
177
189
  detachConsumer();
178
190
  };
179
191
  },
180
- [ref],
192
+ [ref, axis],
181
193
  );
182
194
 
183
195
  if (!scrollable) return { ref: measuredRef };
@@ -189,10 +201,11 @@ export function ScrollRegion({
189
201
  children,
190
202
  className,
191
203
  style,
204
+ axis,
192
205
  'data-testid': testId,
193
206
  ref,
194
207
  }: ScrollRegionProps) {
195
- const region = useOverflowRegion(ref, label);
208
+ const region = useOverflowRegion(ref, label, axis);
196
209
 
197
210
  return (
198
211
  <div
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The optional-subject sub-form shared by every form that keys a run's state to a
3
+ * conversation subject (the add-schedule dialog and the tool run panel). A collapsible
4
+ * block whose target / kind / key fields render only while expanded, so a form's default
5
+ * (collapsed) shape sends no subject.
6
+ *
7
+ * Presentational and prop-driven, like {@link ExecutionKeyPicker}: the host feature owns
8
+ * the conversation-targets query (the SDK holds no data-fetching library) and hands the
9
+ * resolved `targetOptions` in. {@link useSubjectFields} owns the field state; the host
10
+ * feature wraps it with its own targets query under its own query-key namespace. The
11
+ * caption and the two placeholders are the only copy that differs between hosts, so they
12
+ * are props.
13
+ */
14
+ import type { StateSubject } from '@tai42/api-client';
15
+ import { type ReactNode, useState } from 'react';
16
+
17
+ import { Field } from './field';
18
+ import { TextInput } from './inputs';
19
+ import { Select } from './select';
20
+
21
+ /** One conversation-target option: `target_kind:target_name` value, `kind · name` label. */
22
+ export interface SubjectTargetOption {
23
+ readonly value: string;
24
+ readonly label: string;
25
+ }
26
+
27
+ /** The message a partially-filled subject is refused with (all three fields or none). */
28
+ export const SUBJECT_INCOMPLETE_MESSAGE = 'A subject needs a target, a kind, and a key.';
29
+
30
+ /** Map the conversation routes read to the target select's options. */
31
+ export function toSubjectTargetOptions(
32
+ routes: readonly { readonly target_kind: string; readonly target_name: string }[],
33
+ ): SubjectTargetOption[] {
34
+ return routes.map((route) => ({
35
+ value: `${route.target_kind}:${route.target_name}`,
36
+ label: `${route.target_kind} · ${route.target_name}`,
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * Build a {@link StateSubject} from the sub-form values: `null` when untouched (no
42
+ * subject), a subject when target, kind and key are all set, or a loud failure when only
43
+ * some are — never a silently dropped partial subject. The target splits on its FIRST
44
+ * colon, so a target name may itself contain one.
45
+ */
46
+ export function buildSubject(v: {
47
+ readonly target: string;
48
+ readonly kind: string;
49
+ readonly key: string;
50
+ }): { ok: true; subject: StateSubject | null } | { ok: false; message: string } {
51
+ const touched = v.target !== '' || v.kind.trim() !== '' || v.key.trim() !== '';
52
+ if (!touched) return { ok: true, subject: null };
53
+ const separator = v.target.indexOf(':');
54
+ const targetKind = separator < 0 ? '' : v.target.slice(0, separator);
55
+ const targetName = separator < 0 ? '' : v.target.slice(separator + 1);
56
+ if (targetKind === '' || targetName === '' || v.kind.trim() === '' || v.key.trim() === '') {
57
+ return { ok: false, message: SUBJECT_INCOMPLETE_MESSAGE };
58
+ }
59
+ // The target is chosen from the conversation-routes options, so its kind is one of the
60
+ // route target kinds; the value carries it as text and this narrows it to the subject's
61
+ // union without a runtime schema (the SDK holds no api-client value import).
62
+ return {
63
+ ok: true,
64
+ subject: {
65
+ target_kind: targetKind as StateSubject['target_kind'],
66
+ target_name: targetName,
67
+ kind: v.kind.trim(),
68
+ key: v.key.trim(),
69
+ },
70
+ };
71
+ }
72
+
73
+ /** Subject field state + setters + the collapse toggle. The host feature owns the
74
+ * open-gated conversation-targets query and derives `targetOptions` from it. */
75
+ export function useSubjectFields() {
76
+ const [target, setTarget] = useState('');
77
+ const [kind, setKind] = useState('');
78
+ const [key, setKey] = useState('');
79
+ const [error, setError] = useState<string | null>(null);
80
+ const [open, setOpen] = useState(false);
81
+ return { target, setTarget, kind, setKind, key, setKey, error, setError, open, setOpen };
82
+ }
83
+
84
+ export interface SubjectSectionProps {
85
+ readonly open: boolean;
86
+ readonly onToggle: () => void;
87
+ readonly target: string;
88
+ readonly onTargetChange: (value: string) => void;
89
+ readonly kind: string;
90
+ readonly onKindChange: (value: string) => void;
91
+ readonly subjectKey: string;
92
+ readonly onKeyChange: (value: string) => void;
93
+ readonly error: string | null;
94
+ readonly targetOptions: readonly SubjectTargetOption[];
95
+ /** The block's descriptive caption, shown under the expanded header. */
96
+ readonly caption: string;
97
+ /** The target select's empty-choice placeholder. */
98
+ readonly targetPlaceholder: string;
99
+ /** The Subject key field's helper description. */
100
+ readonly subjectKeyDescription: string;
101
+ }
102
+
103
+ export function SubjectSection({
104
+ open,
105
+ onToggle,
106
+ target,
107
+ onTargetChange,
108
+ kind,
109
+ onKindChange,
110
+ subjectKey,
111
+ onKeyChange,
112
+ error,
113
+ targetOptions,
114
+ caption,
115
+ targetPlaceholder,
116
+ subjectKeyDescription,
117
+ }: SubjectSectionProps): ReactNode {
118
+ return (
119
+ <div>
120
+ <button
121
+ type="button"
122
+ className="tai-btn tai-btn-ghost"
123
+ aria-expanded={open}
124
+ onClick={onToggle}
125
+ >
126
+ Subject (optional)
127
+ </button>
128
+ {open ? (
129
+ <div
130
+ style={{
131
+ display: 'flex',
132
+ flexDirection: 'column',
133
+ gap: 'var(--tai-space-3)',
134
+ marginTop: 'var(--tai-space-3)',
135
+ }}
136
+ >
137
+ <p style={{ margin: 0, color: 'var(--tai-color-text-muted)' }}>{caption}</p>
138
+ <Field label="Target">
139
+ <Select
140
+ value={target}
141
+ onValueChange={onTargetChange}
142
+ aria-label="Subject target"
143
+ placeholder={targetPlaceholder}
144
+ options={targetOptions}
145
+ />
146
+ </Field>
147
+ <Field
148
+ label="Subject kind"
149
+ description="The subject family the state declares (e.g. person)."
150
+ >
151
+ <TextInput
152
+ value={kind}
153
+ placeholder="e.g. person"
154
+ onChange={(event) => {
155
+ onKindChange(event.target.value);
156
+ }}
157
+ />
158
+ </Field>
159
+ <Field label="Subject key" description={subjectKeyDescription}>
160
+ <TextInput
161
+ value={subjectKey}
162
+ placeholder="e.g. a-42"
163
+ onChange={(event) => {
164
+ onKeyChange(event.target.value);
165
+ }}
166
+ />
167
+ </Field>
168
+ {error !== null ? (
169
+ <p role="alert" style={{ margin: 0, color: 'var(--tai-color-err-text)' }}>
170
+ {error}
171
+ </p>
172
+ ) : null}
173
+ </div>
174
+ ) : null}
175
+ </div>
176
+ );
177
+ }
@@ -38,6 +38,11 @@ import { ALL_TAGS, SelectedToolBadges, TagFilterField, ToolSelect } from './tool
38
38
 
39
39
  export { BADGES_NOTE } from './tool-picker-parts';
40
40
 
41
+ /** The trigger copy when the picker has no option to offer at all (after exclusions).
42
+ * Shown only once the list has settled — a still-loading picker keeps its own
43
+ * placeholder — so it never misreads a pending fetch as "nothing to pick". */
44
+ const NO_TOOLS_AVAILABLE = 'No tools available';
45
+
41
46
  export interface ToolPickerProps {
42
47
  readonly toolNames: readonly string[];
43
48
  readonly value: string | null;
@@ -94,6 +99,13 @@ export function ToolPicker({
94
99
  const excluded = new Set(excludeNames ?? []);
95
100
  const available = toolNames.filter((name) => !excluded.has(name));
96
101
 
102
+ // A settled, empty option set (not a load in progress) is a "nothing to pick"
103
+ // state: the trigger reads the empty copy and cannot open onto an empty popup. The
104
+ // caller's `disabled` still marks a loading picker, which keeps its own placeholder.
105
+ const noneAvailable = available.length === 0;
106
+ const pickerDisabled = disabled === true || noneAvailable;
107
+ const pickerPlaceholder = noneAvailable && disabled !== true ? NO_TOOLS_AVAILABLE : placeholder;
108
+
97
109
  // Distinct tags across the available tools, sorted, feeding the filter control.
98
110
  const allTags =
99
111
  tagsByTool === undefined
@@ -113,9 +125,9 @@ export function ToolPicker({
113
125
  filtered={filtered}
114
126
  value={value}
115
127
  onChange={onChange}
116
- placeholder={placeholder}
128
+ placeholder={pickerPlaceholder}
117
129
  ariaLabel={ariaLabel}
118
- disabled={disabled}
130
+ disabled={pickerDisabled}
119
131
  displayNames={displayNames}
120
132
  agentToolNames={agentToolNames}
121
133
  />