@qilin-agent/client-ui-approval 0.0.0-stage → 3.1.3
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/LICENSE +21 -0
- package/README.i18n.yaml +28 -0
- package/README.md +44 -2
- package/README.zh.md +45 -0
- package/lib/client.js +353 -0
- package/lib/index.js +6 -0
- package/lib/types/client/ApprovalPanel.d.ts +8 -0
- package/lib/types/client/contract/slots.d.ts +105 -0
- package/lib/types/client/index.d.ts +12 -0
- package/lib/types/client/locales.d.ts +20 -0
- package/lib/types/index.d.ts +4 -0
- package/package.json +72 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/ui-approval/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: a16f3a25592b9a2d
|
|
7
|
+
zh: 6be5e99282ce23d9
|
|
8
|
+
/qilin-client-ui-approval:
|
|
9
|
+
en: 0ac6bc9269959e2f
|
|
10
|
+
zh: 556b595de66d9f16
|
|
11
|
+
/qilin-client-ui-approval/summary:
|
|
12
|
+
en: 8d6c7c6e63f849f6
|
|
13
|
+
zh: d89fa273e30b99d3
|
|
14
|
+
/qilin-client-ui-approval/table-of-contents:
|
|
15
|
+
en: b4683d2a53256fda
|
|
16
|
+
zh: 4e65ff63dc8c48fe
|
|
17
|
+
/qilin-client-ui-approval/model-experience:
|
|
18
|
+
en: 8161dce49e9f899c
|
|
19
|
+
zh: a17401e1e78ba6be
|
|
20
|
+
/qilin-client-ui-approval/model-experience/kv-cache-effect:
|
|
21
|
+
en: adf7112e0691b89e
|
|
22
|
+
zh: 14eeddcff985119f
|
|
23
|
+
/qilin-client-ui-approval/known-limitations-and-deferred-work:
|
|
24
|
+
en: 158f435625e41ca9
|
|
25
|
+
zh: 9d58169aaf6c7ed7
|
|
26
|
+
/qilin-client-ui-approval/known-limitations-and-deferred-work/dev-note:
|
|
27
|
+
en: e67f633dc6fe7773
|
|
28
|
+
zh: e88aeb617ffb74ea
|
package/README.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Browser approval UI that answers Host permission requests through the scoped interaction path."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# @qilin-agent/client-ui-approval
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
English | [中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Browser approval presentation over the Agent-scoped Remote Event waterfall. The plugin publishes each pending request through `ctx.uiSession`, takes over the Conversation composer, optionally renders correlated Tool detail, and returns the user's decision to the waiting Host request. Use it when a browser must collect approval for a waiting Host operation.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Model Experience](#model-experience)
|
|
16
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
17
|
+
- [Dev Note](#dev-note)
|
|
18
|
+
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
<a id="model-experience"></a>
|
|
22
|
+
## Model Experience
|
|
23
|
+
|
|
24
|
+
None, as this package presents approval requests in the browser and registers nothing model-facing.
|
|
25
|
+
|
|
26
|
+
#### KV Cache effect
|
|
27
|
+
|
|
28
|
+
None; approval request and response rendering does not alter a model request.
|
|
29
|
+
|
|
30
|
+
## Known Limitations and Deferred Work
|
|
31
|
+
|
|
32
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
33
|
+
|
|
34
|
+
- **The panel exposes transient decisions only** — it supports allow-once and reject; persistent permission policy remains owned by Host-side approval packages.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
<a id="dev-note"></a>
|
|
38
|
+
### Dev Note
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
42
|
+
|
|
43
|
+
None.
|
|
44
|
+
|
|
45
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "通过作用域交互路径响应 Host 权限请求的浏览器批准界面。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# @qilin-agent/client-ui-approval
|
|
6
|
+
|
|
7
|
+
[English](README.md) | 中文
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
基于 Agent-scoped Remote Event waterfall 的浏览器审批界面。插件通过 `ctx.uiSession` 发布每个待处理请求、接管 Conversation composer、按需渲染关联的 Tool 详情,并将用户决定返回给等待中的 Host 请求。当浏览器必须为等待中的 Host 操作收集批准时,请使用它。
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [模型体验](#model-experience)
|
|
16
|
+
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
|
|
17
|
+
- [开发备注](#dev-note)
|
|
18
|
+
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
<a id="model-experience"></a>
|
|
22
|
+
## 模型体验
|
|
23
|
+
|
|
24
|
+
无,因为本包只在浏览器中呈现审批请求,不注册任何面向模型的内容。
|
|
25
|
+
|
|
26
|
+
#### KV Cache 影响
|
|
27
|
+
|
|
28
|
+
无;审批请求和响应的呈现不会改变模型请求。
|
|
29
|
+
|
|
30
|
+
## 已知限制与暂缓事项
|
|
31
|
+
|
|
32
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
33
|
+
|
|
34
|
+
- **面板只提供临时决定**——它支持仅本次允许和拒绝;持久权限策略仍由 Host 侧审批包拥有。
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
<a id="dev-note"></a>
|
|
38
|
+
### 开发备注
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
42
|
+
|
|
43
|
+
无。
|
|
44
|
+
|
|
45
|
+
</details>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-ui-approval",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
|
|
10
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-approval/src/client/ApprovalPanel.module.css.mjs
|
|
11
|
+
const css = ".Dg9Iaq_root{padding:8px calc(var(--qilin-composer-side-clearance) + 16px) 12px;flex-direction:column;align-items:center;display:flex}.Dg9Iaq_card{width:100%;max-width:var(--qilin-chat-content-width);border:1px solid var(--qilin-alias-state-warn-secondary);background:var(--qilin-specific-input-major);box-shadow:var(--qilin-shadow-lv2);--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);border-radius:20px;overflow:hidden}.Dg9Iaq_strip{background:var(--qilin-alias-state-warn-tertiary);color:var(--qilin-alias-state-warn-primary);align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.Dg9Iaq_dot{corner-shape:round;background:var(--qilin-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.Dg9Iaq_body{box-sizing:border-box;max-height:var(--qilin-composer-text-max-height);flex-direction:column;gap:6px;padding:12px 16px 0;display:flex;overflow-y:auto}.Dg9Iaq_headline{color:var(--qilin-alias-label-primary);font-size:15px;font-weight:500;line-height:24px}.Dg9Iaq_command{color:var(--qilin-alias-label-tertiary);font-family:var(--ds-font-family-code);word-break:break-all;font-size:13px;line-height:20px}.Dg9Iaq_actionRow{justify-content:flex-end;gap:8px;padding:14px 16px;display:flex}.Dg9Iaq_reject:hover:not(:disabled){background:var(--qilin-alias-interactive-bg-hover-danger);color:var(--qilin-alias-state-error-primary);border-color:#0000}";
|
|
12
|
+
const tagId = "@qilin-agent/client-ui-approval/ApprovalPanel.module.css";
|
|
13
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
14
|
+
const tag = document.createElement("style");
|
|
15
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-approval";
|
|
16
|
+
tag.dataset.pluginCss = tagId;
|
|
17
|
+
tag.textContent = css;
|
|
18
|
+
document.head.appendChild(tag);
|
|
19
|
+
}
|
|
20
|
+
var ApprovalPanel_module_css_default = {
|
|
21
|
+
"actionRow": "Dg9Iaq_actionRow",
|
|
22
|
+
"body": "Dg9Iaq_body",
|
|
23
|
+
"card": "Dg9Iaq_card",
|
|
24
|
+
"command": "Dg9Iaq_command",
|
|
25
|
+
"dot": "Dg9Iaq_dot",
|
|
26
|
+
"headline": "Dg9Iaq_headline",
|
|
27
|
+
"reject": "Dg9Iaq_reject",
|
|
28
|
+
"root": "Dg9Iaq_root",
|
|
29
|
+
"strip": "Dg9Iaq_strip"
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region lib/types/client/ApprovalPanel.js
|
|
33
|
+
/** Composer takeover for one pending approval waterfall. */
|
|
34
|
+
/**
|
|
35
|
+
* Render one pending approval and its optional Tool-owned detail.
|
|
36
|
+
* @param props - selector-matched request and standard Slot props.
|
|
37
|
+
* @returns The approval composer takeover.
|
|
38
|
+
*/
|
|
39
|
+
function ApprovalPanel(props) {
|
|
40
|
+
const approval = props.matched;
|
|
41
|
+
const detail = approval.callId === void 0 ? null : props.renderSlot("conversation.approval.detail", { callId: approval.callId });
|
|
42
|
+
return (0, react_jsx_runtime.jsx)(ApprovalFlow, {
|
|
43
|
+
pending: approval,
|
|
44
|
+
reason: approval.displayReason === void 0 ? approval.reason : props.resolveReason(approval.displayReason),
|
|
45
|
+
detail,
|
|
46
|
+
t: props.t
|
|
47
|
+
}, approval.key);
|
|
48
|
+
}
|
|
49
|
+
function ApprovalFlow({ pending, reason, detail, t }) {
|
|
50
|
+
const [answered, setAnswered] = (0, react.useState)(false);
|
|
51
|
+
const active = (0, react.useRef)(false);
|
|
52
|
+
const waiting = (0, react.useRef)(false);
|
|
53
|
+
const composing = (0, react.useRef)(false);
|
|
54
|
+
const compositionEnded = (0, react.useRef)(false);
|
|
55
|
+
(0, react.useEffect)(() => {
|
|
56
|
+
active.current = true;
|
|
57
|
+
return () => {
|
|
58
|
+
active.current = false;
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
const answer = (outcome) => {
|
|
62
|
+
if (waiting.current) return;
|
|
63
|
+
waiting.current = true;
|
|
64
|
+
setAnswered(true);
|
|
65
|
+
pending.answer(outcome).catch(() => {
|
|
66
|
+
if (!active.current) return;
|
|
67
|
+
waiting.current = false;
|
|
68
|
+
setAnswered(false);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const keydown = (event) => {
|
|
72
|
+
const element = event.target;
|
|
73
|
+
if (event.defaultPrevented || !event.currentTarget.contains(document.activeElement) || element.closest("input, textarea, select, [contenteditable=\"true\"], [contenteditable=\"\"]") !== null) return;
|
|
74
|
+
if (event.key !== "Enter" && event.key !== "Escape") return;
|
|
75
|
+
if (event.key === "Enter" && element.closest("button, a[href], [role=\"button\"]") !== null) return;
|
|
76
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
if (event.repeat || composing.current || compositionEnded.current || event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
80
|
+
answer(event.key === "Enter" ? "allowed-once" : "rejected");
|
|
81
|
+
};
|
|
82
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
83
|
+
className: ApprovalPanel_module_css_default.root,
|
|
84
|
+
"data-approval-key": pending.key,
|
|
85
|
+
"aria-busy": answered,
|
|
86
|
+
onKeyDown: keydown,
|
|
87
|
+
onKeyUpCapture: () => {
|
|
88
|
+
compositionEnded.current = false;
|
|
89
|
+
},
|
|
90
|
+
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
91
|
+
className: ApprovalPanel_module_css_default.card,
|
|
92
|
+
children: [
|
|
93
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
94
|
+
className: ApprovalPanel_module_css_default.strip,
|
|
95
|
+
children: [(0, react_jsx_runtime.jsx)("span", { className: ApprovalPanel_module_css_default.dot }), t("waiting")]
|
|
96
|
+
}),
|
|
97
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
98
|
+
className: ApprovalPanel_module_css_default.body,
|
|
99
|
+
"data-approval-scroll": "",
|
|
100
|
+
tabIndex: 0,
|
|
101
|
+
role: "group",
|
|
102
|
+
"aria-label": t("detail.aria"),
|
|
103
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
104
|
+
className: ApprovalPanel_module_css_default.headline,
|
|
105
|
+
children: reason ?? t("escalation", { toolName: pending.toolName })
|
|
106
|
+
}), detail !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
107
|
+
className: ApprovalPanel_module_css_default.command,
|
|
108
|
+
children: detail
|
|
109
|
+
})]
|
|
110
|
+
}),
|
|
111
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
112
|
+
className: ApprovalPanel_module_css_default.actionRow,
|
|
113
|
+
children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
114
|
+
variant: "outline",
|
|
115
|
+
className: ApprovalPanel_module_css_default.reject,
|
|
116
|
+
disabled: answered,
|
|
117
|
+
onClick: () => {
|
|
118
|
+
answer("rejected");
|
|
119
|
+
},
|
|
120
|
+
children: t("reject")
|
|
121
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
122
|
+
variant: "primary",
|
|
123
|
+
disabled: answered,
|
|
124
|
+
onClick: () => {
|
|
125
|
+
answer("allowed-once");
|
|
126
|
+
},
|
|
127
|
+
children: t("allowOnce")
|
|
128
|
+
})]
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region lib/types/client/contract/slots.js
|
|
136
|
+
function settlePendingComposer(settle, failureMessage) {
|
|
137
|
+
try {
|
|
138
|
+
settle();
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return Promise.reject(error instanceof Error ? error : new Error(failureMessage, { cause: error }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let nextApprovalKey = 0;
|
|
145
|
+
/** One answerable Client presentation of a pending Host waterfall. */
|
|
146
|
+
var PendingApproval = class {
|
|
147
|
+
sessionId;
|
|
148
|
+
/** Domain discriminator used by Session pending-interaction consumers. */
|
|
149
|
+
kind;
|
|
150
|
+
/** Opaque render identity and one-shot remount axis. */
|
|
151
|
+
key;
|
|
152
|
+
/** Tool requesting the decision. */
|
|
153
|
+
toolName;
|
|
154
|
+
/** Correlated Tool call, when supplied by the asker. */
|
|
155
|
+
callId;
|
|
156
|
+
/** Human-readable reason supplied by the asker. */
|
|
157
|
+
reason;
|
|
158
|
+
/** Localized presentation copy, when supplied by the asker. */
|
|
159
|
+
displayReason;
|
|
160
|
+
/** Result returned by the Remote Event listener to the Host waterfall. */
|
|
161
|
+
result;
|
|
162
|
+
#resolve;
|
|
163
|
+
#reject;
|
|
164
|
+
#signal;
|
|
165
|
+
#onAbort;
|
|
166
|
+
#delegated = Symbol("pending approval delegated");
|
|
167
|
+
#settled = false;
|
|
168
|
+
/**
|
|
169
|
+
* @param sessionId - Agent/Session identity owning the scoped request.
|
|
170
|
+
* @param request - Host approval request projected through the Remote Event.
|
|
171
|
+
*/
|
|
172
|
+
constructor(sessionId, request) {
|
|
173
|
+
this.sessionId = sessionId;
|
|
174
|
+
this.kind = "approval";
|
|
175
|
+
nextApprovalKey += 1;
|
|
176
|
+
this.key = `approval:${String(nextApprovalKey)}`;
|
|
177
|
+
this.toolName = request.toolName;
|
|
178
|
+
this.callId = request.callId;
|
|
179
|
+
this.reason = request.reason;
|
|
180
|
+
this.displayReason = request.displayReason;
|
|
181
|
+
const completion = Promise.withResolvers();
|
|
182
|
+
this.result = completion.promise;
|
|
183
|
+
this.#resolve = completion.resolve;
|
|
184
|
+
this.#reject = completion.reject;
|
|
185
|
+
this.#signal = request.signal;
|
|
186
|
+
if (request.signal === void 0) {
|
|
187
|
+
this.#onAbort = void 0;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const onAbort = () => {
|
|
191
|
+
this.abort(request.signal?.reason ?? /* @__PURE__ */ new Error("approval request was aborted"));
|
|
192
|
+
};
|
|
193
|
+
this.#onAbort = onAbort;
|
|
194
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
195
|
+
if (request.signal.aborted) onAbort();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the Host waterfall with the user's decision.
|
|
199
|
+
* @param outcome - supported interactive decision.
|
|
200
|
+
*/
|
|
201
|
+
answer(outcome) {
|
|
202
|
+
return settlePendingComposer(() => {
|
|
203
|
+
this.finish(() => {
|
|
204
|
+
this.#resolve(outcome);
|
|
205
|
+
});
|
|
206
|
+
}, "pending approval settlement failed");
|
|
207
|
+
}
|
|
208
|
+
/** Delegate an unanswered request to the next waterfall listener. */
|
|
209
|
+
delegate() {
|
|
210
|
+
if (this.#settled) return;
|
|
211
|
+
this.finish(() => {
|
|
212
|
+
this.#reject(this.#delegated);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Test whether a rejection requests waterfall delegation.
|
|
217
|
+
* @param reason - rejection received from {@link PendingApproval.result}.
|
|
218
|
+
* @returns whether {@link PendingApproval.delegate} produced it.
|
|
219
|
+
*/
|
|
220
|
+
isDelegation(reason) {
|
|
221
|
+
return reason === this.#delegated;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* End an unanswered presentation when its transport, scope, or plugin lifetime ends.
|
|
225
|
+
* @param reason - rejection exposed to the waiting Remote Event listener.
|
|
226
|
+
*/
|
|
227
|
+
abort(reason) {
|
|
228
|
+
if (this.#settled) return;
|
|
229
|
+
this.finish(() => {
|
|
230
|
+
this.#reject(reason);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
finish(settle) {
|
|
234
|
+
if (this.#settled) throw new Error(`pending approval ${this.key} is already settled`);
|
|
235
|
+
this.#settled = true;
|
|
236
|
+
if (this.#signal !== void 0 && this.#onAbort !== void 0) this.#signal.removeEventListener("abort", this.#onAbort);
|
|
237
|
+
settle();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region lib/types/client/locales.js
|
|
242
|
+
/** `approval` namespace dictionaries. */
|
|
243
|
+
/** Simplified Chinese dictionary and key-set source of truth. */
|
|
244
|
+
const zh = {
|
|
245
|
+
waiting: "等待审批",
|
|
246
|
+
"detail.aria": "审批详情",
|
|
247
|
+
escalation: "工具 {toolName} 请求越权执行",
|
|
248
|
+
reject: "拒绝",
|
|
249
|
+
allowOnce: "允许一次"
|
|
250
|
+
};
|
|
251
|
+
/** English dictionary, checked against the Chinese key set. */
|
|
252
|
+
const en = {
|
|
253
|
+
waiting: "Waiting for approval",
|
|
254
|
+
"detail.aria": "Approval details",
|
|
255
|
+
escalation: "Tool {toolName} requests privileged execution",
|
|
256
|
+
reject: "Reject",
|
|
257
|
+
allowOnce: "Allow once"
|
|
258
|
+
};
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region lib/types/client/index.js
|
|
261
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, and copy. */
|
|
262
|
+
const inject = [
|
|
263
|
+
"sessions",
|
|
264
|
+
"remote",
|
|
265
|
+
"uiSession",
|
|
266
|
+
"slots",
|
|
267
|
+
"locale"
|
|
268
|
+
];
|
|
269
|
+
const NS = "approval";
|
|
270
|
+
/** Present one request until the user answers or its lifetime ends. */
|
|
271
|
+
async function answerApproval(ctx, owner, request, next, registerPendingInteraction) {
|
|
272
|
+
const sessionId = ctx.sessions.scopeOf(owner);
|
|
273
|
+
if (sessionId === void 0) return next();
|
|
274
|
+
const pending = new PendingApproval(sessionId, {
|
|
275
|
+
toolName: request.toolName,
|
|
276
|
+
...request.callId === void 0 ? {} : { callId: request.callId },
|
|
277
|
+
...request.reason === void 0 ? {} : { reason: request.reason },
|
|
278
|
+
...request.displayReason === void 0 ? {} : { displayReason: request.displayReason },
|
|
279
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
280
|
+
});
|
|
281
|
+
const completed = Promise.withResolvers();
|
|
282
|
+
const remove = registerPendingInteraction(pending, async () => {
|
|
283
|
+
pending.delegate();
|
|
284
|
+
await completed.promise;
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
try {
|
|
288
|
+
return await pending.result;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (pending.isDelegation(error)) return await next();
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
remove();
|
|
295
|
+
completed.resolve();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Install approval copy and the scoped waterfall consumer.
|
|
300
|
+
* @param ctx - Client root context.
|
|
301
|
+
*/
|
|
302
|
+
function apply(ctx) {
|
|
303
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
304
|
+
zh,
|
|
305
|
+
en
|
|
306
|
+
}), "ui-approval: dictionaries");
|
|
307
|
+
ctx.inject(["shortcuts"], (scope) => {
|
|
308
|
+
const t = ctx.locale.bind(NS);
|
|
309
|
+
scope.effect(() => scope.shortcuts.registerFixed({
|
|
310
|
+
id: "approval.allow",
|
|
311
|
+
label: () => t("allowOnce"),
|
|
312
|
+
keys: ["Enter"],
|
|
313
|
+
bindings: [{
|
|
314
|
+
code: "Enter",
|
|
315
|
+
modifiers: []
|
|
316
|
+
}],
|
|
317
|
+
group: "approval"
|
|
318
|
+
}), "ui-approval: fixed allow reference");
|
|
319
|
+
scope.effect(() => scope.shortcuts.registerFixed({
|
|
320
|
+
id: "approval.reject",
|
|
321
|
+
label: () => t("reject"),
|
|
322
|
+
keys: ["Esc"],
|
|
323
|
+
bindings: [{
|
|
324
|
+
code: "Escape",
|
|
325
|
+
modifiers: []
|
|
326
|
+
}],
|
|
327
|
+
group: "approval"
|
|
328
|
+
}), "ui-approval: fixed reject reference");
|
|
329
|
+
});
|
|
330
|
+
const registerPendingInteraction = ctx.uiSession.registerPendingInteraction(() => 0);
|
|
331
|
+
ctx.slots.inject("conversation.composer", () => ctx.slots.register({
|
|
332
|
+
name: "conversation.composer",
|
|
333
|
+
priority: 1,
|
|
334
|
+
select: ({ pendingInteraction }) => pendingInteraction instanceof PendingApproval ? pendingInteraction : null,
|
|
335
|
+
locale: NS,
|
|
336
|
+
inject: () => ({ resolveReason: (reason) => ctx.locale.resolveText(reason) }),
|
|
337
|
+
children: { "conversation.approval.detail": {
|
|
338
|
+
kind: "single",
|
|
339
|
+
scope: "session"
|
|
340
|
+
} }
|
|
341
|
+
}, ApprovalPanel));
|
|
342
|
+
ctx.remote.$on("approval/request", function(request, next) {
|
|
343
|
+
return answerApproval(ctx, this, request, next, registerPendingInteraction);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
exports.apply = apply;
|
|
348
|
+
exports.inject = inject;
|
|
349
|
+
return module.exports;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApprovalComposerProps } from './contract/slots.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Render one pending approval and its optional Tool-owned detail.
|
|
4
|
+
* @param props - selector-matched request and standard Slot props.
|
|
5
|
+
* @returns The approval composer takeover.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ApprovalPanel(props: ApprovalComposerProps): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=ApprovalPanel.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Approval composer and optional correlated-detail contracts. */
|
|
2
|
+
import type { ToolCallId } from '@qilin-agent/llm';
|
|
3
|
+
import type { SessionId } from '@qilin-agent/session/types';
|
|
4
|
+
import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@qilin-agent/client-ui-slots';
|
|
5
|
+
import type { ApprovalKey } from '../locales.ts';
|
|
6
|
+
declare module '@qilin-agent/client-ui-session/client' {
|
|
7
|
+
interface SessionPendingInteractionMap {
|
|
8
|
+
/** Pending approval request. */
|
|
9
|
+
approval: PendingApproval;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare module '@qilin-agent/client-ui-slots' {
|
|
13
|
+
interface LocaleNamespaceMap {
|
|
14
|
+
/** Approval prompt copy. */
|
|
15
|
+
approval: ApprovalKey;
|
|
16
|
+
}
|
|
17
|
+
interface SlotMap {
|
|
18
|
+
/** Optional detail for the Tool call correlated with an approval request. */
|
|
19
|
+
'conversation.approval.detail': {
|
|
20
|
+
kind: 'single';
|
|
21
|
+
scope: 'session';
|
|
22
|
+
owner: ApprovalDetailOwnerProps;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Stable identity handed to an optional approval-detail renderer. */
|
|
27
|
+
export interface ApprovalDetailOwnerProps {
|
|
28
|
+
/** Tool call correlated with the request. */
|
|
29
|
+
callId: ToolCallId;
|
|
30
|
+
}
|
|
31
|
+
/** Client-visible fields of an approval request projected through Remote Events. */
|
|
32
|
+
export interface ApprovalPresentationRequest {
|
|
33
|
+
/** Tool requesting the decision. */
|
|
34
|
+
readonly toolName: string;
|
|
35
|
+
/** Tool call correlated with the request. */
|
|
36
|
+
readonly callId?: ToolCallId;
|
|
37
|
+
/** Human-readable reason supplied by the requester. */
|
|
38
|
+
readonly reason?: string;
|
|
39
|
+
/** Localized presentation copy; the audit reason remains unchanged. */
|
|
40
|
+
readonly displayReason?: {
|
|
41
|
+
readonly en: string;
|
|
42
|
+
readonly [locale: string]: string;
|
|
43
|
+
};
|
|
44
|
+
/** Cancellation projected from the Host waterfall. */
|
|
45
|
+
readonly signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
/** Decisions this interactive Client presentation can return. */
|
|
48
|
+
export type ApprovalDecision = 'allowed-once' | 'rejected';
|
|
49
|
+
/** One answerable Client presentation of a pending Host waterfall. */
|
|
50
|
+
export declare class PendingApproval {
|
|
51
|
+
#private;
|
|
52
|
+
readonly sessionId: SessionId;
|
|
53
|
+
/** Domain discriminator used by Session pending-interaction consumers. */
|
|
54
|
+
readonly kind: 'approval';
|
|
55
|
+
/** Opaque render identity and one-shot remount axis. */
|
|
56
|
+
readonly key: string;
|
|
57
|
+
/** Tool requesting the decision. */
|
|
58
|
+
readonly toolName: string;
|
|
59
|
+
/** Correlated Tool call, when supplied by the asker. */
|
|
60
|
+
readonly callId: ToolCallId | undefined;
|
|
61
|
+
/** Human-readable reason supplied by the asker. */
|
|
62
|
+
readonly reason: string | undefined;
|
|
63
|
+
/** Localized presentation copy, when supplied by the asker. */
|
|
64
|
+
readonly displayReason: ApprovalPresentationRequest['displayReason'];
|
|
65
|
+
/** Result returned by the Remote Event listener to the Host waterfall. */
|
|
66
|
+
readonly result: Promise<ApprovalDecision>;
|
|
67
|
+
/**
|
|
68
|
+
* @param sessionId - Agent/Session identity owning the scoped request.
|
|
69
|
+
* @param request - Host approval request projected through the Remote Event.
|
|
70
|
+
*/
|
|
71
|
+
constructor(sessionId: SessionId, request: ApprovalPresentationRequest);
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the Host waterfall with the user's decision.
|
|
74
|
+
* @param outcome - supported interactive decision.
|
|
75
|
+
*/
|
|
76
|
+
answer(outcome: ApprovalDecision): Promise<void>;
|
|
77
|
+
/** Delegate an unanswered request to the next waterfall listener. */
|
|
78
|
+
delegate(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Test whether a rejection requests waterfall delegation.
|
|
81
|
+
* @param reason - rejection received from {@link PendingApproval.result}.
|
|
82
|
+
* @returns whether {@link PendingApproval.delegate} produced it.
|
|
83
|
+
*/
|
|
84
|
+
isDelegation(reason: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* End an unanswered presentation when its transport, scope, or plugin lifetime ends.
|
|
87
|
+
* @param reason - rejection exposed to the waiting Remote Event listener.
|
|
88
|
+
*/
|
|
89
|
+
abort(reason: unknown): void;
|
|
90
|
+
private finish;
|
|
91
|
+
}
|
|
92
|
+
/** Locale lookup supplied by the approval registration. */
|
|
93
|
+
export interface ApprovalInjected {
|
|
94
|
+
/**
|
|
95
|
+
* Resolve requester-owned copy in the current UI language.
|
|
96
|
+
* @param reason - localized presentation text with an English fallback.
|
|
97
|
+
* @returns text for the active UI language.
|
|
98
|
+
*/
|
|
99
|
+
resolveReason(reason: NonNullable<ApprovalPresentationRequest['displayReason']>): string;
|
|
100
|
+
}
|
|
101
|
+
/** Full props of the approval composer takeover. */
|
|
102
|
+
export type ApprovalComposerProps = PropsRuntime<'conversation.composer'> & PropsRenderSlots<'conversation.approval.detail'> & {
|
|
103
|
+
matched: PendingApproval;
|
|
104
|
+
} & PropsLocale<'approval'> & ApprovalInjected;
|
|
105
|
+
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Browser approval consumer over the existing scoped Remote Event waterfall. */
|
|
2
|
+
import type { Context as ClientContext } from '@qilin-agent/kylin';
|
|
3
|
+
export type { ApprovalComposerProps, ApprovalDecision, ApprovalDetailOwnerProps, ApprovalPresentationRequest, PendingApproval, } from './contract/slots.ts';
|
|
4
|
+
export type { ApprovalKey } from './locales.ts';
|
|
5
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, and copy. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Install approval copy and the scoped waterfall consumer.
|
|
9
|
+
* @param ctx - Client root context.
|
|
10
|
+
*/
|
|
11
|
+
export declare function apply(ctx: ClientContext): void;
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `approval` namespace dictionaries. */
|
|
2
|
+
/** Simplified Chinese dictionary and key-set source of truth. */
|
|
3
|
+
export declare const zh: {
|
|
4
|
+
waiting: string;
|
|
5
|
+
'detail.aria': string;
|
|
6
|
+
escalation: string;
|
|
7
|
+
reject: string;
|
|
8
|
+
allowOnce: string;
|
|
9
|
+
};
|
|
10
|
+
/** Approval dictionary key union. */
|
|
11
|
+
export type ApprovalKey = keyof typeof zh;
|
|
12
|
+
/** English dictionary, checked against the Chinese key set. */
|
|
13
|
+
export declare const en: {
|
|
14
|
+
waiting: string;
|
|
15
|
+
'detail.aria': string;
|
|
16
|
+
escalation: string;
|
|
17
|
+
reject: string;
|
|
18
|
+
allowOnce: string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=locales.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/client-ui-approval",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Approval composer takeover over the scoped Remote Event waterfall",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/client/ui-approval"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./lib/types/client/index.d.ts",
|
|
26
|
+
"default": "./lib/client.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"qilin": {
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"@qilin-agent/api-remotes",
|
|
35
|
+
"@qilin-agent/api-session-controller",
|
|
36
|
+
"@qilin-agent/client-locale",
|
|
37
|
+
"@qilin-agent/client-shortcuts",
|
|
38
|
+
"@qilin-agent/client-ui-conversation",
|
|
39
|
+
"@qilin-agent/client-ui-renderer",
|
|
40
|
+
"@qilin-agent/client-ui-session"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@qilin-agent/kylin": "~4.0.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/react": "~18.3.1",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
53
|
+
"@qilin-agent/api-remotes": "3.1.3",
|
|
54
|
+
"@qilin-agent/api-session-controller": "3.1.3",
|
|
55
|
+
"@qilin-agent/client-ui-conversation": "3.1.3",
|
|
56
|
+
"@qilin-agent/client-shortcuts": "3.1.3",
|
|
57
|
+
"@qilin-agent/client-locale": "3.1.3",
|
|
58
|
+
"@qilin-agent/client-ui-primitives": "3.1.3",
|
|
59
|
+
"@qilin-agent/client-ui-renderer": "3.1.3",
|
|
60
|
+
"@qilin-agent/client-ui-session": "3.1.3",
|
|
61
|
+
"@qilin-agent/client-ui-slots": "3.1.3",
|
|
62
|
+
"@qilin-agent/llm": "3.1.3",
|
|
63
|
+
"@qilin-agent/typert-protocol": "3.1.3",
|
|
64
|
+
"@qilin-agent/session": "3.1.3"
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"lib/index.js",
|
|
68
|
+
"lib/client.js",
|
|
69
|
+
"lib/types/**/*.d.ts"
|
|
70
|
+
],
|
|
71
|
+
"scripts": {
|
|
72
|
+
"bundle": "tsdown",
|
|
73
|
+
"watch": "tsdown --watch"
|
|
74
|
+
}
|
|
6
75
|
}
|