@softize/opus 25.0.0 → 25.2.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/CHANGELOG.md +93 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/assistant-schema.d.mts +55 -0
- package/bin/lib/assistant-schema.mjs +286 -0
- package/bin/lib/check.mjs +173 -0
- package/bin/lib/copy.mjs +2 -0
- package/bin/lib/gen-manifest.mjs +3 -0
- package/bin/lib/gen-runner.mjs +29 -1
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
- package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
- package/docs/protocol.md +54 -0
- package/docs/releasing.md +8 -2
- package/docs/stacking-scale.md +53 -0
- package/package.json +10 -1
- package/registry/skills/create-opus-action/SKILL.md +8 -3
- package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/test-opus-action/SKILL.md +4 -1
- package/registry/skills/test-opus-action/references/evaluations.md +2 -0
- package/registry/skills/test-opus-action/references/harness.md +14 -0
- package/src/ai/index.ts +3 -0
- package/src/ai/resource-context.ts +113 -0
- package/src/core/assistant.ts +545 -0
- package/src/core/index.ts +23 -0
- package/src/core/runtime.ts +136 -0
- package/src/core/types.ts +34 -0
- package/src/mcp/index.ts +3 -0
- package/src/ui/components/primitives/dialog.tsx +285 -26
- package/src/ui/components/primitives/drawer.tsx +104 -13
- package/src/ui/components/primitives/floating-layer.tsx +79 -0
- package/src/ui/components/primitives/menu.tsx +17 -2
- package/src/ui/components/primitives/popover.tsx +6 -1
- package/src/ui/components/primitives/tooltip.tsx +6 -1
- package/src/ui/docs/content/actions.md +97 -1
- package/src/ui/docs/content/ai.md +4 -2
- package/src/ui/docs/content/cli.md +2 -1
- package/src/ui/docs/content/dialog.md +3 -0
- package/src/ui/docs/content/drawer.md +3 -0
- package/src/ui/docs/content/floating-layer.md +178 -0
- package/src/ui/docs/content/mcp.md +3 -1
- package/src/ui/docs/registry.tsx +6 -0
- package/src/ui/lib/dismiss-on-iframe.ts +4 -1
- package/src/ui/lib/modal-layers.ts +547 -0
- package/src/ui/meta.ts +6 -0
- package/src/ui/react.tsx +4 -0
|
@@ -18,7 +18,19 @@ import {
|
|
|
18
18
|
type StructuralMediaProps,
|
|
19
19
|
} from "../patterns/structural-media.tsx";
|
|
20
20
|
import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
composeModalDismissHandlers,
|
|
23
|
+
overlayClasses,
|
|
24
|
+
useComposedRef,
|
|
25
|
+
useModalOpenState,
|
|
26
|
+
} from "./dialog.tsx";
|
|
27
|
+
import {
|
|
28
|
+
LAYER_MODAL_ATTRIBUTE,
|
|
29
|
+
LayerPortalProvider,
|
|
30
|
+
handleModalTabKey,
|
|
31
|
+
useLayerPortal,
|
|
32
|
+
useModalLayerEntry,
|
|
33
|
+
} from "../../lib/modal-layers.ts";
|
|
22
34
|
|
|
23
35
|
interface DrawerContentContextValue {
|
|
24
36
|
showCloseButton: boolean;
|
|
@@ -34,10 +46,36 @@ const DrawerContentContext = React.createContext<DrawerContentContextValue>({
|
|
|
34
46
|
|
|
35
47
|
const DrawerIntroContext = React.createContext(false);
|
|
36
48
|
|
|
49
|
+
/** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
|
|
50
|
+
const DrawerStateContext = React.createContext({ open: false, modal: true });
|
|
51
|
+
|
|
37
52
|
function Drawer({
|
|
53
|
+
open: openProp,
|
|
54
|
+
defaultOpen,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
modal,
|
|
38
57
|
...props
|
|
39
58
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
|
40
|
-
|
|
59
|
+
const [open, setOpen] = useModalOpenState({
|
|
60
|
+
open: openProp,
|
|
61
|
+
defaultOpen,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
});
|
|
64
|
+
const state = React.useMemo(
|
|
65
|
+
() => ({ open, modal: modal !== false }),
|
|
66
|
+
[open, modal],
|
|
67
|
+
);
|
|
68
|
+
return (
|
|
69
|
+
<DrawerStateContext.Provider value={state}>
|
|
70
|
+
<DrawerPrimitive.Root
|
|
71
|
+
data-slot="drawer"
|
|
72
|
+
modal={modal}
|
|
73
|
+
open={open}
|
|
74
|
+
onOpenChange={setOpen}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
</DrawerStateContext.Provider>
|
|
78
|
+
);
|
|
41
79
|
}
|
|
42
80
|
|
|
43
81
|
function DrawerTrigger({
|
|
@@ -53,19 +91,30 @@ function DrawerClose({
|
|
|
53
91
|
}
|
|
54
92
|
|
|
55
93
|
function DrawerPortal({
|
|
94
|
+
container,
|
|
56
95
|
...props
|
|
57
96
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
|
58
|
-
|
|
97
|
+
// Aberto de dentro de uma camada flutuante, o Drawer vai para o hospedeiro dela (ADR 0052).
|
|
98
|
+
const layer = useLayerPortal();
|
|
99
|
+
return (
|
|
100
|
+
<DrawerPrimitive.Portal
|
|
101
|
+
data-slot="drawer-portal"
|
|
102
|
+
container={container ?? layer.container}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
function DrawerOverlay({
|
|
62
109
|
className,
|
|
63
110
|
...props
|
|
64
111
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
|
112
|
+
const layer = useLayerPortal();
|
|
65
113
|
return (
|
|
66
114
|
<DrawerPrimitive.Overlay
|
|
67
115
|
data-slot="drawer-overlay"
|
|
68
|
-
|
|
116
|
+
{...{ [LAYER_MODAL_ATTRIBUTE]: layer.container !== undefined || undefined }}
|
|
117
|
+
className={cn(overlayClasses, layer.zIndexClass, className)}
|
|
69
118
|
{...props}
|
|
70
119
|
/>
|
|
71
120
|
);
|
|
@@ -95,7 +144,11 @@ function DrawerContent({
|
|
|
95
144
|
layout = "sectioned",
|
|
96
145
|
"aria-describedby": ariaDescribedBy,
|
|
97
146
|
onEscapeKeyDown,
|
|
147
|
+
onPointerDownOutside,
|
|
98
148
|
onInteractOutside,
|
|
149
|
+
onCloseAutoFocus,
|
|
150
|
+
onKeyDown,
|
|
151
|
+
ref,
|
|
99
152
|
...props
|
|
100
153
|
}: React.ComponentProps<typeof DrawerPrimitive.Content> & {
|
|
101
154
|
side?: "top" | "right" | "bottom" | "left";
|
|
@@ -109,12 +162,44 @@ function DrawerContent({
|
|
|
109
162
|
/** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
|
|
110
163
|
layout?: SurfaceLayout;
|
|
111
164
|
}) {
|
|
165
|
+
const { open, modal } = React.useContext(DrawerStateContext);
|
|
166
|
+
const layer = useLayerPortal();
|
|
167
|
+
const internal = layer.container !== undefined;
|
|
168
|
+
// Seção 6 da ADR 0052: portado para uma camada, o Drawer é interno a ela; fora, é de trabalho.
|
|
169
|
+
const mark = internal ? "internal" : "work";
|
|
170
|
+
const entryRef = useModalLayerEntry({
|
|
171
|
+
open,
|
|
172
|
+
modal,
|
|
173
|
+
mark,
|
|
174
|
+
surface: "drawer",
|
|
175
|
+
side,
|
|
176
|
+
});
|
|
177
|
+
const composedRef = useComposedRef(ref, entryRef);
|
|
178
|
+
const dismiss = composeModalDismissHandlers({
|
|
179
|
+
mark,
|
|
180
|
+
closeDisabled,
|
|
181
|
+
onEscapeKeyDown,
|
|
182
|
+
onPointerDownOutside,
|
|
183
|
+
onInteractOutside,
|
|
184
|
+
onCloseAutoFocus,
|
|
185
|
+
});
|
|
186
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
187
|
+
onKeyDown?.(event);
|
|
188
|
+
handleModalTabKey(event);
|
|
189
|
+
};
|
|
190
|
+
// Os portais abertos daqui herdam a marca de modal quando este Drawer mora numa camada.
|
|
191
|
+
const nestedPortal = React.useMemo(
|
|
192
|
+
() => ({ ...layer, modalSubtree: layer.modalSubtree || internal }),
|
|
193
|
+
[layer, internal],
|
|
194
|
+
);
|
|
112
195
|
return (
|
|
113
196
|
<DrawerPortal>
|
|
114
197
|
<DrawerOverlay />
|
|
115
198
|
<DrawerPrimitive.Content
|
|
199
|
+
ref={composedRef}
|
|
116
200
|
data-slot="drawer-content"
|
|
117
201
|
data-layout={layout}
|
|
202
|
+
{...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
|
|
118
203
|
className={cn(
|
|
119
204
|
"fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
|
120
205
|
side === "right" &&
|
|
@@ -125,20 +210,26 @@ function DrawerContent({
|
|
|
125
210
|
"inset-x-1 top-1 h-auto data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
|
126
211
|
side === "bottom" &&
|
|
127
212
|
"inset-x-1 bottom-1 h-auto data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
|
213
|
+
layer.zIndexClass,
|
|
128
214
|
className,
|
|
129
215
|
)}
|
|
130
216
|
aria-describedby={ariaDescribedBy}
|
|
131
|
-
onEscapeKeyDown={
|
|
132
|
-
|
|
217
|
+
onEscapeKeyDown={dismiss.onEscapeKeyDown}
|
|
218
|
+
onPointerDownOutside={dismiss.onPointerDownOutside}
|
|
219
|
+
onInteractOutside={dismiss.onInteractOutside}
|
|
220
|
+
onCloseAutoFocus={dismiss.onCloseAutoFocus}
|
|
221
|
+
onKeyDown={handleKeyDown}
|
|
133
222
|
{...props}
|
|
134
223
|
>
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
224
|
+
<LayerPortalProvider value={nestedPortal}>
|
|
225
|
+
<SurfaceLayoutProvider value={layout}>
|
|
226
|
+
<DrawerContentContext.Provider
|
|
227
|
+
value={{ showCloseButton, closeDisabled, closeSize }}
|
|
228
|
+
>
|
|
229
|
+
{children}
|
|
230
|
+
</DrawerContentContext.Provider>
|
|
231
|
+
</SurfaceLayoutProvider>
|
|
232
|
+
</LayerPortalProvider>
|
|
142
233
|
</DrawerPrimitive.Content>
|
|
143
234
|
</DrawerPortal>
|
|
144
235
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/cn.ts";
|
|
5
|
+
import {
|
|
6
|
+
LayerPortalProvider,
|
|
7
|
+
registerFloatingLayer,
|
|
8
|
+
} from "../../lib/modal-layers.ts";
|
|
9
|
+
|
|
10
|
+
export interface FloatingLayerProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Classes do hospedeiro. A posição de cada filho continua sendo do consumidor. */
|
|
13
|
+
className?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Esc enquanto a pessoa está na camada, com ou sem modal aberto. É chamado uma vez por tecla.
|
|
16
|
+
* Nenhum modal fecha nesse caso, e o Opus não fecha a camada sozinho, porque o estado de aberto
|
|
17
|
+
* pertence ao consumidor.
|
|
18
|
+
*/
|
|
19
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hospedeiro fixo, na tela inteira e acima dos modais de trabalho. Ele mesmo não recebe clique;
|
|
24
|
+
* os filhos diretos recebem, mesmo com o `body` bloqueado por um modal.
|
|
25
|
+
*/
|
|
26
|
+
const hostClasses =
|
|
27
|
+
"pointer-events-none fixed inset-0 z-60 *:pointer-events-auto";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Região da aplicação que fica acima do conteúdo e dos modais de trabalho (Dialog e Drawer) e
|
|
31
|
+
* continua utilizável enquanto eles estão abertos, como o botão e o painel de um assistente
|
|
32
|
+
* (ADR 0052).
|
|
33
|
+
*
|
|
34
|
+
* Os filhos são portados para `div[data-slot="floating-layer"]` no `body`. O hospedeiro publica o
|
|
35
|
+
* modal de trabalho do topo em `data-modal` (`none`, `dialog` ou `drawer`) e o lado do Drawer em
|
|
36
|
+
* `data-modal-side`, para o consumidor reposicionar o que precisar. Popover, Menu, Tooltip, Dialog
|
|
37
|
+
* e Drawer abertos de dentro da camada vão para o mesmo hospedeiro.
|
|
38
|
+
*/
|
|
39
|
+
export function FloatingLayer({
|
|
40
|
+
children,
|
|
41
|
+
className,
|
|
42
|
+
onEscapeKeyDown,
|
|
43
|
+
}: FloatingLayerProps) {
|
|
44
|
+
const [host, setHost] = React.useState<HTMLElement | null>(null);
|
|
45
|
+
const escapeRef = React.useRef(onEscapeKeyDown);
|
|
46
|
+
escapeRef.current = onEscapeKeyDown;
|
|
47
|
+
|
|
48
|
+
React.useLayoutEffect(() => {
|
|
49
|
+
const element = document.createElement("div");
|
|
50
|
+
element.setAttribute("data-slot", "floating-layer");
|
|
51
|
+
document.body.appendChild(element);
|
|
52
|
+
const unregister = registerFloatingLayer(element, (event) =>
|
|
53
|
+
escapeRef.current?.(event),
|
|
54
|
+
);
|
|
55
|
+
setHost(element);
|
|
56
|
+
// Desfaz o registro, retira o hospedeiro e, se o foco estava nele com um modal aberto,
|
|
57
|
+
// devolve o foco ao modal.
|
|
58
|
+
return unregister;
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
React.useLayoutEffect(() => {
|
|
62
|
+
if (host !== null) host.className = cn(hostClasses, className);
|
|
63
|
+
}, [host, className]);
|
|
64
|
+
|
|
65
|
+
const portal = React.useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
container: host ?? undefined,
|
|
68
|
+
zIndexClass: undefined,
|
|
69
|
+
modalSubtree: false,
|
|
70
|
+
}),
|
|
71
|
+
[host],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (host === null) return null;
|
|
75
|
+
return createPortal(
|
|
76
|
+
<LayerPortalProvider value={portal}>{children}</LayerPortalProvider>,
|
|
77
|
+
host,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -5,6 +5,7 @@ import { DropdownMenu as MenuPrimitive } from "radix-ui";
|
|
|
5
5
|
|
|
6
6
|
import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts";
|
|
7
7
|
import { cn } from "../../lib/cn.ts";
|
|
8
|
+
import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts";
|
|
8
9
|
|
|
9
10
|
function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
|
|
10
11
|
return <MenuPrimitive.Root data-slot="menu" {...props} />;
|
|
@@ -16,9 +17,17 @@ function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
|
|
|
16
17
|
* próxima versão major.
|
|
17
18
|
*/
|
|
18
19
|
function MenuPortal({
|
|
20
|
+
container,
|
|
19
21
|
...props
|
|
20
22
|
}: React.ComponentProps<typeof MenuPrimitive.Portal>) {
|
|
21
|
-
|
|
23
|
+
const layer = useLayerPortal();
|
|
24
|
+
return (
|
|
25
|
+
<MenuPrimitive.Portal
|
|
26
|
+
data-slot="menu-portal"
|
|
27
|
+
container={container ?? layer.container}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
function MenuTrigger({
|
|
@@ -35,14 +44,18 @@ function MenuContent({
|
|
|
35
44
|
// Mesma razão do Popover: clique dentro de iframe não chega aqui fora.
|
|
36
45
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
37
46
|
useDismissOnIframeFocus(ref);
|
|
47
|
+
// Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
|
|
48
|
+
const layer = useLayerPortal();
|
|
38
49
|
return (
|
|
39
|
-
<MenuPrimitive.Portal>
|
|
50
|
+
<MenuPrimitive.Portal container={layer.container}>
|
|
40
51
|
<MenuPrimitive.Content
|
|
41
52
|
ref={ref}
|
|
42
53
|
data-slot="menu-content"
|
|
54
|
+
{...layerModalMarker(layer)}
|
|
43
55
|
sideOffset={sideOffset}
|
|
44
56
|
className={cn(
|
|
45
57
|
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
58
|
+
layer.zIndexClass,
|
|
46
59
|
className,
|
|
47
60
|
)}
|
|
48
61
|
{...props}
|
|
@@ -215,11 +228,13 @@ function MenuSubContent({
|
|
|
215
228
|
className,
|
|
216
229
|
...props
|
|
217
230
|
}: React.ComponentProps<typeof MenuPrimitive.SubContent>) {
|
|
231
|
+
const layer = useLayerPortal();
|
|
218
232
|
return (
|
|
219
233
|
<MenuPrimitive.SubContent
|
|
220
234
|
data-slot="menu-sub-content"
|
|
221
235
|
className={cn(
|
|
222
236
|
"z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
237
|
+
layer.zIndexClass,
|
|
223
238
|
className,
|
|
224
239
|
)}
|
|
225
240
|
{...props}
|
|
@@ -2,6 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import { Popover as PopoverPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
4
|
import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
|
|
5
|
+
import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
|
|
5
6
|
import { cn } from '../../lib/cn.ts'
|
|
6
7
|
|
|
7
8
|
function Popover({
|
|
@@ -26,15 +27,19 @@ function PopoverContent({
|
|
|
26
27
|
// pendurado sobre o conteúdo. Na cabine, o preview É um iframe.
|
|
27
28
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
28
29
|
useDismissOnIframeFocus(ref)
|
|
30
|
+
// Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
|
|
31
|
+
const layer = useLayerPortal()
|
|
29
32
|
return (
|
|
30
|
-
<PopoverPrimitive.Portal>
|
|
33
|
+
<PopoverPrimitive.Portal container={layer.container}>
|
|
31
34
|
<PopoverPrimitive.Content
|
|
32
35
|
ref={ref}
|
|
33
36
|
data-slot="popover-content"
|
|
37
|
+
{...layerModalMarker(layer)}
|
|
34
38
|
align={align}
|
|
35
39
|
sideOffset={sideOffset}
|
|
36
40
|
className={cn(
|
|
37
41
|
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
|
42
|
+
layer.zIndexClass,
|
|
38
43
|
className
|
|
39
44
|
)}
|
|
40
45
|
{...props}
|
|
@@ -2,6 +2,7 @@ import * as React from "react"
|
|
|
2
2
|
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn.ts'
|
|
5
|
+
import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
|
|
5
6
|
|
|
6
7
|
function TooltipProvider({
|
|
7
8
|
delayDuration = 0,
|
|
@@ -34,13 +35,17 @@ function TooltipContent({
|
|
|
34
35
|
children,
|
|
35
36
|
...props
|
|
36
37
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
38
|
+
// Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
|
|
39
|
+
const layer = useLayerPortal()
|
|
37
40
|
return (
|
|
38
|
-
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Portal container={layer.container}>
|
|
39
42
|
<TooltipPrimitive.Content
|
|
40
43
|
data-slot="tooltip-content"
|
|
44
|
+
{...layerModalMarker(layer)}
|
|
41
45
|
sideOffset={sideOffset}
|
|
42
46
|
className={cn(
|
|
43
47
|
"z-50 w-fit max-w-sm origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-pretty text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
48
|
+
layer.zIndexClass,
|
|
44
49
|
className
|
|
45
50
|
)}
|
|
46
51
|
{...props}
|
|
@@ -136,7 +136,7 @@ name, kind
|
|
|
136
136
|
→ public, requires, authorize, loads
|
|
137
137
|
→ handler
|
|
138
138
|
→ audit, confirm, invalidates, rateLimit, idempotency
|
|
139
|
-
→ ai, automatable, internal
|
|
139
|
+
→ ai, assistant, automatable, internal
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
|
|
@@ -179,6 +179,102 @@ A autorização é fechada por padrão: sem `public: true`, a action exige usuá
|
|
|
179
179
|
`authorize`, exige também a permissão declarada. `automatable: true` permite que um
|
|
180
180
|
[schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
|
|
181
181
|
|
|
182
|
+
## Declarar o assistente do recurso
|
|
183
|
+
|
|
184
|
+
Um produto pode oferecer, além do chat geral, uma conversa sobre um recurso específico: um cliente,
|
|
185
|
+
um relatório, um pedido. Essa conversa precisa ler o recurso com a permissão de quem conversa, seguir
|
|
186
|
+
o procedimento próprio daquele tipo de recurso e entregar ao modelo só os dados necessários. Os três
|
|
187
|
+
ficam declarados num lugar só: a action `view` que lê o recurso declara `assistant` e passa a ser a
|
|
188
|
+
**action dona** daquele tipo de recurso.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
export const customerViewContract = defineContract({
|
|
192
|
+
name: 'customer.view',
|
|
193
|
+
kind: 'view',
|
|
194
|
+
label: 'Ver cliente',
|
|
195
|
+
input: z.object({ id: z.string().uuid() }),
|
|
196
|
+
output: customerViewSchema,
|
|
197
|
+
personalData: true,
|
|
198
|
+
requires: 'customer.read',
|
|
199
|
+
authorize: (ctx) => ctx.can('customer.read'),
|
|
200
|
+
ai: true,
|
|
201
|
+
assistant: {
|
|
202
|
+
resource: 'customer',
|
|
203
|
+
skill: 'review-customer',
|
|
204
|
+
label: 'Assistente do cliente',
|
|
205
|
+
title: 'name',
|
|
206
|
+
context: ['kind', 'stage', 'sources', 'leads.totalCount', 'leads.latest.status'],
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| Campo | O que declara |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `resource` | O tipo de recurso de que a action é dona, em camelCase. Cada tipo tem uma única action dona. |
|
|
214
|
+
| `skill` | A chave do procedimento que a IA segue ao tratar do recurso, em minúsculas com hífen. O Opus não conhece o procedimento; o produto o mantém e confere que a chave existe. |
|
|
215
|
+
| `label` | O nome do assistente para pessoas. Entra no inventário do `opus copy`. |
|
|
216
|
+
| `title` | O caminho, na saída, do campo que nomeia o recurso. |
|
|
217
|
+
| `context` | Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. |
|
|
218
|
+
| `maxContextChars` | Opcional. O teto do dado entregue, em caracteres de JSON: 4.000 por padrão, de 1.000 a 16.000. |
|
|
219
|
+
|
|
220
|
+
A action dona precisa ser `view`, estar publicada para a IA (`ai`), declarar `personalData` e
|
|
221
|
+
receber só `{ id }`: o `id`, do tipo texto, é o único campo obrigatório da entrada, e outros campos,
|
|
222
|
+
se houver, têm valor padrão ou são opcionais. Os caminhos usam ponto para descer em objetos; um
|
|
223
|
+
caminho que passa por uma lista vale para cada item, e um campo intermediário `null` aparece como
|
|
224
|
+
`null`. Declare em `context` só o que orienta a conversa: o exemplo deixa fora contato, documento e
|
|
225
|
+
endereço, e o nome já vai como `title`. Um caminho que termina num objeto ou numa lista leva tudo o
|
|
226
|
+
que está abaixo dele, inclusive campos acrescentados depois à saída; para conferir num teste o que
|
|
227
|
+
cada caminho alcança, use `assistantContextLeaves`, de `@softize/opus/assistant-schema`, sobre o
|
|
228
|
+
schema de saída do manifest. O `title` precisa apontar para texto ou número.
|
|
229
|
+
|
|
230
|
+
O `opus check` cobra a forma da declaração (regra `assistant-declaration`), o `opus gen` recusa
|
|
231
|
+
`title`, caminho de `context` ou entrada que não conferem com os schemas, e o registro recusa duas
|
|
232
|
+
actions donas do mesmo recurso. O manifest, `runtime.aiTools()` e o MCP (`com.softize.opus/assistant`
|
|
233
|
+
em `_meta`) levam a declaração, e o manifest passa a mostrar também a publicação para a IA.
|
|
234
|
+
|
|
235
|
+
### Ler o recurso pela action dona
|
|
236
|
+
|
|
237
|
+
O recurso de um assistente é lido, e o acesso a ele é conferido, somente pela action dona.
|
|
238
|
+
`runtime.readResource` executa a action com `{ id }` pelo mesmo `execute` de qualquer chamada; a
|
|
239
|
+
autorização, a auditoria e a marca de dado pessoal são as da action:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const read = await runtime.readResource('customer', id, base, { context: true })
|
|
243
|
+
if (!read.ok) return notFound() // o erro da action, com a categoria dela
|
|
244
|
+
read.resource // { kind: 'customer', id, title: 'Maria Souza', label: 'Assistente do cliente', skill: 'review-customer' }
|
|
245
|
+
read.context // { data, truncated, omitted, personalData: true }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Sem `{ context: true }`, a leitura devolve só o descritor, que basta para conferir o acesso e
|
|
249
|
+
mostrar o nome. Um tipo sem action dona devolve `runtime.assistant_resource_unknown` sem executar
|
|
250
|
+
nada. `runtime.assistants()` lista as declarações com a action dona de cada uma, para o produto
|
|
251
|
+
montar rotas e telas sem lista paralela e para conferir, num teste, que cada `skill` existe no
|
|
252
|
+
registro de procedimentos dele. O Opus não enxerga as rotas HTTP que o produto registra por conta
|
|
253
|
+
própria: um teste do produto deve garantir que nenhuma rota confere acesso a recurso sem passar por
|
|
254
|
+
uma action.
|
|
255
|
+
|
|
256
|
+
### O dado que vai ao modelo
|
|
257
|
+
|
|
258
|
+
`context.data` traz só os caminhos declarados, com a estrutura da saída. Quando o JSON passa do
|
|
259
|
+
teto, o corte acontece por etapas, até caber, e fica registrado em `omitted`:
|
|
260
|
+
|
|
261
|
+
1. sai o último item da maior lista, repetidamente;
|
|
262
|
+
2. com as listas vazias, o texto mais longo é encurtado pela metade, até 200 caracteres;
|
|
263
|
+
3. por fim, saem campos inteiros, do último declarado para o primeiro.
|
|
264
|
+
|
|
265
|
+
Com qualquer corte, `truncated` vale `true`. O título fica fora do teto e é encurtado a 200
|
|
266
|
+
caracteres. Um caminho que falta na saída em execução é omitido com o motivo `missing` e gera um
|
|
267
|
+
aviso no log, sem falhar a leitura. Quem recebe a saída por outro canal, como o MCP, aplica a mesma
|
|
268
|
+
projeção com `projectAssistantContext(assistant, output)`.
|
|
269
|
+
|
|
270
|
+
Entregue o dado ao modelo como bloco delimitado, numa mensagem de dado, nunca no prompt de sistema.
|
|
271
|
+
`resourceDataBlock`, de `@softize/opus/ai`, monta o bloco com o tipo, o id, o título, os dados e,
|
|
272
|
+
quando houve corte ou ausência, `truncated` e `omitted`, e escapa dentro dos valores o que poderia
|
|
273
|
+
fechar o delimitador. Quando o modelo chama a action dona como ferramenta, recebe a saída completa
|
|
274
|
+
do contrato, como em qualquer chamada: a projeção governa só o que o host entrega por iniciativa
|
|
275
|
+
própria. Cada execução de uma action com dado pessoal é uma exposição registrada, então reaproveite
|
|
276
|
+
a leitura que já conferiu o acesso em vez de executar a action de novo só para obter o contexto.
|
|
277
|
+
|
|
182
278
|
## Prototipar com dados simulados
|
|
183
279
|
|
|
184
280
|
Para desenhar a interface antes de o backend existir, rode o app em modo design. Nesse modo, toda
|
|
@@ -86,8 +86,10 @@ const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
|
|
|
86
86
|
usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
|
|
87
87
|
`description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
|
|
88
88
|
`metadata.dataProducts` lista os [Produtos de Dados](/docs/data-products) relacionados e
|
|
89
|
-
`metadata.dataProductLabels` associa cada identificador ao seu nome legível.
|
|
90
|
-
|
|
89
|
+
`metadata.dataProductLabels` associa cada identificador ao seu nome legível. Na action dona de um
|
|
90
|
+
tipo de recurso, `metadata.assistant` traz o [assistente declarado](/docs/actions): o recurso, a
|
|
91
|
+
chave do procedimento, o nome, o campo do título, os campos do dado ao modelo e o teto. Esses
|
|
92
|
+
metadados servem ao host: não entram no prompt e não concedem acesso.
|
|
91
93
|
|
|
92
94
|
O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
|
|
93
95
|
para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
|
|
@@ -21,7 +21,8 @@ opus seed check # bindings, dependências, ciclos e scripts paralelos de s
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
`opus check` lê o código-fonte sem executá-lo e aplica regras em quatro grupos: actions
|
|
24
|
-
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`
|
|
24
|
+
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind` e
|
|
25
|
+
`assistant-declaration`, que cobra a forma do [assistente do recurso](/docs/actions)), UI
|
|
25
26
|
(`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`),
|
|
26
27
|
[Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
|
|
27
28
|
`-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`) e
|
|
@@ -441,6 +441,9 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Arqu
|
|
|
441
441
|
- O comportamento selecionado por `mode` determina o papel de acessibilidade; `role` não configura
|
|
442
442
|
o comportamento.
|
|
443
443
|
- O foco permanece dentro do diálogo aberto e retorna ao gatilho após o fechamento.
|
|
444
|
+
- No modo padrão, uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente,
|
|
445
|
+
continua utilizável com o diálogo aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc
|
|
446
|
+
age onde a pessoa está. O modo `alert` cobre a camada até a resposta.
|
|
444
447
|
- `DialogHost` apresenta uma solicitação por vez para que cada resposta tenha um alvo inequívoco.
|
|
445
448
|
|
|
446
449
|
## Propriedades de Dialog
|
|
@@ -170,6 +170,9 @@ de continuar, use [`Dialog mode="alert"`](/ui/dialog), que não fecha pelo cliqu
|
|
|
170
170
|
- Com `modal` ativo, o padrão, o foco permanece dentro do painel e o restante da página fica
|
|
171
171
|
inacessível até o fechamento.
|
|
172
172
|
- Ao fechar, o foco retorna ao gatilho.
|
|
173
|
+
- Uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente, continua utilizável
|
|
174
|
+
com o painel lateral aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc age onde a
|
|
175
|
+
pessoa está. Ela recebe o lado do Drawer em `data-modal-side` para sair da frente dele.
|
|
173
176
|
- O botão de fechar tem o nome acessível `Fechar`. Botões só com ícone no cabeçalho precisam de
|
|
174
177
|
`aria-label`.
|
|
175
178
|
|