@superxd-studio/adptr 0.5.0 → 0.6.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/GENERATIVE.md +1 -1
- package/README.md +20 -11
- package/REUSE.md +11 -1
- package/REVIEW.md +2 -2
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/chunks/{callout-DKMHSnjc.js → callout-B89MDU9m.js} +31 -35
- package/chunks/{canvas-BJ15-XxE.js → canvas-Dd78FPe0.js} +9 -9
- package/chunks/{text-field-BLtoaZFn.js → text-field-mMHllx03.js} +1 -1
- package/chunks/thinking-DlBmZsgc.js +13 -0
- package/chunks/{ui-9DyNsYXY.js → ui-WR0qTNm7.js} +309 -260
- package/generative/index.js +156 -155
- package/interactions/index.js +1073 -499
- package/package.json +2 -1
- package/source/components/icons.tsx +14 -0
- package/source/components/peek.tsx +113 -0
- package/source/components/thinking.tsx +13 -2
- package/source/reusable/generative/index.tsx +14 -9
- package/source/reusable/generative/vocabulary.tsx +4 -2
- package/source/reusable/interactions/context.tsx +4 -0
- package/source/reusable/interactions/copy.ts +39 -0
- package/source/reusable/interactions/gate.tsx +246 -0
- package/source/reusable/interactions/index.ts +14 -0
- package/source/reusable/interactions/presentation.tsx +4 -1
- package/source/reusable/interactions/progress.tsx +240 -0
- package/source/reusable/interactions/receipt.tsx +230 -0
- package/source/reusable/interactions/run.tsx +206 -0
- package/source/reusable/ui/index.ts +1 -0
- package/source/reusable/web/review/index.tsx +21 -10
- package/source/styles/library.css +56 -0
- package/styles.css +1 -1
- package/types/components/icons.d.ts +8 -0
- package/types/components/peek.d.ts +17 -0
- package/types/components/thinking.d.ts +3 -1
- package/types/reusable/interactions/context.d.ts +39 -0
- package/types/reusable/interactions/copy.d.ts +43 -0
- package/types/reusable/interactions/gate.d.ts +39 -0
- package/types/reusable/interactions/index.d.ts +8 -0
- package/types/reusable/interactions/progress.d.ts +45 -0
- package/types/reusable/interactions/receipt.d.ts +54 -0
- package/types/reusable/interactions/run.d.ts +83 -0
- package/types/reusable/ui/index.d.ts +1 -0
- package/ui/index.js +3 -3
- package/web/index.js +350 -353
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superxd-studio/adptr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ADPTR by SuperXD: React components and finished product flows for conversation, review and AI-assisted work, with every state designed.",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"clsx": "^2.1.1",
|
|
109
109
|
"tailwind-merge": "^3.7.0",
|
|
110
110
|
"react-aria-components": "^1.21.1",
|
|
111
|
+
"react-aria": "3.52.1",
|
|
111
112
|
"react-markdown": ">=10.0.0 <=10.1.0",
|
|
112
113
|
"remark-gfm": ">=4.0.0 <=4.0.1",
|
|
113
114
|
"@internationalized/date": "^3.12.4",
|
|
@@ -136,6 +136,20 @@ export const IconEdit = createIcon(
|
|
|
136
136
|
</>,
|
|
137
137
|
);
|
|
138
138
|
export const IconCheck = createIcon("Check", <path d="M20 6 9 17l-5-5" />);
|
|
139
|
+
export const IconBookOpen = createIcon(
|
|
140
|
+
"BookOpen",
|
|
141
|
+
<>
|
|
142
|
+
<path d="M12 7v14" />
|
|
143
|
+
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
|
144
|
+
</>,
|
|
145
|
+
);
|
|
146
|
+
export const IconTerminal = createIcon(
|
|
147
|
+
"Terminal",
|
|
148
|
+
<>
|
|
149
|
+
<path d="M12 19h8" />
|
|
150
|
+
<path d="m4 17 6-6-6-6" />
|
|
151
|
+
</>,
|
|
152
|
+
);
|
|
139
153
|
|
|
140
154
|
export const IconX = createIcon(
|
|
141
155
|
"X",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { mergeProps, useButton, useFocusRing, useHover } from "react-aria";
|
|
3
|
+
import { Dialog, Popover } from "react-aria-components";
|
|
4
|
+
import { PortalTheme } from "./ui/portal-theme.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
|
|
7
|
+
/** How long the pointer rests on a word before its detail peeks open, and lingers after */
|
|
8
|
+
const OPEN_MS = 280;
|
|
9
|
+
const CLOSE_MS = 160;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A word in a sentence with more behind it, like an explorable explanation or the hover card
|
|
13
|
+
* behind an @mention. Resting the pointer on it peeks the detail open; pressing it (click,
|
|
14
|
+
* tap, Enter or Space) keeps it open as a small dialog, closed with Escape or by pressing
|
|
15
|
+
* elsewhere. The dotted underline is the only hint, so the sentence still reads as text.
|
|
16
|
+
*
|
|
17
|
+
* A file looks like a link, as in VS Code: ultramarine and monospaced, and it opens when pressed
|
|
18
|
+
* rather than when the pointer rests on it.
|
|
19
|
+
*/
|
|
20
|
+
export function Peek({
|
|
21
|
+
label,
|
|
22
|
+
title,
|
|
23
|
+
look = "word",
|
|
24
|
+
children,
|
|
25
|
+
}: {
|
|
26
|
+
label: ReactNode;
|
|
27
|
+
/** What the detail is, for screen readers */
|
|
28
|
+
title: string;
|
|
29
|
+
look?: "word" | "file";
|
|
30
|
+
children: ReactNode | ((close: () => void) => ReactNode);
|
|
31
|
+
}) {
|
|
32
|
+
const hovers = look === "word";
|
|
33
|
+
// A span, not a <button>, so a long phrase wraps with the sentence like ordinary text
|
|
34
|
+
const trigger = useRef<HTMLSpanElement>(null);
|
|
35
|
+
const timer = useRef<number | undefined>(undefined);
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [pinned, setPinned] = useState(false);
|
|
38
|
+
useEffect(() => () => window.clearTimeout(timer.current), []);
|
|
39
|
+
const later = (next: boolean, ms: number) => {
|
|
40
|
+
window.clearTimeout(timer.current);
|
|
41
|
+
timer.current = window.setTimeout(() => setOpen(next), ms);
|
|
42
|
+
};
|
|
43
|
+
const close = () => {
|
|
44
|
+
window.clearTimeout(timer.current);
|
|
45
|
+
setOpen(false);
|
|
46
|
+
setPinned(false);
|
|
47
|
+
};
|
|
48
|
+
const { buttonProps } = useButton(
|
|
49
|
+
{
|
|
50
|
+
elementType: "span",
|
|
51
|
+
"aria-expanded": open,
|
|
52
|
+
onPress: () => {
|
|
53
|
+
window.clearTimeout(timer.current);
|
|
54
|
+
if (pinned) return close();
|
|
55
|
+
setPinned(true);
|
|
56
|
+
setOpen(true);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
trigger,
|
|
60
|
+
);
|
|
61
|
+
const { hoverProps, isHovered } = useHover({
|
|
62
|
+
onHoverStart: () => hovers && !pinned && later(true, OPEN_MS),
|
|
63
|
+
onHoverEnd: () => hovers && !pinned && later(false, CLOSE_MS),
|
|
64
|
+
});
|
|
65
|
+
const { focusProps, isFocusVisible } = useFocusRing();
|
|
66
|
+
return (
|
|
67
|
+
<PortalTheme>
|
|
68
|
+
{(theme) => (
|
|
69
|
+
<>
|
|
70
|
+
<span
|
|
71
|
+
ref={trigger}
|
|
72
|
+
{...mergeProps(buttonProps, hoverProps, focusProps)}
|
|
73
|
+
data-hovered={isHovered || undefined}
|
|
74
|
+
data-focus-visible={isFocusVisible || undefined}
|
|
75
|
+
className={cn(
|
|
76
|
+
"cursor-pointer rounded-cell wrap-anywhere outline-none data-focus-visible:focus-ring",
|
|
77
|
+
look === "word"
|
|
78
|
+
? "font-medium text-fg-emphasis underline decoration-line-emphasis decoration-dotted decoration-[1.5px] underline-offset-[5px] transition-[text-decoration-color] duration-(--duration-snap) ease-snap data-hovered:decoration-fg-subtle"
|
|
79
|
+
: "font-mono text-done underline-offset-[3px] data-hovered:underline",
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{label}
|
|
83
|
+
</span>
|
|
84
|
+
<Popover
|
|
85
|
+
triggerRef={trigger}
|
|
86
|
+
isOpen={open}
|
|
87
|
+
onOpenChange={(next) => (next ? setOpen(true) : close())}
|
|
88
|
+
// A peek doesn't take focus or block the page; a pressed word opens a real dialog
|
|
89
|
+
isNonModal={!pinned}
|
|
90
|
+
placement="bottom start"
|
|
91
|
+
offset={8}
|
|
92
|
+
style={theme.style}
|
|
93
|
+
className={cn(
|
|
94
|
+
theme.className,
|
|
95
|
+
// Long details scroll inside the peek rather than running off the screen
|
|
96
|
+
"z-50 max-h-[min(24rem,60vh)] w-80 max-w-[calc(100vw-2rem)] origin-(--trigger-anchor-point) overflow-y-auto rounded-surface border border-line-subtle bg-surface p-4 wrap-anywhere text-fg shadow-popover outline-none data-entering:animate-pop-in data-exiting:animate-pop-out",
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<Dialog aria-label={title} className="outline-none">
|
|
100
|
+
{/* Moving the pointer into the detail keeps it open */}
|
|
101
|
+
<div
|
|
102
|
+
onPointerEnter={() => !pinned && window.clearTimeout(timer.current)}
|
|
103
|
+
onPointerLeave={() => !pinned && later(false, CLOSE_MS)}
|
|
104
|
+
>
|
|
105
|
+
{typeof children === "function" ? children(close) : children}
|
|
106
|
+
</div>
|
|
107
|
+
</Dialog>
|
|
108
|
+
</Popover>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</PortalTheme>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -5,9 +5,20 @@ import { cn } from "../lib/cn.js";
|
|
|
5
5
|
* length (an AI preparing an answer). About the height of a line of text. Decorative: say
|
|
6
6
|
* what's happening in words beside it.
|
|
7
7
|
*/
|
|
8
|
-
export function Thinking({
|
|
8
|
+
export function Thinking({
|
|
9
|
+
className,
|
|
10
|
+
tone = "brand",
|
|
11
|
+
}: {
|
|
12
|
+
className?: string;
|
|
13
|
+
/** brand (ultramarine) while the system works; info (calm blue) while it waits on a person */
|
|
14
|
+
tone?: "brand" | "info";
|
|
15
|
+
}) {
|
|
9
16
|
return (
|
|
10
|
-
<span
|
|
17
|
+
<span
|
|
18
|
+
aria-hidden="true"
|
|
19
|
+
data-tone={tone === "info" ? "info" : undefined}
|
|
20
|
+
className={cn("thinking-matrix", className)}
|
|
21
|
+
>
|
|
11
22
|
{Array.from({ length: 9 }, (_, i) => (
|
|
12
23
|
<i key={i} />
|
|
13
24
|
))}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { renderGenerativeUI } from "@assistant-ui/react-generative-ui";
|
|
14
14
|
import { Button } from "../../components/button.js";
|
|
15
15
|
import { Callout } from "../../components/callout.js";
|
|
16
|
+
import { IconArrowLeft } from "../../components/icons.js";
|
|
16
17
|
import { Spinner } from "../../components/spinner.js";
|
|
17
18
|
import { generativeCopy as defaultCopy, type GenerativeCopy } from "./copy.js";
|
|
18
19
|
import { parseStructuredSpec, type StructuredSpec } from "./schema.js";
|
|
@@ -390,10 +391,11 @@ function ResponseView({
|
|
|
390
391
|
{!retainSteps ? (
|
|
391
392
|
<Button
|
|
392
393
|
size="sm"
|
|
393
|
-
variant="
|
|
394
|
+
variant="secondary"
|
|
394
395
|
className="self-start"
|
|
395
396
|
onPress={() => controller.reset()}
|
|
396
397
|
>
|
|
398
|
+
<IconArrowLeft />
|
|
397
399
|
{copy.back}
|
|
398
400
|
</Button>
|
|
399
401
|
) : null}
|
|
@@ -428,9 +430,19 @@ function ResponseView({
|
|
|
428
430
|
))}
|
|
429
431
|
</dl>
|
|
430
432
|
) : null}
|
|
431
|
-
|
|
433
|
+
{/* Back on the left, Confirm on the right */}
|
|
434
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
435
|
+
<Button
|
|
436
|
+
variant="secondary"
|
|
437
|
+
isDisabled={snapshot.stage !== "confirm"}
|
|
438
|
+
onPress={() => controller.back()}
|
|
439
|
+
>
|
|
440
|
+
<IconArrowLeft />
|
|
441
|
+
{copy.back}
|
|
442
|
+
</Button>
|
|
432
443
|
<Button
|
|
433
444
|
variant="primary"
|
|
445
|
+
className="ml-auto"
|
|
434
446
|
isDisabled={snapshot.stage !== "confirm"}
|
|
435
447
|
aria-busy={snapshot.stage !== "confirm"}
|
|
436
448
|
aria-label={
|
|
@@ -449,13 +461,6 @@ function ResponseView({
|
|
|
449
461
|
? copy.working
|
|
450
462
|
: copy.confirm}
|
|
451
463
|
</Button>
|
|
452
|
-
<Button
|
|
453
|
-
variant="ghost"
|
|
454
|
-
isDisabled={snapshot.stage !== "confirm"}
|
|
455
|
-
onPress={() => controller.back()}
|
|
456
|
-
>
|
|
457
|
-
{copy.back}
|
|
458
|
-
</Button>
|
|
459
464
|
</div>
|
|
460
465
|
{snapshot.stage !== "confirm" ? (
|
|
461
466
|
<p role="status" className="sr-only">
|
|
@@ -820,8 +820,10 @@ function GeneratedButton({
|
|
|
820
820
|
: "secondary"
|
|
821
821
|
}
|
|
822
822
|
className={cn(
|
|
823
|
-
|
|
824
|
-
|
|
823
|
+
// Buttons fit their words and sit at the right end; a "block" button spans the width
|
|
824
|
+
// only on phones
|
|
825
|
+
"h-auto min-h-control-md max-w-full self-end py-control-y wrap-anywhere whitespace-normal",
|
|
826
|
+
block && "w-full sm:w-auto",
|
|
825
827
|
)}
|
|
826
828
|
isDisabled={
|
|
827
829
|
blocked ||
|
|
@@ -32,6 +32,10 @@ export function InteractionProvider({
|
|
|
32
32
|
agentStates: { ...parent.agentStates, ...copy.agentStates },
|
|
33
33
|
artifactStates: { ...parent.artifactStates, ...copy.artifactStates },
|
|
34
34
|
connection: { ...parent.connection, ...copy.connection },
|
|
35
|
+
gate: { ...parent.gate, ...copy.gate },
|
|
36
|
+
run: { ...parent.run, ...copy.run },
|
|
37
|
+
receipt: { ...parent.receipt, ...copy.receipt },
|
|
38
|
+
progress: { ...parent.progress, ...copy.progress },
|
|
35
39
|
};
|
|
36
40
|
return <CopyContext.Provider value={merged}>{children}</CopyContext.Provider>;
|
|
37
41
|
}
|
|
@@ -114,4 +114,43 @@ export const interactionCopy = {
|
|
|
114
114
|
noThreads: "No conversations in this view.",
|
|
115
115
|
threadError: "Couldn’t complete that request. The current conversation is still here.",
|
|
116
116
|
loading: "Loading",
|
|
117
|
+
/** The Decision Gate */
|
|
118
|
+
gate: {
|
|
119
|
+
review: "Review",
|
|
120
|
+
approve: "Approve once",
|
|
121
|
+
approving: "Approving",
|
|
122
|
+
decline: "Decline",
|
|
123
|
+
edit: "Edit before approving",
|
|
124
|
+
irreversible: "Can't be undone",
|
|
125
|
+
waiting: "Waiting for your approval",
|
|
126
|
+
changed: "Something changed while you were deciding",
|
|
127
|
+
reviewChange: "Review the change",
|
|
128
|
+
reconsider: "Reconsider",
|
|
129
|
+
},
|
|
130
|
+
/** The Action Receipt */
|
|
131
|
+
receipt: {
|
|
132
|
+
whatChanged: "What changed",
|
|
133
|
+
checking: "Checking what happened",
|
|
134
|
+
states: {
|
|
135
|
+
changed: "Changed",
|
|
136
|
+
"not-changed": "Not changed",
|
|
137
|
+
unknown: "Not known yet",
|
|
138
|
+
reverted: "Reverted",
|
|
139
|
+
},
|
|
140
|
+
minutesLeft: (minutes: number) => (minutes === 1 ? "1 min left" : `${minutes} min left`),
|
|
141
|
+
closed: "Undo has closed",
|
|
142
|
+
},
|
|
143
|
+
/** Agent task progress */
|
|
144
|
+
progress: {
|
|
145
|
+
showStages: "Show stages",
|
|
146
|
+
stagesDone: (done: number, total: number) => `${done} of ${total} stages`,
|
|
147
|
+
noAction: "No action needed",
|
|
148
|
+
review: "Review",
|
|
149
|
+
stageWords: { done: "done", now: "now", next: "to come" },
|
|
150
|
+
},
|
|
151
|
+
/** An agent's run */
|
|
152
|
+
run: {
|
|
153
|
+
number: (value: number) => new Intl.NumberFormat("en").format(value),
|
|
154
|
+
lines: (added: string, removed: string) => `${added} lines added, ${removed} removed`,
|
|
155
|
+
},
|
|
117
156
|
} as const;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useId, useState, type ReactNode } from "react";
|
|
2
|
+
import { Button } from "../../components/button.js";
|
|
3
|
+
import { IconAlert, IconCheck, IconChevronDown, IconEdit, IconX } from "../../components/icons.js";
|
|
4
|
+
import { Thinking } from "../../components/thinking.js";
|
|
5
|
+
import { Tooltip } from "../../components/ui/navigation.js";
|
|
6
|
+
import { cn } from "../../lib/cn.js";
|
|
7
|
+
import { useInteractionCopy } from "./context.js";
|
|
8
|
+
import { RunEntry, useInRun } from "./run.js";
|
|
9
|
+
|
|
10
|
+
/** Where the request is: waiting for the person, being carried out, or settled */
|
|
11
|
+
export type DecisionGateStatus = "waiting" | "approving" | "approved" | "declined";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Text buttons whose words can wrap when text is very large or the space very narrow, between
|
|
15
|
+
* words rather than inside them
|
|
16
|
+
*/
|
|
17
|
+
const wraps = "h-auto min-h-control-sm whitespace-normal break-words py-1.5 text-center";
|
|
18
|
+
|
|
19
|
+
export interface DecisionGateProps {
|
|
20
|
+
/** What the agent wants to do, in a few words. Closed, it's shortened to one line. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Approving can't be undone: a red mark beside the title, and a red tag when open */
|
|
23
|
+
irreversible?: boolean;
|
|
24
|
+
status?: DecisionGateStatus;
|
|
25
|
+
/** The title once approved, as a receipt */
|
|
26
|
+
approvedTitle?: string;
|
|
27
|
+
/** The title once declined */
|
|
28
|
+
declinedTitle?: string;
|
|
29
|
+
/** What will happen, in one sentence. Put details behind its words with Peek. */
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
onApprove: () => void;
|
|
32
|
+
onDecline: () => void;
|
|
33
|
+
/** Shows the Edit button */
|
|
34
|
+
onEdit?: () => void;
|
|
35
|
+
/** The host's form for changing the request. While it's shown, it replaces the sentence. */
|
|
36
|
+
editor?: ReactNode;
|
|
37
|
+
/** Something changed while the person was deciding. Approval waits until they've seen it. */
|
|
38
|
+
changed?: ReactNode;
|
|
39
|
+
onReviewChange?: () => void;
|
|
40
|
+
/** Shows Reconsider once declined */
|
|
41
|
+
onReconsider?: () => void;
|
|
42
|
+
/** What the Approve button says while the request is carried out */
|
|
43
|
+
approvingLabel?: string;
|
|
44
|
+
defaultExpanded?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The Decision Gate: what an agent wants to do, in one contained card. Closed, it's a tile with
|
|
49
|
+
* the pixel matrix, the title and Review. Review opens it like an accordion to the sentence and
|
|
50
|
+
* the decision. Nothing is approved from the closed card, so the person approves what they read.
|
|
51
|
+
*
|
|
52
|
+
* The interface asks; the product enforces. The host decides what each answer does and passes
|
|
53
|
+
* the status back.
|
|
54
|
+
*/
|
|
55
|
+
export function DecisionGate(props: DecisionGateProps) {
|
|
56
|
+
const card = <GateCard {...props} />;
|
|
57
|
+
if (!useInRun()) return card;
|
|
58
|
+
const status = props.status ?? "waiting";
|
|
59
|
+
return (
|
|
60
|
+
<RunEntry
|
|
61
|
+
card
|
|
62
|
+
dot={status === "approved" ? "success" : status === "declined" ? "note" : "waiting"}
|
|
63
|
+
>
|
|
64
|
+
{card}
|
|
65
|
+
</RunEntry>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function GateCard({
|
|
70
|
+
title,
|
|
71
|
+
irreversible = false,
|
|
72
|
+
status = "waiting",
|
|
73
|
+
approvedTitle,
|
|
74
|
+
declinedTitle,
|
|
75
|
+
children,
|
|
76
|
+
onApprove,
|
|
77
|
+
onDecline,
|
|
78
|
+
onEdit,
|
|
79
|
+
editor,
|
|
80
|
+
changed,
|
|
81
|
+
onReviewChange,
|
|
82
|
+
onReconsider,
|
|
83
|
+
approvingLabel,
|
|
84
|
+
defaultExpanded = false,
|
|
85
|
+
}: DecisionGateProps) {
|
|
86
|
+
const copy = useInteractionCopy().gate;
|
|
87
|
+
const [open, setOpen] = useState(defaultExpanded);
|
|
88
|
+
const panelId = useId();
|
|
89
|
+
const approved = status === "approved";
|
|
90
|
+
const declined = status === "declined";
|
|
91
|
+
const settled = approved || declined;
|
|
92
|
+
const editing = editor !== undefined && editor !== null;
|
|
93
|
+
const expanded = !settled && (open || editing);
|
|
94
|
+
const deciding = status === "waiting" && !changed && !editing;
|
|
95
|
+
const heading = approved ? (approvedTitle ?? title) : declined ? (declinedTitle ?? title) : title;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="@container/gate overflow-hidden rounded-surface border border-line-subtle bg-surface-muted">
|
|
99
|
+
<p role="status" className="sr-only">
|
|
100
|
+
{approved
|
|
101
|
+
? heading
|
|
102
|
+
: declined
|
|
103
|
+
? heading
|
|
104
|
+
: status === "approving"
|
|
105
|
+
? (approvingLabel ?? copy.approving)
|
|
106
|
+
: changed
|
|
107
|
+
? copy.changed
|
|
108
|
+
: ""}
|
|
109
|
+
</p>
|
|
110
|
+
{/*
|
|
111
|
+
* Tile, title and Review in a row. With very large text in a narrow panel they stack.
|
|
112
|
+
*/}
|
|
113
|
+
<div className="flex flex-wrap items-start gap-3 p-3">
|
|
114
|
+
{/* The matrix says it's waiting; screen readers hear it in words */}
|
|
115
|
+
<span
|
|
116
|
+
{...(settled ? { "aria-hidden": true } : { role: "img", "aria-label": copy.waiting })}
|
|
117
|
+
className={cn(
|
|
118
|
+
"flex size-10 shrink-0 items-center justify-center rounded-control border border-line-subtle bg-surface",
|
|
119
|
+
approved && "border-done bg-done text-on-done",
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{approved ? (
|
|
123
|
+
<IconCheck className="size-4 animate-done-in" />
|
|
124
|
+
) : declined ? (
|
|
125
|
+
<IconX className="size-4 text-fg-subtle" />
|
|
126
|
+
) : (
|
|
127
|
+
<Thinking tone="info" />
|
|
128
|
+
)}
|
|
129
|
+
</span>
|
|
130
|
+
{/*
|
|
131
|
+
* Closed, the card is just its title, on one line, ending in "…" if it's longer, and a
|
|
132
|
+
* red mark if it can't be undone. Review opens the title in full, with the rest. Screen
|
|
133
|
+
* readers always hear all of it.
|
|
134
|
+
*/}
|
|
135
|
+
<div className="flex min-h-10 min-w-0 flex-1 basis-24 items-center gap-1.5">
|
|
136
|
+
<span
|
|
137
|
+
className={cn(
|
|
138
|
+
"min-w-0 type-label text-fg-emphasis",
|
|
139
|
+
expanded ? "text-pretty wrap-anywhere" : "truncate",
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{heading}
|
|
143
|
+
</span>
|
|
144
|
+
{irreversible && !settled && !expanded ? (
|
|
145
|
+
<span role="img" aria-label={copy.irreversible} className="flex shrink-0">
|
|
146
|
+
<IconAlert className="size-4 text-error" />
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
149
|
+
</div>
|
|
150
|
+
<span className="ml-auto flex min-h-10 shrink-0 items-center">
|
|
151
|
+
{settled ? (
|
|
152
|
+
declined && onReconsider ? (
|
|
153
|
+
<Button size="sm" className={wraps} onPress={onReconsider}>
|
|
154
|
+
{copy.reconsider}
|
|
155
|
+
</Button>
|
|
156
|
+
) : null
|
|
157
|
+
) : (
|
|
158
|
+
<Button
|
|
159
|
+
size="sm"
|
|
160
|
+
className={cn("group/trigger", wraps)}
|
|
161
|
+
aria-label={copy.review}
|
|
162
|
+
aria-expanded={expanded}
|
|
163
|
+
aria-controls={panelId}
|
|
164
|
+
isDisabled={editing}
|
|
165
|
+
onPress={() => setOpen((value) => !value)}
|
|
166
|
+
>
|
|
167
|
+
{/* In a narrow space, just the chevron */}
|
|
168
|
+
<span className="@max-sm/gate:hidden">{copy.review}</span>
|
|
169
|
+
<IconChevronDown className="transition-[rotate] duration-(--duration-settle) ease-settle group-aria-expanded/trigger:rotate-180 motion-reduce:transition-none" />
|
|
170
|
+
</Button>
|
|
171
|
+
)}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{changed && !settled ? (
|
|
176
|
+
<div className="flex flex-col gap-3 border-t border-line-subtle px-4 py-3">
|
|
177
|
+
<p className="type-body text-pretty text-attention">{changed}</p>
|
|
178
|
+
{onReviewChange ? (
|
|
179
|
+
<div className="flex justify-end">
|
|
180
|
+
<Button size="sm" className={wraps} onPress={onReviewChange}>
|
|
181
|
+
{copy.reviewChange}
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
) : null}
|
|
187
|
+
|
|
188
|
+
{expanded ? (
|
|
189
|
+
<div
|
|
190
|
+
id={panelId}
|
|
191
|
+
className="flex animate-reveal-in flex-col gap-4 border-t border-line-subtle bg-surface p-4"
|
|
192
|
+
>
|
|
193
|
+
{editing ? (
|
|
194
|
+
editor
|
|
195
|
+
) : (
|
|
196
|
+
<>
|
|
197
|
+
{irreversible ? (
|
|
198
|
+
<span className="inline-flex items-center gap-1 self-start rounded-pill border border-error-line bg-error-subtle px-2 py-0.5 type-caption break-words text-error">
|
|
199
|
+
<IconAlert className="size-3.5 shrink-0" />
|
|
200
|
+
{copy.irreversible}
|
|
201
|
+
</span>
|
|
202
|
+
) : null}
|
|
203
|
+
<div className="type-body text-pretty wrap-anywhere text-fg">{children}</div>
|
|
204
|
+
{/* The decision sits at the right end, Approve once furthest right */}
|
|
205
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
206
|
+
{onEdit ? (
|
|
207
|
+
<Tooltip content={copy.edit}>
|
|
208
|
+
<Button
|
|
209
|
+
variant="secondary"
|
|
210
|
+
size="icon-sm"
|
|
211
|
+
aria-label={copy.edit}
|
|
212
|
+
onPress={onEdit}
|
|
213
|
+
isDisabled={!deciding}
|
|
214
|
+
>
|
|
215
|
+
<IconEdit />
|
|
216
|
+
</Button>
|
|
217
|
+
</Tooltip>
|
|
218
|
+
) : null}
|
|
219
|
+
<Button
|
|
220
|
+
variant="secondary"
|
|
221
|
+
size="sm"
|
|
222
|
+
className={cn("text-error", wraps)}
|
|
223
|
+
onPress={onDecline}
|
|
224
|
+
isDisabled={!deciding}
|
|
225
|
+
>
|
|
226
|
+
{copy.decline}
|
|
227
|
+
</Button>
|
|
228
|
+
<Button
|
|
229
|
+
variant="primary"
|
|
230
|
+
size="sm"
|
|
231
|
+
className={wraps}
|
|
232
|
+
onPress={onApprove}
|
|
233
|
+
isDisabled={Boolean(changed)}
|
|
234
|
+
isPending={status === "approving"}
|
|
235
|
+
pendingLabel={approvingLabel ?? copy.approving}
|
|
236
|
+
>
|
|
237
|
+
{copy.approve}
|
|
238
|
+
</Button>
|
|
239
|
+
</div>
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
) : null}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -4,6 +4,20 @@ export * from "./feedback.js";
|
|
|
4
4
|
export * from "./threads.js";
|
|
5
5
|
export * from "./presentation.js";
|
|
6
6
|
export * from "./web.js";
|
|
7
|
+
export { RunTimeline, RunStep, RunThought, RunMessage, RunWorking } from "./run.js";
|
|
8
|
+
export type { RunFile } from "./run.js";
|
|
9
|
+
export { DecisionGate } from "./gate.js";
|
|
10
|
+
export type { DecisionGateProps, DecisionGateStatus } from "./gate.js";
|
|
11
|
+
export { ActionReceipt } from "./receipt.js";
|
|
12
|
+
export type {
|
|
13
|
+
ActionReceiptProps,
|
|
14
|
+
ReceiptAction,
|
|
15
|
+
ReceiptChange,
|
|
16
|
+
ReceiptChangeState,
|
|
17
|
+
ReceiptOutcome,
|
|
18
|
+
} from "./receipt.js";
|
|
19
|
+
export { TaskProgress } from "./progress.js";
|
|
20
|
+
export type { TaskProgressMove, TaskProgressProps, TaskProgressStatus } from "./progress.js";
|
|
7
21
|
export { interactionCopy } from "./copy.js";
|
|
8
22
|
export { jsonSchema } from "./store.js";
|
|
9
23
|
|
|
@@ -333,7 +333,10 @@ export function ComposerTools({
|
|
|
333
333
|
/>
|
|
334
334
|
)}{" "}
|
|
335
335
|
{commands.length > 0 && onCommand && (
|
|
336
|
-
|
|
336
|
+
// In a column, a wrapper lets the Commands button fit its words
|
|
337
|
+
<div>
|
|
338
|
+
<Menu label={copy.commands} items={commands} onAction={onCommand} />
|
|
339
|
+
</div>
|
|
337
340
|
)}
|
|
338
341
|
</div>
|
|
339
342
|
);
|