@dowel-ui/react 0.2.0 → 0.3.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/README.md +10 -4
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts +50 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.d.ts.map +1 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js +156 -0
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -0
- package/dist/components/ai-agent-plan/index.d.ts +2 -0
- package/dist/components/ai-agent-plan/index.js +2 -0
- package/dist/components/ai-agent-plan/meta.js +17 -0
- package/dist/components/ai-agent-plan/meta.js.map +1 -0
- package/dist/components/ai-agent-status/ai-agent-status.d.ts +1 -1
- package/dist/components/ai-approval-request/ai-approval-request.d.ts +67 -0
- package/dist/components/ai-approval-request/ai-approval-request.d.ts.map +1 -0
- package/dist/components/ai-approval-request/ai-approval-request.js +218 -0
- package/dist/components/ai-approval-request/ai-approval-request.js.map +1 -0
- package/dist/components/ai-approval-request/index.d.ts +2 -0
- package/dist/components/ai-approval-request/index.js +2 -0
- package/dist/components/ai-approval-request/meta.js +17 -0
- package/dist/components/ai-approval-request/meta.js.map +1 -0
- package/dist/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/components/ai-response/ai-response.js +1 -0
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.d.ts.map +1 -1
- package/dist/components/ai-structured-output/ai-structured-output.js +13 -2
- package/dist/components/ai-structured-output/ai-structured-output.js.map +1 -1
- package/dist/components/ai-tool/ai-tool.d.ts +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/file-upload/file-upload.d.ts +46 -0
- package/dist/components/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload/file-upload.js +204 -0
- package/dist/components/file-upload/file-upload.js.map +1 -0
- package/dist/components/file-upload/index.d.ts +3 -0
- package/dist/components/file-upload/index.js +3 -0
- package/dist/components/file-upload/meta.js +17 -0
- package/dist/components/file-upload/meta.js.map +1 -0
- package/dist/components/file-upload/upload-queue.d.ts +87 -0
- package/dist/components/file-upload/upload-queue.d.ts.map +1 -0
- package/dist/components/file-upload/upload-queue.js +249 -0
- package/dist/components/file-upload/upload-queue.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/progress/progress.js +1 -0
- package/dist/components/progress/progress.js.map +1 -1
- package/dist/components/spinner/spinner.js +1 -0
- package/dist/components/spinner/spinner.js.map +1 -1
- package/dist/components/tags-input/index.d.ts +2 -0
- package/dist/components/tags-input/index.js +2 -0
- package/dist/components/tags-input/meta.js +17 -0
- package/dist/components/tags-input/meta.js.map +1 -0
- package/dist/components/tags-input/tags-input.d.ts +47 -0
- package/dist/components/tags-input/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input/tags-input.js +182 -0
- package/dist/components/tags-input/tags-input.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +58 -50
- package/dist/registry/components.js.map +1 -1
- package/package.json +4 -4
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-agent-plan/index.ts +10 -0
- package/src/components/ai-agent-plan/meta.ts +19 -0
- package/src/components/ai-approval-request/ai-approval-request.tsx +368 -0
- package/src/components/ai-approval-request/index.ts +7 -0
- package/src/components/ai-approval-request/meta.ts +21 -0
- package/src/components/ai-response/ai-response.tsx +1 -0
- package/src/components/ai-structured-output/ai-structured-output.tsx +25 -1
- package/src/components/file-upload/file-upload.tsx +322 -0
- package/src/components/file-upload/index.ts +20 -0
- package/src/components/file-upload/meta.ts +21 -0
- package/src/components/file-upload/upload-queue.ts +337 -0
- package/src/components/progress/progress.tsx +3 -0
- package/src/components/spinner/spinner.tsx +5 -0
- package/src/components/tags-input/index.ts +1 -0
- package/src/components/tags-input/meta.ts +23 -0
- package/src/components/tags-input/tags-input.tsx +300 -0
- package/src/index.ts +4 -0
- package/src/registry/components.ts +8 -0
package/README.md
CHANGED
|
@@ -54,17 +54,18 @@ they want to change something.
|
|
|
54
54
|
|
|
55
55
|
## What is in it
|
|
56
56
|
|
|
57
|
-
**
|
|
57
|
+
**56 components** and **8 blocks**, every one keyboard-operable and audited for
|
|
58
58
|
contrast.
|
|
59
59
|
|
|
60
60
|
**AI** — the reason this library exists
|
|
61
61
|
Conversation · Message · Response · Prompt Input · Tool Call · Reasoning ·
|
|
62
|
-
Sources · Model Selector · Token Usage · Agent Status · **
|
|
63
|
-
**
|
|
62
|
+
Sources · Model Selector · Token Usage · Agent Status · **Agent Plan** ·
|
|
63
|
+
**Approval Request** · **Action Ledger** · **Structured Output** ·
|
|
64
|
+
**Inline Completion**
|
|
64
65
|
|
|
65
66
|
**Forms**
|
|
66
67
|
Input · Label · Checkbox · Radio Group · Switch · Slider · Select · Combobox ·
|
|
67
|
-
Form · Calendar · Date Picker
|
|
68
|
+
Form · Calendar · Date Picker · **Tags Input** · **File Upload**
|
|
68
69
|
|
|
69
70
|
**Overlays**
|
|
70
71
|
Dialog · Sheet · Drawer · Popover · Tooltip · Dropdown Menu
|
|
@@ -87,6 +88,11 @@ Most "AI component libraries" ship a chat transcript and stop. Real AI features
|
|
|
87
88
|
inside real software are extraction, enrichment, autofill and agents that
|
|
88
89
|
_change things_ — and those need surfaces nobody else provides.
|
|
89
90
|
|
|
91
|
+
**`ai-agent-plan` + `ai-approval-request` + `ai-action-ledger`** — the whole
|
|
92
|
+
sequence, which no other library covers: what the agent intends, the gate where
|
|
93
|
+
you approve it (after correcting the arguments it got wrong), and the record of
|
|
94
|
+
what it actually did.
|
|
95
|
+
|
|
90
96
|
**`ai-action-ledger`** — what the agent actually did, and what can be undone.
|
|
91
97
|
Everyone ships approval _before_ a tool runs. Nothing ships the part after,
|
|
92
98
|
where a deletion can be reverted, a refund can only be offset by another
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/ai-agent-plan/ai-agent-plan.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* What an agent intends to do, and how far through it is.
|
|
5
|
+
*
|
|
6
|
+
* Not a stepper. A wizard's steps are decided up front and never change; an
|
|
7
|
+
* agent's plan is a hypothesis it revises as it learns — it adds a step when a
|
|
8
|
+
* lookup fails, drops one that turned out to be unnecessary, and reorders when
|
|
9
|
+
* a dependency appears. Every stepper treats its step list as fixed, so a plan
|
|
10
|
+
* rendered in one either silently mutates under the reader or re-renders from
|
|
11
|
+
* scratch and loses their place.
|
|
12
|
+
*
|
|
13
|
+
* So revision is the feature here, not an edge case. When the plan changes, the
|
|
14
|
+
* change is announced — "the model added a step" is information, and watching a
|
|
15
|
+
* list quietly grow is not the same as being told.
|
|
16
|
+
*
|
|
17
|
+
* Approval is not here. Deciding whether a plan may run is a different
|
|
18
|
+
* component with a different shape; this one displays and reports.
|
|
19
|
+
*/
|
|
20
|
+
type StepStatus = "pending" | "running" | "done" | "failed" | "skipped";
|
|
21
|
+
interface PlanStep {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
status: StepStatus;
|
|
25
|
+
/** What the step will do, or what it found. Shown under the title. */
|
|
26
|
+
detail?: string;
|
|
27
|
+
/** Why it failed. Shown only when the status is "failed". */
|
|
28
|
+
error?: string;
|
|
29
|
+
/** One level of sub-steps. Deeper nesting is a tree, which this is not. */
|
|
30
|
+
steps?: PlanStep[];
|
|
31
|
+
}
|
|
32
|
+
interface AgentPlanProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
33
|
+
steps: PlanStep[];
|
|
34
|
+
/** Names the plan, so the list is not an unlabelled sequence. */
|
|
35
|
+
label: string;
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
}
|
|
38
|
+
declare function AgentPlan({ className, steps, label, children, ...props }: AgentPlanProps): import("react").JSX.Element;
|
|
39
|
+
/** "Step 3 of 6" plus the overall state, in words. */
|
|
40
|
+
declare function AgentPlanSummary({ className, ...props }: ComponentPropsWithRef<"p">): import("react").JSX.Element;
|
|
41
|
+
declare function AgentPlanSteps({ className, ...props }: ComponentPropsWithRef<"ol">): import("react").JSX.Element;
|
|
42
|
+
interface AgentPlanStepProps extends Omit<ComponentPropsWithRef<"li">, "children"> {
|
|
43
|
+
step: PlanStep;
|
|
44
|
+
/** Set on sub-steps. Not part of the public API surface. */
|
|
45
|
+
nested?: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare function AgentPlanStep({ className, step, nested, ...props }: AgentPlanStepProps): import("react").JSX.Element;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { AgentPlan, AgentPlanProps, AgentPlanStep, AgentPlanStepProps, AgentPlanSteps, AgentPlanSummary, PlanStep, StepStatus };
|
|
50
|
+
//# sourceMappingURL=ai-agent-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-agent-plan.d.ts","names":[],"sources":["../../../src/components/ai-agent-plan/ai-agent-plan.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;KA+BY;UAEK;EACf;EACA;EACA,QAAQ;;EAER;;EAEA;;EAEA,QAAQ;;UAyCO,uBAAuB,KAAK;EAC3C,OAAO;;EAEP;EACA,WAAW;;iBAGG,YAAY,WAAW,OAAO,OAAO,aAAa,SAAS,iCAAc,IAAA;;iBAqDzE,mBAAmB,cAAc,SAAS,6CAA0B,IAAA;iBA2BpE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;UAkBlE,2BAA2B,KAAK;EAC/C,MAAM;;EAEN;;iBAGc,gBACd,WACA,MACA,WACG,SACF,qCAAkB,IAAA"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
5
|
+
//#region src/components/ai-agent-plan/ai-agent-plan.tsx
|
|
6
|
+
const STATUS_LABEL = {
|
|
7
|
+
pending: "Not started",
|
|
8
|
+
running: "In progress",
|
|
9
|
+
done: "Done",
|
|
10
|
+
failed: "Failed",
|
|
11
|
+
skipped: "Skipped"
|
|
12
|
+
};
|
|
13
|
+
/** Flattens one level, so counting and "step N of M" include sub-steps. */
|
|
14
|
+
function flatten(steps) {
|
|
15
|
+
return steps.flatMap((step) => [step, ...step.steps ?? []]);
|
|
16
|
+
}
|
|
17
|
+
function describeRevision(added, removed) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
if (added.length === 1 && added[0]) parts.push(`Plan updated: added "${added[0].title}"`);
|
|
20
|
+
else if (added.length > 1) parts.push(`Plan updated: ${String(added.length)} steps added`);
|
|
21
|
+
if (removed === 1) parts.push("1 step removed");
|
|
22
|
+
else if (removed > 1) parts.push(`${String(removed)} steps removed`);
|
|
23
|
+
return parts.join(", ");
|
|
24
|
+
}
|
|
25
|
+
const PlanContext = createContext(null);
|
|
26
|
+
function usePlanContext(component) {
|
|
27
|
+
const context = useContext(PlanContext);
|
|
28
|
+
if (!context) throw new Error(`${component} must be rendered inside <AgentPlan>.`);
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
function AgentPlan({ className, steps, label, children, ...props }) {
|
|
32
|
+
const flat = useMemo(() => flatten(steps), [steps]);
|
|
33
|
+
const ids = flat.map((step) => step.id);
|
|
34
|
+
const [seen, setSeen] = useState(ids);
|
|
35
|
+
const [revision, setRevision] = useState("");
|
|
36
|
+
if (ids.join("\0") !== seen.join("\0")) {
|
|
37
|
+
const added = flat.filter((step) => !seen.includes(step.id));
|
|
38
|
+
const removed = seen.filter((id) => !ids.includes(id)).length;
|
|
39
|
+
setSeen(ids);
|
|
40
|
+
setRevision(added.length > 0 || removed > 0 ? describeRevision(added, removed) : "");
|
|
41
|
+
}
|
|
42
|
+
const currentIndex = flat.findIndex((step) => step.status === "running");
|
|
43
|
+
const context = useMemo(() => ({
|
|
44
|
+
steps,
|
|
45
|
+
flat,
|
|
46
|
+
currentIndex
|
|
47
|
+
}), [
|
|
48
|
+
steps,
|
|
49
|
+
flat,
|
|
50
|
+
currentIndex
|
|
51
|
+
]);
|
|
52
|
+
return /* @__PURE__ */ jsx(PlanContext.Provider, {
|
|
53
|
+
value: context,
|
|
54
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
55
|
+
"data-slot": "agent-plan",
|
|
56
|
+
"aria-label": label,
|
|
57
|
+
role: "group",
|
|
58
|
+
className: cn("flex flex-col gap-2", className),
|
|
59
|
+
...props,
|
|
60
|
+
children: [children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AgentPlanSummary, {}), /* @__PURE__ */ jsx(AgentPlanSteps, {})] }), /* @__PURE__ */ jsx("span", {
|
|
61
|
+
"aria-live": "polite",
|
|
62
|
+
"data-slot": "agent-plan-revision",
|
|
63
|
+
className: "sr-only",
|
|
64
|
+
children: revision
|
|
65
|
+
})]
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** "Step 3 of 6" plus the overall state, in words. */
|
|
70
|
+
function AgentPlanSummary({ className, ...props }) {
|
|
71
|
+
const { flat, currentIndex } = usePlanContext("AgentPlanSummary");
|
|
72
|
+
const done = flat.filter((step) => step.status === "done").length;
|
|
73
|
+
const failed = flat.filter((step) => step.status === "failed").length;
|
|
74
|
+
const running = currentIndex >= 0;
|
|
75
|
+
const message = !flat.some((step) => step.status !== "pending") ? `Proposed plan, ${String(flat.length)} steps` : running ? `Step ${String(currentIndex + 1)} of ${String(flat.length)}` : failed > 0 ? `${String(done)} of ${String(flat.length)} done, ${String(failed)} failed` : `${String(done)} of ${String(flat.length)} done`;
|
|
76
|
+
return /* @__PURE__ */ jsx("p", {
|
|
77
|
+
"data-slot": "agent-plan-summary",
|
|
78
|
+
className: cn("text-xs text-muted-foreground tabular-nums", className),
|
|
79
|
+
...props,
|
|
80
|
+
children: message
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function AgentPlanSteps({ className, ...props }) {
|
|
84
|
+
const { steps } = usePlanContext("AgentPlanSteps");
|
|
85
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
86
|
+
"data-slot": "agent-plan-steps",
|
|
87
|
+
className: cn("flex list-none flex-col gap-1", className),
|
|
88
|
+
...props,
|
|
89
|
+
children: steps.map((step) => /* @__PURE__ */ jsx(AgentPlanStep, { step }, step.id))
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function AgentPlanStep({ className, step, nested = false, ...props }) {
|
|
93
|
+
const { status, title, detail, error, steps: children } = step;
|
|
94
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
95
|
+
"data-slot": "agent-plan-step",
|
|
96
|
+
"data-status": status,
|
|
97
|
+
"aria-current": status === "running" ? "step" : void 0,
|
|
98
|
+
className: cn("flex flex-col gap-1", nested && "ml-5", className),
|
|
99
|
+
...props,
|
|
100
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
101
|
+
className: "flex items-start gap-2",
|
|
102
|
+
children: [/* @__PURE__ */ jsx(StepMarker, { status }), /* @__PURE__ */ jsxs("div", {
|
|
103
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsxs("span", {
|
|
106
|
+
className: cn("text-sm", status === "pending" && "text-muted-foreground", status === "skipped" && "text-muted-foreground line-through", status === "failed" && "text-destructive", status === "running" && "font-medium"),
|
|
107
|
+
children: [title, /* @__PURE__ */ jsxs("span", {
|
|
108
|
+
className: "sr-only",
|
|
109
|
+
children: [", ", STATUS_LABEL[status]]
|
|
110
|
+
})]
|
|
111
|
+
}),
|
|
112
|
+
detail ? /* @__PURE__ */ jsx("span", {
|
|
113
|
+
className: "text-xs text-muted-foreground",
|
|
114
|
+
children: detail
|
|
115
|
+
}) : null,
|
|
116
|
+
status === "failed" && error ? /* @__PURE__ */ jsx("span", {
|
|
117
|
+
"data-slot": "agent-plan-error",
|
|
118
|
+
className: "text-xs text-destructive",
|
|
119
|
+
children: error
|
|
120
|
+
}) : null
|
|
121
|
+
]
|
|
122
|
+
})]
|
|
123
|
+
}), children && children.length > 0 ? /* @__PURE__ */ jsx("ol", {
|
|
124
|
+
className: "flex list-none flex-col gap-1",
|
|
125
|
+
children: children.map((child) => /* @__PURE__ */ jsx(AgentPlanStep, {
|
|
126
|
+
step: child,
|
|
127
|
+
nested: true
|
|
128
|
+
}, child.id))
|
|
129
|
+
}) : null]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The status shape.
|
|
134
|
+
*
|
|
135
|
+
* aria-hidden throughout: every marker repeats what the step already states in
|
|
136
|
+
* text, and announcing both means hearing the status twice per step.
|
|
137
|
+
*/
|
|
138
|
+
function StepMarker({ status }) {
|
|
139
|
+
const base = "mt-0.5 grid size-4 shrink-0 place-items-center rounded-full border text-[9px]";
|
|
140
|
+
if (status === "running") return /* @__PURE__ */ jsx("span", {
|
|
141
|
+
"aria-hidden": "true",
|
|
142
|
+
"data-slot": "agent-plan-marker",
|
|
143
|
+
className: cn(base, "animate-pulse-soft border-primary bg-primary/15 text-primary"),
|
|
144
|
+
children: /* @__PURE__ */ jsx("span", { className: "size-1.5 rounded-full bg-primary" })
|
|
145
|
+
});
|
|
146
|
+
return /* @__PURE__ */ jsx("span", {
|
|
147
|
+
"aria-hidden": "true",
|
|
148
|
+
"data-slot": "agent-plan-marker",
|
|
149
|
+
className: cn(base, status === "done" && "border-success bg-success text-success-foreground", status === "failed" && "border-destructive bg-destructive text-destructive-foreground", status === "skipped" && "border-border-strong text-muted-foreground", status === "pending" && "border-border-strong"),
|
|
150
|
+
children: status === "done" ? "✓" : status === "failed" ? "✕" : status === "skipped" ? "–" : null
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
export { AgentPlan, AgentPlanStep, AgentPlanSteps, AgentPlanSummary };
|
|
155
|
+
|
|
156
|
+
//# sourceMappingURL=ai-agent-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-agent-plan.js","names":[],"sources":["../../../src/components/ai-agent-plan/ai-agent-plan.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n createContext,\n useContext,\n useMemo,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * What an agent intends to do, and how far through it is.\n *\n * Not a stepper. A wizard's steps are decided up front and never change; an\n * agent's plan is a hypothesis it revises as it learns — it adds a step when a\n * lookup fails, drops one that turned out to be unnecessary, and reorders when\n * a dependency appears. Every stepper treats its step list as fixed, so a plan\n * rendered in one either silently mutates under the reader or re-renders from\n * scratch and loses their place.\n *\n * So revision is the feature here, not an edge case. When the plan changes, the\n * change is announced — \"the model added a step\" is information, and watching a\n * list quietly grow is not the same as being told.\n *\n * Approval is not here. Deciding whether a plan may run is a different\n * component with a different shape; this one displays and reports.\n */\n\nexport type StepStatus = \"pending\" | \"running\" | \"done\" | \"failed\" | \"skipped\";\n\nexport interface PlanStep {\n id: string;\n title: string;\n status: StepStatus;\n /** What the step will do, or what it found. Shown under the title. */\n detail?: string;\n /** Why it failed. Shown only when the status is \"failed\". */\n error?: string;\n /** One level of sub-steps. Deeper nesting is a tree, which this is not. */\n steps?: PlanStep[];\n}\n\nconst STATUS_LABEL: Record<StepStatus, string> = {\n pending: \"Not started\",\n running: \"In progress\",\n done: \"Done\",\n failed: \"Failed\",\n skipped: \"Skipped\",\n};\n\n/** Flattens one level, so counting and \"step N of M\" include sub-steps. */\nfunction flatten(steps: PlanStep[]): PlanStep[] {\n return steps.flatMap((step) => [step, ...(step.steps ?? [])]);\n}\n\nfunction describeRevision(added: PlanStep[], removed: number): string {\n const parts: string[] = [];\n if (added.length === 1 && added[0]) parts.push(`Plan updated: added \"${added[0].title}\"`);\n else if (added.length > 1) parts.push(`Plan updated: ${String(added.length)} steps added`);\n if (removed === 1) parts.push(\"1 step removed\");\n else if (removed > 1) parts.push(`${String(removed)} steps removed`);\n return parts.join(\", \");\n}\n\ninterface PlanContextValue {\n steps: PlanStep[];\n flat: PlanStep[];\n currentIndex: number;\n}\n\nconst PlanContext = createContext<PlanContextValue | null>(null);\n\nfunction usePlanContext(component: string): PlanContextValue {\n const context = useContext(PlanContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <AgentPlan>.`);\n }\n return context;\n}\n\nexport interface AgentPlanProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n steps: PlanStep[];\n /** Names the plan, so the list is not an unlabelled sequence. */\n label: string;\n children?: ReactNode;\n}\n\nexport function AgentPlan({ className, steps, label, children, ...props }: AgentPlanProps) {\n const flat = useMemo(() => flatten(steps), [steps]);\n const ids = flat.map((step) => step.id);\n\n // Revision detection, adjusted during render rather than in an effect: an\n // effect would announce a frame after the change is already on screen, and a\n // ref read during render is unsafe under concurrent rendering.\n const [seen, setSeen] = useState<string[]>(ids);\n const [revision, setRevision] = useState(\"\");\n\n if (ids.join(\"\u0000\") !== seen.join(\"\u0000\")) {\n const added = flat.filter((step) => !seen.includes(step.id));\n const removed = seen.filter((id) => !ids.includes(id)).length;\n setSeen(ids);\n // A plan whose steps only changed status has not been revised. Only\n // structural change is worth interrupting for.\n setRevision(added.length > 0 || removed > 0 ? describeRevision(added, removed) : \"\");\n }\n\n const currentIndex = flat.findIndex((step) => step.status === \"running\");\n\n const context = useMemo<PlanContextValue>(\n () => ({ steps, flat, currentIndex }),\n [steps, flat, currentIndex],\n );\n\n return (\n <PlanContext.Provider value={context}>\n <div\n data-slot=\"agent-plan\"\n aria-label={label}\n role=\"group\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <AgentPlanSummary />\n <AgentPlanSteps />\n </>\n )}\n\n {/* Structural changes only. Announcing every status transition would\n talk over the reader continuously on a plan of any length. */}\n <span aria-live=\"polite\" data-slot=\"agent-plan-revision\" className=\"sr-only\">\n {revision}\n </span>\n </div>\n </PlanContext.Provider>\n );\n}\n\n/** \"Step 3 of 6\" plus the overall state, in words. */\nexport function AgentPlanSummary({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { flat, currentIndex } = usePlanContext(\"AgentPlanSummary\");\n\n const done = flat.filter((step) => step.status === \"done\").length;\n const failed = flat.filter((step) => step.status === \"failed\").length;\n const running = currentIndex >= 0;\n const started = flat.some((step) => step.status !== \"pending\");\n\n const message = !started\n ? `Proposed plan, ${String(flat.length)} steps`\n : running\n ? `Step ${String(currentIndex + 1)} of ${String(flat.length)}`\n : failed > 0\n ? `${String(done)} of ${String(flat.length)} done, ${String(failed)} failed`\n : `${String(done)} of ${String(flat.length)} done`;\n\n return (\n <p\n data-slot=\"agent-plan-summary\"\n className={cn(\"text-xs text-muted-foreground tabular-nums\", className)}\n {...props}\n >\n {message}\n </p>\n );\n}\n\nexport function AgentPlanSteps({ className, ...props }: ComponentPropsWithRef<\"ol\">) {\n const { steps } = usePlanContext(\"AgentPlanSteps\");\n\n return (\n // An ordered list: a plan is a sequence, and the order carries meaning that\n // an unordered list would throw away.\n <ol\n data-slot=\"agent-plan-steps\"\n className={cn(\"flex list-none flex-col gap-1\", className)}\n {...props}\n >\n {steps.map((step) => (\n <AgentPlanStep key={step.id} step={step} />\n ))}\n </ol>\n );\n}\n\nexport interface AgentPlanStepProps extends Omit<ComponentPropsWithRef<\"li\">, \"children\"> {\n step: PlanStep;\n /** Set on sub-steps. Not part of the public API surface. */\n nested?: boolean;\n}\n\nexport function AgentPlanStep({\n className,\n step,\n nested = false,\n ...props\n}: AgentPlanStepProps) {\n const { status, title, detail, error, steps: children } = step;\n\n return (\n <li\n data-slot=\"agent-plan-step\"\n data-status={status}\n // Marks where the reader is in the sequence without moving focus, which\n // would fight anyone reading ahead.\n aria-current={status === \"running\" ? \"step\" : undefined}\n className={cn(\"flex flex-col gap-1\", nested && \"ml-5\", className)}\n {...props}\n >\n <div className=\"flex items-start gap-2\">\n <StepMarker status={status} />\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <span\n className={cn(\n \"text-sm\",\n status === \"pending\" && \"text-muted-foreground\",\n status === \"skipped\" && \"text-muted-foreground line-through\",\n status === \"failed\" && \"text-destructive\",\n status === \"running\" && \"font-medium\",\n )}\n >\n {title}\n {/* Status in text, always. The marker is a shape and a colour, and\n neither is information on its own. */}\n <span className=\"sr-only\">, {STATUS_LABEL[status]}</span>\n </span>\n\n {detail ? <span className=\"text-xs text-muted-foreground\">{detail}</span> : null}\n {status === \"failed\" && error ? (\n <span data-slot=\"agent-plan-error\" className=\"text-xs text-destructive\">\n {error}\n </span>\n ) : null}\n </div>\n </div>\n\n {children && children.length > 0 ? (\n <ol className=\"flex list-none flex-col gap-1\">\n {children.map((child) => (\n <AgentPlanStep key={child.id} step={child} nested />\n ))}\n </ol>\n ) : null}\n </li>\n );\n}\n\n/**\n * The status shape.\n *\n * aria-hidden throughout: every marker repeats what the step already states in\n * text, and announcing both means hearing the status twice per step.\n */\nfunction StepMarker({ status }: { status: StepStatus }) {\n const base = \"mt-0.5 grid size-4 shrink-0 place-items-center rounded-full border text-[9px]\";\n\n if (status === \"running\") {\n return (\n // Deliberately not data-motion=\"indicator\". The bar for that exemption is\n // that stopping the animation would say something false, and it would\n // not: the step already states \"In progress\" in text and carries\n // aria-current. This pulse is reinforcement, so under reduced motion it\n // stops like any other decoration.\n <span\n aria-hidden=\"true\"\n data-slot=\"agent-plan-marker\"\n className={cn(base, \"animate-pulse-soft border-primary bg-primary/15 text-primary\")}\n >\n <span className=\"size-1.5 rounded-full bg-primary\" />\n </span>\n );\n }\n\n return (\n <span\n aria-hidden=\"true\"\n data-slot=\"agent-plan-marker\"\n className={cn(\n base,\n status === \"done\" && \"border-success bg-success text-success-foreground\",\n status === \"failed\" && \"border-destructive bg-destructive text-destructive-foreground\",\n status === \"skipped\" && \"border-border-strong text-muted-foreground\",\n status === \"pending\" && \"border-border-strong\",\n )}\n >\n {status === \"done\" ? \"✓\" : status === \"failed\" ? \"✕\" : status === \"skipped\" ? \"–\" : null}\n </span>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { AgentPlan, AgentPlanProps, AgentPlanStep, AgentPlanStepProps, AgentPlanSteps, AgentPlanSummary, PlanStep, StepStatus } from "./ai-agent-plan.js";
|
|
2
|
+
export { AgentPlan, type AgentPlanProps, AgentPlanStep, type AgentPlanStepProps, AgentPlanSteps, AgentPlanSummary, type PlanStep, type StepStatus };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/ai-agent-plan/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "ai-agent-plan",
|
|
5
|
+
title: "AI Agent Plan",
|
|
6
|
+
description: "What an agent intends to do — including when it changes its mind mid-run.",
|
|
7
|
+
category: "ai",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["ai-agent-plan.tsx"],
|
|
12
|
+
a11y: "An ordered list, because a plan is a sequence and the order carries meaning an unordered list would discard. The running step carries aria-current=\"step\", which marks the reader's place without moving focus and fighting anyone reading ahead. Every status is stated in text as well as drawn, and the markers are aria-hidden so the status is not heard twice per step. The live region reports structural revisions only — announcing every status transition would talk over the reader continuously on a plan of any length."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/ai-agent-plan/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"ai-agent-plan\",\n title: \"AI Agent Plan\",\n description: \"What an agent intends to do — including when it changes its mind mid-run.\",\n category: \"ai\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"ai-agent-plan.tsx\"],\n a11y:\n \"An ordered list, because a plan is a sequence and the order carries meaning an unordered \" +\n 'list would discard. The running step carries aria-current=\"step\", which marks the reader\\'s ' +\n \"place without moving focus and fighting anyone reading ahead. Every status is stated in text \" +\n \"as well as drawn, and the markers are aria-hidden so the status is not heard twice per step. \" +\n \"The live region reports structural revisions only — announcing every status transition would \" +\n \"talk over the reader continuously on a plan of any length.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,mBAAmB;CAC3B,MACE;AAMJ,CAAC"}
|
|
@@ -10,7 +10,7 @@ import { ComponentPropsWithRef } from "react";
|
|
|
10
10
|
*/
|
|
11
11
|
type AgentState = "idle" | "thinking" | "working" | "waiting" | "done" | "error";
|
|
12
12
|
declare const agentStatusVariants: (props?: ({
|
|
13
|
-
state?: "done" | "
|
|
13
|
+
state?: "done" | "error" | "idle" | "thinking" | "working" | "waiting" | null | undefined;
|
|
14
14
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
15
15
|
interface AgentStatusProps extends ComponentPropsWithRef<"span">, VariantProps<typeof agentStatusVariants> {
|
|
16
16
|
state?: AgentState;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/ai-approval-request/ai-approval-request.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The gate between an agent deciding to act and it acting.
|
|
5
|
+
*
|
|
6
|
+
* Two things separate this from the confirmations that already exist.
|
|
7
|
+
*
|
|
8
|
+
* The first is that the proposed arguments are editable. Every implementation
|
|
9
|
+
* surveyed returns a boolean over a read-only payload, which forces a false
|
|
10
|
+
* choice: approve a call whose recipient is wrong, or deny it and make the
|
|
11
|
+
* model try again. "Approve this, but fix the address first" is the answer
|
|
12
|
+
* people actually want, and it is only possible if the arguments are fields.
|
|
13
|
+
* Edited values come back in the decision, and are marked as changed so an
|
|
14
|
+
* audit trail can tell the model's proposal from the human's correction.
|
|
15
|
+
*
|
|
16
|
+
* The second is that it renders while the arguments are still arriving. The
|
|
17
|
+
* common implementation returns null until the tool input is complete, which
|
|
18
|
+
* means nothing is on screen at the moment approval becomes relevant and the
|
|
19
|
+
* request appears abruptly, fully formed. Here the request is visible as it
|
|
20
|
+
* forms, with the decision held until there is something whole to decide on.
|
|
21
|
+
*/
|
|
22
|
+
type ApprovalScope = "once" | "always";
|
|
23
|
+
type ApprovalDecision = {
|
|
24
|
+
approved: true;
|
|
25
|
+
scope: ApprovalScope;
|
|
26
|
+
arguments: Record<string, string>;
|
|
27
|
+
edited: string[];
|
|
28
|
+
} | {
|
|
29
|
+
approved: false;
|
|
30
|
+
reason?: string;
|
|
31
|
+
};
|
|
32
|
+
interface ApprovalField {
|
|
33
|
+
name: string;
|
|
34
|
+
label: string;
|
|
35
|
+
/** A textarea instead of a single line. For message bodies and prompts. */
|
|
36
|
+
multiline?: boolean;
|
|
37
|
+
/** Locked arguments are shown but not editable — an id, a resolved amount. */
|
|
38
|
+
readOnly?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface ApprovalRequestProps extends Omit<ComponentPropsWithRef<"section">, "onSubmit" | "children"> {
|
|
41
|
+
/** The tool the model wants to call, as it named it. */
|
|
42
|
+
tool: string;
|
|
43
|
+
/** What the call will do, in the reader's language rather than the API's. */
|
|
44
|
+
summary: string;
|
|
45
|
+
/** Proposed arguments. Missing keys are treated as still arriving. */
|
|
46
|
+
arguments: Record<string, string>;
|
|
47
|
+
/** Which arguments to show, in order, and which may be changed. */
|
|
48
|
+
fields: ApprovalField[];
|
|
49
|
+
onDecision: (decision: ApprovalDecision) => void;
|
|
50
|
+
/**
|
|
51
|
+
* True while the model is still producing the arguments. The request renders
|
|
52
|
+
* anyway; only the decision waits.
|
|
53
|
+
*/
|
|
54
|
+
streaming?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Said plainly above the controls when the call cannot be taken back. Not a
|
|
57
|
+
* severity colour — the sentence is the warning.
|
|
58
|
+
*/
|
|
59
|
+
irreversible?: string;
|
|
60
|
+
/** Set once decided, to render the outcome instead of the controls. */
|
|
61
|
+
decision?: ApprovalDecision;
|
|
62
|
+
children?: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
declare function ApprovalRequest({ className, tool, summary, arguments: proposed, fields, onDecision, streaming, irreversible, decision, children, ...props }: ApprovalRequestProps): import("react").JSX.Element;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { ApprovalDecision, ApprovalField, ApprovalRequest, ApprovalRequestProps, ApprovalScope };
|
|
67
|
+
//# sourceMappingURL=ai-approval-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-approval-request.d.ts","names":[],"sources":["../../../src/components/ai-approval-request/ai-approval-request.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;KA2BY;KAEA;EAEN;EACA,OAAO;EACP,WAAW;EACX;;EAEA;EAAiB;;UAEN;EACf;EACA;;EAEA;;EAEA;;UAGe,6BAA6B,KAC5C;;EAIA;;EAEA;;EAEA,WAAW;;EAEX,QAAQ;EACR,aAAa,UAAU;;;;;EAKvB;;;;;EAKA;;EAEA,WAAW;EACX,WAAW;;iBAGG,kBACd,WACA,MACA,SACA,WAAW,UACX,QACA,YACA,WACA,cACA,UACA,aACG,SACF,uCAAoB,IAAA"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useId, useMemo, useState } from "react";
|
|
6
|
+
//#region src/components/ai-approval-request/ai-approval-request.tsx
|
|
7
|
+
function ApprovalRequest({ className, tool, summary, arguments: proposed, fields, onDecision, streaming = false, irreversible, decision, children, ...props }) {
|
|
8
|
+
const headingId = useId();
|
|
9
|
+
const [edits, setEdits] = useState({});
|
|
10
|
+
const [denying, setDenying] = useState(false);
|
|
11
|
+
const [reason, setReason] = useState("");
|
|
12
|
+
const values = useMemo(() => ({
|
|
13
|
+
...proposed,
|
|
14
|
+
...edits
|
|
15
|
+
}), [proposed, edits]);
|
|
16
|
+
const edited = useMemo(() => Object.keys(edits).filter((name) => edits[name] !== proposed[name]), [edits, proposed]);
|
|
17
|
+
const missing = fields.filter((field) => proposed[field.name] === void 0);
|
|
18
|
+
const settled = !streaming && missing.length === 0;
|
|
19
|
+
if (decision) return /* @__PURE__ */ jsxs("section", {
|
|
20
|
+
"data-slot": "approval-request",
|
|
21
|
+
"data-state": decision.approved ? "approved" : "denied",
|
|
22
|
+
"aria-labelledby": headingId,
|
|
23
|
+
className: cn("rounded-lg border p-4 text-sm", decision.approved ? "border-border bg-muted/40" : "border-border bg-muted/40", className),
|
|
24
|
+
...props,
|
|
25
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
26
|
+
id: headingId,
|
|
27
|
+
className: "text-sm font-medium",
|
|
28
|
+
children: summary
|
|
29
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
30
|
+
"data-slot": "approval-outcome",
|
|
31
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
32
|
+
children: decision.approved ? `Approved ${decision.scope === "always" ? "for every call to this tool" : "once"}` + (decision.edited.length > 0 ? `, with ${String(decision.edited.length)} argument${decision.edited.length === 1 ? "" : "s"} corrected` : "") : `Denied${decision.reason ? `: ${decision.reason}` : ""}`
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
36
|
+
"data-slot": "approval-request",
|
|
37
|
+
"data-state": settled ? "pending" : "forming",
|
|
38
|
+
"aria-labelledby": headingId,
|
|
39
|
+
"aria-busy": !settled,
|
|
40
|
+
className: cn("rounded-lg border border-border bg-card p-4", className),
|
|
41
|
+
...props,
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsxs("div", {
|
|
44
|
+
className: "flex flex-wrap items-baseline justify-between gap-2",
|
|
45
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
46
|
+
id: headingId,
|
|
47
|
+
className: "text-sm font-medium",
|
|
48
|
+
children: summary
|
|
49
|
+
}), /* @__PURE__ */ jsx("code", {
|
|
50
|
+
className: "rounded bg-muted px-1.5 py-0.5 font-mono text-2xs text-muted-foreground",
|
|
51
|
+
children: tool
|
|
52
|
+
})]
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ jsx("dl", {
|
|
55
|
+
className: "mt-3 flex flex-col gap-2.5",
|
|
56
|
+
children: fields.map((field) => {
|
|
57
|
+
const arrived = proposed[field.name] !== void 0;
|
|
58
|
+
const changed = edited.includes(field.name);
|
|
59
|
+
return /* @__PURE__ */ jsx(ApprovalArgument, {
|
|
60
|
+
field,
|
|
61
|
+
value: values[field.name] ?? "",
|
|
62
|
+
arrived,
|
|
63
|
+
changed,
|
|
64
|
+
onChange: (next) => {
|
|
65
|
+
setEdits((current) => ({
|
|
66
|
+
...current,
|
|
67
|
+
[field.name]: next
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
}, field.name);
|
|
71
|
+
})
|
|
72
|
+
}),
|
|
73
|
+
children,
|
|
74
|
+
irreversible ? /* @__PURE__ */ jsx("p", {
|
|
75
|
+
"data-slot": "approval-warning",
|
|
76
|
+
className: "mt-3 text-xs text-destructive",
|
|
77
|
+
children: irreversible
|
|
78
|
+
}) : null,
|
|
79
|
+
denying ? /* @__PURE__ */ jsxs("div", {
|
|
80
|
+
className: "mt-3 flex flex-col gap-2",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx("label", {
|
|
83
|
+
htmlFor: `${headingId}-reason`,
|
|
84
|
+
className: "text-xs text-muted-foreground",
|
|
85
|
+
children: "Why not? The model sees this."
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
88
|
+
id: `${headingId}-reason`,
|
|
89
|
+
rows: 2,
|
|
90
|
+
value: reason,
|
|
91
|
+
onChange: (event) => {
|
|
92
|
+
setReason(event.target.value);
|
|
93
|
+
},
|
|
94
|
+
className: cn("w-full resize-none rounded-md border border-input bg-background px-2 py-1.5 text-sm", focusRing)
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsxs("div", {
|
|
97
|
+
className: "flex flex-wrap gap-2",
|
|
98
|
+
children: [/* @__PURE__ */ jsx(Action, {
|
|
99
|
+
variant: "destructive",
|
|
100
|
+
onClick: () => {
|
|
101
|
+
onDecision({
|
|
102
|
+
approved: false,
|
|
103
|
+
reason: reason.trim() || void 0
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
children: "Deny"
|
|
107
|
+
}), /* @__PURE__ */ jsx(Action, {
|
|
108
|
+
onClick: () => {
|
|
109
|
+
setDenying(false);
|
|
110
|
+
},
|
|
111
|
+
children: "Back"
|
|
112
|
+
})]
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
116
|
+
className: "mt-4 flex flex-wrap items-center gap-2",
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ jsx(Action, {
|
|
119
|
+
variant: "primary",
|
|
120
|
+
disabled: !settled,
|
|
121
|
+
onClick: () => {
|
|
122
|
+
onDecision({
|
|
123
|
+
approved: true,
|
|
124
|
+
scope: "once",
|
|
125
|
+
arguments: values,
|
|
126
|
+
edited
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
children: edited.length > 0 ? "Approve with changes" : "Approve once"
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ jsxs(Action, {
|
|
132
|
+
disabled: !settled,
|
|
133
|
+
onClick: () => {
|
|
134
|
+
onDecision({
|
|
135
|
+
approved: true,
|
|
136
|
+
scope: "always",
|
|
137
|
+
arguments: values,
|
|
138
|
+
edited
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
children: ["Always allow ", tool]
|
|
142
|
+
}),
|
|
143
|
+
/* @__PURE__ */ jsx(Action, {
|
|
144
|
+
onClick: () => {
|
|
145
|
+
setDenying(true);
|
|
146
|
+
},
|
|
147
|
+
children: "Deny…"
|
|
148
|
+
}),
|
|
149
|
+
!settled ? /* @__PURE__ */ jsx("span", {
|
|
150
|
+
"aria-live": "polite",
|
|
151
|
+
className: "text-xs text-muted-foreground",
|
|
152
|
+
children: "Waiting for the model to finish the request"
|
|
153
|
+
}) : null
|
|
154
|
+
]
|
|
155
|
+
})
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function ApprovalArgument({ field, value, arrived, changed, onChange }) {
|
|
160
|
+
const id = useId();
|
|
161
|
+
const changedId = useId();
|
|
162
|
+
const editable = !field.readOnly && arrived;
|
|
163
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
164
|
+
"data-slot": "approval-argument",
|
|
165
|
+
"data-changed": changed || void 0,
|
|
166
|
+
className: "flex flex-col gap-1",
|
|
167
|
+
children: [/* @__PURE__ */ jsxs("dt", {
|
|
168
|
+
className: "flex items-baseline gap-1",
|
|
169
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
170
|
+
htmlFor: editable ? id : void 0,
|
|
171
|
+
className: "text-xs text-muted-foreground",
|
|
172
|
+
children: field.label
|
|
173
|
+
}), changed ? /* @__PURE__ */ jsx("span", {
|
|
174
|
+
id: changedId,
|
|
175
|
+
className: "text-xs text-warning",
|
|
176
|
+
children: "· changed from the model’s proposal"
|
|
177
|
+
}) : null]
|
|
178
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
179
|
+
className: "m-0",
|
|
180
|
+
children: !arrived ? /* @__PURE__ */ jsx("span", {
|
|
181
|
+
"aria-hidden": "true",
|
|
182
|
+
className: "block h-8 w-full animate-pulse-soft rounded-md bg-muted"
|
|
183
|
+
}) : editable ? field.multiline ? /* @__PURE__ */ jsx("textarea", {
|
|
184
|
+
id,
|
|
185
|
+
"aria-describedby": changed ? changedId : void 0,
|
|
186
|
+
rows: 3,
|
|
187
|
+
value,
|
|
188
|
+
onChange: (event) => {
|
|
189
|
+
onChange(event.target.value);
|
|
190
|
+
},
|
|
191
|
+
className: cn("w-full resize-none rounded-md border border-input bg-background px-2 py-1.5 font-mono text-xs", changed && "border-warning", focusRing)
|
|
192
|
+
}) : /* @__PURE__ */ jsx("input", {
|
|
193
|
+
id,
|
|
194
|
+
type: "text",
|
|
195
|
+
"aria-describedby": changed ? changedId : void 0,
|
|
196
|
+
value,
|
|
197
|
+
onChange: (event) => {
|
|
198
|
+
onChange(event.target.value);
|
|
199
|
+
},
|
|
200
|
+
className: cn("w-full rounded-md border border-input bg-background px-2 py-1.5 font-mono text-xs", changed && "border-warning", focusRing)
|
|
201
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
202
|
+
className: "rounded-md bg-muted px-2 py-1.5 font-mono text-xs break-words",
|
|
203
|
+
children: value
|
|
204
|
+
})
|
|
205
|
+
})]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function Action({ className, variant = "default", ...props }) {
|
|
209
|
+
return /* @__PURE__ */ jsx("button", {
|
|
210
|
+
type: "button",
|
|
211
|
+
className: cn("rounded-md border px-2.5 py-1 text-xs font-medium transition-colors", variant === "primary" && "border-primary bg-primary text-primary-foreground hover:bg-primary-hover", variant === "destructive" && "border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/90", variant === "default" && "border-input bg-background hover:bg-accent hover:text-accent-foreground", focusRing, disabledStyles, className),
|
|
212
|
+
...props
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { ApprovalRequest };
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=ai-approval-request.js.map
|