@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.
- package/dist/components/execution-key-picker.d.ts +37 -0
- package/dist/components/execution-key-picker.d.ts.map +1 -0
- package/dist/components/execution-key-picker.js +49 -0
- package/dist/components/execution-key-picker.js.map +1 -0
- package/dist/components/json-tree-nodes.d.ts.map +1 -1
- package/dist/components/json-tree-nodes.js +24 -9
- package/dist/components/json-tree-nodes.js.map +1 -1
- package/dist/components/overflow-measure.d.ts +11 -3
- package/dist/components/overflow-measure.d.ts.map +1 -1
- package/dist/components/overflow-measure.js +8 -5
- package/dist/components/overflow-measure.js.map +1 -1
- package/dist/components/scroll-region.d.ts +12 -2
- package/dist/components/scroll-region.d.ts.map +1 -1
- package/dist/components/scroll-region.js +7 -5
- package/dist/components/scroll-region.js.map +1 -1
- package/dist/components/subject-section.d.ts +78 -0
- package/dist/components/subject-section.d.ts.map +1 -0
- package/dist/components/subject-section.js +66 -0
- package/dist/components/subject-section.js.map +1 -0
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +11 -1
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/hooks/useApi.d.ts +96 -45
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +3 -3
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/context.d.ts +19 -0
- package/dist/schema-form/context.d.ts.map +1 -1
- package/dist/schema-form/context.js.map +1 -1
- package/dist/schema-form/field-model.d.ts +19 -0
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts +10 -3
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +16 -2
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/keyed-callbacks.d.ts +2 -0
- package/dist/schema-form/keyed-callbacks.d.ts.map +1 -0
- package/dist/schema-form/keyed-callbacks.js +45 -0
- package/dist/schema-form/keyed-callbacks.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +5 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/record-field.d.ts.map +1 -1
- package/dist/schema-form/record-field.js +4 -6
- package/dist/schema-form/record-field.js.map +1 -1
- package/dist/schema-form/string-annotations.d.ts.map +1 -1
- package/dist/schema-form/string-annotations.js +26 -0
- package/dist/schema-form/string-annotations.js.map +1 -1
- package/dist/schema-form/string-field.js +22 -6
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/testing.d.ts +9 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +16 -13
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
- package/src/components/execution-key-picker.tsx +116 -0
- package/src/components/json-tree-nodes.tsx +36 -18
- package/src/components/overflow-measure.ts +16 -5
- package/src/components/scroll-region.tsx +16 -3
- package/src/components/subject-section.tsx +177 -0
- package/src/components/tool-picker.tsx +14 -2
- package/src/index.ts +11 -0
- package/src/schema-form/array-field.tsx +3 -3
- package/src/schema-form/context.ts +20 -0
- package/src/schema-form/field-model.ts +20 -0
- package/src/schema-form/field-node.tsx +11 -3
- package/src/schema-form/keyed-callbacks.ts +46 -0
- package/src/schema-form/object-fields.tsx +8 -3
- package/src/schema-form/record-field.tsx +7 -6
- package/src/schema-form/string-annotations.ts +32 -1
- package/src/schema-form/string-field.tsx +25 -6
- 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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
92
|
-
const
|
|
93
|
-
const
|
|
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
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
|
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
|
-
|
|
106
|
-
Object.defineProperty(element,
|
|
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 ?
|
|
111
|
+
value: overflowing ? OVERFLOWING_SCROLL_EXTENT : FITTING_EXTENT,
|
|
109
112
|
});
|
|
110
113
|
}
|
|
111
114
|
/**
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","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;AAa7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,aAAa;IACR,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,GAAG,CAAC,KAAa;QACf,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IAClD,CAAC;IAED,OAAO,CAAC,GAAW;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACxC,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,MAAe;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAuB,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACpF,OAAO;QACL,MAAM;QACN,WAAW,EAAE,IAAI;QACjB,aAAa,EAAE,CAAC,IAAI,CAAC;QACrB,cAAc,EAAE,CAAC,IAAI,CAAC;QACtB,yBAAyB,EAAE,CAAC,IAAI,CAAC;KAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB;IAClC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC;QAC5C,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,SAAS;QACtC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAe;IACrD,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"testing.js","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;AAa7E,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAsB,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,aAAa;IACR,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,GAAG,CAAC,KAAa;QACf,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IAClD,CAAC;IAED,OAAO,CAAC,GAAW;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;IACxC,CAAC;IAED,OAAO,CAAC,GAAW,EAAE,KAAa;QAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,UAAU,CAAC,GAAW;QACpB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AAED;;;;;GAKG;AACH,SAAS,mBAAmB,CAAC,MAAe;IAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAuB,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACpF,OAAO;QACL,MAAM;QACN,WAAW,EAAE,IAAI;QACjB,aAAa,EAAE,CAAC,IAAI,CAAC;QACrB,cAAc,EAAE,CAAC,IAAI,CAAC;QACtB,yBAAyB,EAAE,CAAC,IAAI,CAAC;KAClC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB;IAClC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC;QAC5C,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,SAAS;QACtC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAe;IACrD,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAoB,EACpB,WAAoB,EACpB,OAAkC,YAAY;IAE9C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpB,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAC1F,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;IACtF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE;QACrC,YAAY,EAAE,IAAI;QAClB,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,cAAc;KAChE,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,OAAO,UAAU,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;QACpD,MAAM,kBAAkB;YACb,KAAK,CAAqB;YAEnC,YAAY,QAAgC;gBAC1C,IAAI,CAAC,KAAK,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,EAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;gBACvE,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtC,CAAC;YAED,OAAO,CAAC,MAAe;gBACrB,yEAAyE;gBACzE,gEAAgE;gBAChE,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACpC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBAC/B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAC3D,CAAC;YAED,SAAS,CAAC,MAAe;gBACvB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACpC,CAAC;YAED,UAAU;gBACR,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBAC3B,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzC,CAAC;SACF;QACD,UAAU,CAAC,cAAc,GAAG,kBAAkB,CAAC;IACjD,CAAC;IAED,IAAI,OAAO,OAAO,CAAC,SAAS,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;QAC3D,OAAO,CAAC,SAAS,CAAC,cAAc,GAAG,SAAS,cAAc;YACxD,gBAAgB;QAClB,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,SAAS,CAAC,iBAAiB,KAAK,UAAU,EAAE,CAAC;QAC9D,OAAO,CAAC,SAAS,CAAC,iBAAiB,GAAG,SAAS,iBAAiB;YAC9D,OAAO,KAAK,CAAC;QACf,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,SAAS,CAAC,iBAAiB,KAAK,UAAU,EAAE,CAAC;QAC9D,OAAO,CAAC,SAAS,CAAC,iBAAiB,GAAG,SAAS,iBAAiB;YAC9D,gBAAgB;QAClB,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,SAAS,CAAC,qBAAqB,KAAK,UAAU,EAAE,CAAC;QAClE,OAAO,CAAC,SAAS,CAAC,qBAAqB,GAAG,SAAS,qBAAqB;YACtE,gBAAgB;QAClB,CAAC,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,+EAA+E;IAC/E,8EAA8E;IAC9E,4EAA4E;IAC5E,IAAI,OAAO,OAAO,CAAC,SAAS,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACrD,OAAO,CAAC,SAAS,CAAC,QAAQ,GAAG,SAAS,QAAQ;YAC5C,gBAAgB;QAClB,CAAC,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,QAAQ,GAAG,SAAS,QAAQ;QACjC,gBAAgB;IAClB,CAAC,CAAC;IAEF,yEAAyE;IACzE,cAAc;IACd,IAAI,OAAO,GAAG,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QAC9C,GAAG,CAAC,eAAe,GAAG,GAAW,EAAE,CAAC,WAAW,CAAC;IAClD,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QAC9C,GAAG,CAAC,eAAe,GAAG,GAAS,EAAE;YAC/B,gBAAgB;QAClB,CAAC,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,8EAA8E;IAC9E,gBAAgB;IAChB,IAAI,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9C,IAAI,CAAC,SAAS,CAAC,IAAI,GAAG,SAAS,UAAU;YACvC,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC7C,MAAM,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAChC,MAAM,CAAC,MAAM,GAAG,GAAS,EAAE;oBACzB,OAAO,CAAC,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAClE,CAAC,CAAC;gBACF,MAAM,CAAC,OAAO,GAAG,GAAS,EAAE;oBAC1B,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;gBACzD,CAAC,CAAC;gBACF,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,yEAAyE;IACzE,iFAAiF;IACjF,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC7E,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,cAAc,EAAE;QAChD,KAAK,EAAE,IAAI,aAAa,EAAE;QAC1B,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IACH,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,gBAAgB,EAAE;QAClD,KAAK,EAAE,IAAI,aAAa,EAAE;QAC1B,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ;IAKtB,IAAI,OAA4B,CAAC;IACjC,IAAI,MAAkC,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC1C,OAAO,GAAG,GAAG,CAAC;QACd,MAAM,GAAG,GAAG,CAAC;IACf,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tai42/studio-sdk",
|
|
3
|
-
"version": "
|
|
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.
|
|
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": "
|
|
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
|
-
/**
|
|
44
|
-
|
|
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 = (
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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={
|
|
128
|
+
placeholder={pickerPlaceholder}
|
|
117
129
|
ariaLabel={ariaLabel}
|
|
118
|
-
disabled={
|
|
130
|
+
disabled={pickerDisabled}
|
|
119
131
|
displayNames={displayNames}
|
|
120
132
|
agentToolNames={agentToolNames}
|
|
121
133
|
/>
|